Closes #18153: Introduce virtual circuit types (#18300)

* Closes #18153: Introduce virtual circuit types

* Fix TagTestCase

* Fix GraphQL API test
This commit is contained in:
Jeremy Stretch 2025-01-06 13:37:43 -05:00 committed by GitHub
parent 89d7487197
commit 83d62315cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 643 additions and 49 deletions

View File

@ -18,6 +18,10 @@ The [provider account](./provideraccount.md) with which the virtual circuit is a
The unique identifier assigned to the virtual circuit by its [provider](./provider.md). The unique identifier assigned to the virtual circuit by its [provider](./provider.md).
### Type
The assigned [virtual circuit type](./virtualcircuittype.md).
### Status ### Status
The operational status of the virtual circuit. By default, the following statuses are available: The operational status of the virtual circuit. By default, the following statuses are available:

View File

@ -0,0 +1,13 @@
# Virtual Circuit Types
Like physical [circuits](./circuit.md), [virtual circuits](./virtualcircuit.md) are classified by functional type. These types are completely customizable, and can help categorize circuits by function or technology.
## Fields
### Name
A unique human-friendly name.
### Slug
A unique URL-friendly identifier. (This value can be used for filtering.)

View File

@ -6,7 +6,7 @@ from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, Virtu
from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES
from circuits.models import ( from circuits.models import (
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit, Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
VirtualCircuitTermination, VirtualCircuitTermination, VirtualCircuitType,
) )
from dcim.api.serializers_.device_components import InterfaceSerializer from dcim.api.serializers_.device_components import InterfaceSerializer
from dcim.api.serializers_.cables import CabledObjectSerializer from dcim.api.serializers_.cables import CabledObjectSerializer
@ -25,6 +25,7 @@ __all__ = (
'CircuitTypeSerializer', 'CircuitTypeSerializer',
'VirtualCircuitSerializer', 'VirtualCircuitSerializer',
'VirtualCircuitTerminationSerializer', 'VirtualCircuitTerminationSerializer',
'VirtualCircuitTypeSerializer',
) )
@ -175,17 +176,32 @@ class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
return serializer(obj.member, nested=True, context=context).data return serializer(obj.member, nested=True, context=context).data
class VirtualCircuitTypeSerializer(NetBoxModelSerializer):
# Related object counts
virtual_circuit_count = RelatedObjectCountField('virtual_circuits')
class Meta:
model = VirtualCircuitType
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'virtual_circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')
class VirtualCircuitSerializer(NetBoxModelSerializer): class VirtualCircuitSerializer(NetBoxModelSerializer):
provider_network = ProviderNetworkSerializer(nested=True) provider_network = ProviderNetworkSerializer(nested=True)
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None) provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
type = VirtualCircuitTypeSerializer(nested=True)
status = ChoiceField(choices=CircuitStatusChoices, required=False) status = ChoiceField(choices=CircuitStatusChoices, required=False)
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
class Meta: class Meta:
model = VirtualCircuit model = VirtualCircuit
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'status', 'tenant', 'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'type', 'status',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description') brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description')

View File

@ -19,6 +19,7 @@ router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet
# Virtual circuits # Virtual circuits
router.register('virtual-circuits', views.VirtualCircuitViewSet) router.register('virtual-circuits', views.VirtualCircuitViewSet)
router.register('virtual-circuit-types', views.VirtualCircuitTypeViewSet)
router.register('virtual-circuit-terminations', views.VirtualCircuitTerminationViewSet) router.register('virtual-circuit-terminations', views.VirtualCircuitTerminationViewSet)
app_name = 'circuits-api' app_name = 'circuits-api'

View File

@ -95,6 +95,16 @@ class ProviderNetworkViewSet(NetBoxModelViewSet):
filterset_class = filtersets.ProviderNetworkFilterSet filterset_class = filtersets.ProviderNetworkFilterSet
#
# Virtual circuit types
#
class VirtualCircuitTypeViewSet(NetBoxModelViewSet):
queryset = VirtualCircuitType.objects.all()
serializer_class = serializers.VirtualCircuitTypeSerializer
filterset_class = filtersets.VirtualCircuitTypeFilterSet
# #
# Virtual circuits # Virtual circuits
# #

View File

@ -25,6 +25,7 @@ __all__ = (
'ProviderFilterSet', 'ProviderFilterSet',
'VirtualCircuitFilterSet', 'VirtualCircuitFilterSet',
'VirtualCircuitTerminationFilterSet', 'VirtualCircuitTerminationFilterSet',
'VirtualCircuitTypeFilterSet',
) )
@ -462,6 +463,13 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
) )
class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta:
model = VirtualCircuitType
fields = ('id', 'name', 'slug', 'color', 'description')
class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter( provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_network__provider', field_name='provider_network__provider',
@ -489,6 +497,16 @@ class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
queryset=ProviderNetwork.objects.all(), queryset=ProviderNetwork.objects.all(),
label=_('Provider network (ID)'), label=_('Provider network (ID)'),
) )
type_id = django_filters.ModelMultipleChoiceFilter(
queryset=VirtualCircuitType.objects.all(),
label=_('Virtual circuit type (ID)'),
)
type = django_filters.ModelMultipleChoiceFilter(
field_name='type__slug',
queryset=VirtualCircuitType.objects.all(),
to_field_name='slug',
label=_('Virtual circuit type (slug)'),
)
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
null_value=None null_value=None

View File

@ -32,6 +32,7 @@ __all__ = (
'ProviderNetworkBulkEditForm', 'ProviderNetworkBulkEditForm',
'VirtualCircuitBulkEditForm', 'VirtualCircuitBulkEditForm',
'VirtualCircuitTerminationBulkEditForm', 'VirtualCircuitTerminationBulkEditForm',
'VirtualCircuitTypeBulkEditForm',
) )
@ -297,6 +298,24 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = ('priority',) nullable_fields = ('priority',)
class VirtualCircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
color = ColorField(
label=_('Color'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
model = VirtualCircuitType
fieldsets = (
FieldSet('color', 'description'),
)
nullable_fields = ('color', 'description')
class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm): class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
provider_network = DynamicModelChoiceField( provider_network = DynamicModelChoiceField(
label=_('Provider network'), label=_('Provider network'),
@ -308,6 +327,11 @@ class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
queryset=ProviderAccount.objects.all(), queryset=ProviderAccount.objects.all(),
required=False required=False
) )
type = DynamicModelChoiceField(
label=_('Type'),
queryset=VirtualCircuitType.objects.all(),
required=False
)
status = forms.ChoiceField( status = forms.ChoiceField(
label=_('Status'), label=_('Status'),
choices=add_blank_choice(CircuitStatusChoices), choices=add_blank_choice(CircuitStatusChoices),

View File

@ -24,6 +24,7 @@ __all__ = (
'VirtualCircuitImportForm', 'VirtualCircuitImportForm',
'VirtualCircuitTerminationImportForm', 'VirtualCircuitTerminationImportForm',
'VirtualCircuitTerminationImportRelatedForm', 'VirtualCircuitTerminationImportRelatedForm',
'VirtualCircuitTypeImportForm',
) )
@ -194,6 +195,14 @@ class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
fields = ('member_type', 'member_id', 'group', 'priority') fields = ('member_type', 'member_id', 'group', 'priority')
class VirtualCircuitTypeImportForm(NetBoxModelImportForm):
slug = SlugField()
class Meta:
model = VirtualCircuitType
fields = ('name', 'slug', 'color', 'description', 'tags')
class VirtualCircuitImportForm(NetBoxModelImportForm): class VirtualCircuitImportForm(NetBoxModelImportForm):
provider_network = CSVModelChoiceField( provider_network = CSVModelChoiceField(
label=_('Provider network'), label=_('Provider network'),
@ -208,6 +217,12 @@ class VirtualCircuitImportForm(NetBoxModelImportForm):
help_text=_('Assigned provider account (if any)'), help_text=_('Assigned provider account (if any)'),
required=False required=False
) )
type = CSVModelChoiceField(
label=_('Type'),
queryset=VirtualCircuitType.objects.all(),
to_field_name='name',
help_text=_('Type of virtual circuit')
)
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'), label=_('Status'),
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
@ -224,7 +239,8 @@ class VirtualCircuitImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = VirtualCircuit model = VirtualCircuit
fields = [ fields = [
'cid', 'provider_network', 'provider_account', 'status', 'tenant', 'description', 'comments', 'tags', 'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'comments',
'tags',
] ]

View File

@ -27,6 +27,7 @@ __all__ = (
'ProviderNetworkFilterForm', 'ProviderNetworkFilterForm',
'VirtualCircuitFilterForm', 'VirtualCircuitFilterForm',
'VirtualCircuitTerminationFilterForm', 'VirtualCircuitTerminationFilterForm',
'VirtualCircuitTypeFilterForm',
) )
@ -302,12 +303,26 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm):
model = VirtualCircuitType
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('color', name=_('Attributes')),
)
tag = TagFilterField(model)
color = ColorField(
label=_('Color'),
required=False
)
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = VirtualCircuit model = VirtualCircuit
fieldsets = ( fieldsets = (
FieldSet('q', 'filter_id', 'tag'), FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')), FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
FieldSet('status', name=_('Attributes')), FieldSet('type', 'status', name=_('Attributes')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id') selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
@ -332,6 +347,11 @@ class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBox
}, },
label=_('Provider network') label=_('Provider network')
) )
type_id = DynamicModelMultipleChoiceField(
queryset=VirtualCircuitType.objects.all(),
required=False,
label=_('Type')
)
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'), label=_('Status'),
choices=CircuitStatusChoices, choices=CircuitStatusChoices,

View File

@ -31,6 +31,7 @@ __all__ = (
'ProviderNetworkForm', 'ProviderNetworkForm',
'VirtualCircuitForm', 'VirtualCircuitForm',
'VirtualCircuitTerminationForm', 'VirtualCircuitTerminationForm',
'VirtualCircuitTypeForm',
) )
@ -305,6 +306,20 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
self.instance.member = self.cleaned_data.get('member') self.instance.member = self.cleaned_data.get('member')
class VirtualCircuitTypeForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
FieldSet('name', 'slug', 'color', 'description', 'tags'),
)
class Meta:
model = VirtualCircuitType
fields = [
'name', 'slug', 'color', 'description', 'tags',
]
class VirtualCircuitForm(TenancyForm, NetBoxModelForm): class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
provider_network = DynamicModelChoiceField( provider_network = DynamicModelChoiceField(
label=_('Provider network'), label=_('Provider network'),
@ -316,11 +331,16 @@ class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
queryset=ProviderAccount.objects.all(), queryset=ProviderAccount.objects.all(),
required=False required=False
) )
type = DynamicModelChoiceField(
queryset=VirtualCircuitType.objects.all(),
quick_add=True
)
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
FieldSet( FieldSet(
'provider_network', 'provider_account', 'cid', 'status', 'description', 'tags', name=_('Virtual circuit'), 'provider_network', 'provider_account', 'cid', 'type', 'status', 'description', 'tags',
name=_('Virtual circuit'),
), ),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
@ -328,7 +348,7 @@ class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
class Meta: class Meta:
model = VirtualCircuit model = VirtualCircuit
fields = [ fields = [
'cid', 'provider_network', 'provider_account', 'status', 'description', 'tenant_group', 'tenant', 'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant',
'comments', 'tags', 'comments', 'tags',
] ]

View File

@ -14,6 +14,7 @@ __all__ = (
'ProviderNetworkFilter', 'ProviderNetworkFilter',
'VirtualCircuitFilter', 'VirtualCircuitFilter',
'VirtualCircuitTerminationFilter', 'VirtualCircuitTerminationFilter',
'VirtualCircuitTypeFilter',
) )
@ -65,6 +66,12 @@ class ProviderNetworkFilter(BaseFilterMixin):
pass pass
@strawberry_django.filter(models.VirtualCircuitType, lookups=True)
@autotype_decorator(filtersets.VirtualCircuitTypeFilterSet)
class VirtualCircuitTypeFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VirtualCircuit, lookups=True) @strawberry_django.filter(models.VirtualCircuit, lookups=True)
@autotype_decorator(filtersets.VirtualCircuitFilterSet) @autotype_decorator(filtersets.VirtualCircuitFilterSet)
class VirtualCircuitFilter(BaseFilterMixin): class VirtualCircuitFilter(BaseFilterMixin):

View File

@ -37,3 +37,6 @@ class CircuitsQuery:
virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field() virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field()
virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field() virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field()
virtual_circuit_type: VirtualCircuitTypeType = strawberry_django.field()
virtual_circuit_type_list: List[VirtualCircuitTypeType] = strawberry_django.field()

View File

@ -21,6 +21,7 @@ __all__ = (
'ProviderNetworkType', 'ProviderNetworkType',
'VirtualCircuitTerminationType', 'VirtualCircuitTerminationType',
'VirtualCircuitType', 'VirtualCircuitType',
'VirtualCircuitTypeType',
) )
@ -130,6 +131,17 @@ class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
return self.member return self.member
@strawberry_django.type(
models.VirtualCircuitType,
fields='__all__',
filters=VirtualCircuitTypeFilter
)
class VirtualCircuitTypeType(OrganizationalObjectType):
color: str
virtual_circuits: List[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type( @strawberry_django.type(
models.VirtualCircuitTermination, models.VirtualCircuitTermination,
fields='__all__', fields='__all__',
@ -154,6 +166,9 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
class VirtualCircuitType(NetBoxObjectType): class VirtualCircuitType(NetBoxObjectType):
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"]) provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
provider_account: ProviderAccountType | None provider_account: ProviderAccountType | None
type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field(
select_related=["type"]
)
tenant: TenantType | None tenant: TenantType | None
terminations: List[VirtualCircuitTerminationType] terminations: List[VirtualCircuitTerminationType]

View File

@ -2,6 +2,7 @@ import django.db.models.deletion
import taggit.managers import taggit.managers
from django.db import migrations, models from django.db import migrations, models
import utilities.fields
import utilities.json import utilities.json
@ -14,6 +15,29 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.CreateModel(
name='VirtualCircuitType',
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
)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)),
('color', utilities.fields.ColorField(blank=True, max_length=6)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'virtual circuit type',
'verbose_name_plural': 'virtual circuit types',
'ordering': ('name',),
},
),
migrations.CreateModel( migrations.CreateModel(
name='VirtualCircuit', name='VirtualCircuit',
fields=[ fields=[
@ -47,6 +71,14 @@ class Migration(migrations.Migration):
), ),
), ),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
(
'type',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name='virtual_circuits',
to='circuits.virtualcircuittype'
)
),
( (
'tenant', 'tenant',
models.ForeignKey( models.ForeignKey(

View File

@ -0,0 +1,23 @@
from django.utils.translation import gettext_lazy as _
from netbox.models import OrganizationalModel
from utilities.fields import ColorField
__all__ = (
'BaseCircuitType',
)
class BaseCircuitType(OrganizationalModel):
"""
Abstract base model to represent a type of physical or virtual circuit.
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band".
"""
color = ColorField(
verbose_name=_('color'),
blank=True
)
class Meta:
abstract = True

View File

@ -13,7 +13,7 @@ from netbox.models.mixins import DistanceMixin
from netbox.models.features import ( from netbox.models.features import (
ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin, ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin,
) )
from utilities.fields import ColorField from .base import BaseCircuitType
__all__ = ( __all__ = (
'Circuit', 'Circuit',
@ -24,16 +24,11 @@ __all__ = (
) )
class CircuitType(OrganizationalModel): class CircuitType(BaseCircuitType):
""" """
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band". "Long Haul," "Metro," or "Out-of-Band".
""" """
color = ColorField(
verbose_name=_('color'),
blank=True
)
class Meta: class Meta:
ordering = ('name',) ordering = ('name',)
verbose_name = _('circuit type') verbose_name = _('circuit type')
@ -64,7 +59,7 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel)
null=True null=True
) )
type = models.ForeignKey( type = models.ForeignKey(
to='CircuitType', to='circuits.CircuitType',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='circuits' related_name='circuits'
) )

View File

@ -9,13 +9,26 @@ from django.utils.translation import gettext_lazy as _
from circuits.choices import * from circuits.choices import *
from netbox.models import ChangeLoggedModel, PrimaryModel from netbox.models import ChangeLoggedModel, PrimaryModel
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin
from .base import BaseCircuitType
__all__ = ( __all__ = (
'VirtualCircuit', 'VirtualCircuit',
'VirtualCircuitTermination', 'VirtualCircuitTermination',
'VirtualCircuitType',
) )
class VirtualCircuitType(BaseCircuitType):
"""
Like physical circuits, virtual circuits can be organized by their functional role. For example, a user might wish
to categorize virtual circuits by their technological nature or by product name.
"""
class Meta:
ordering = ('name',)
verbose_name = _('virtual circuit type')
verbose_name_plural = _('virtual circuit types')
class VirtualCircuit(PrimaryModel): class VirtualCircuit(PrimaryModel):
""" """
A virtual connection between two or more endpoints, delivered across one or more physical circuits. A virtual connection between two or more endpoints, delivered across one or more physical circuits.
@ -37,6 +50,11 @@ class VirtualCircuit(PrimaryModel):
blank=True, blank=True,
null=True null=True
) )
type = models.ForeignKey(
to='circuits.VirtualCircuitType',
on_delete=models.PROTECT,
related_name='virtual_circuits'
)
status = models.CharField( status = models.CharField(
verbose_name=_('status'), verbose_name=_('status'),
max_length=50, max_length=50,
@ -63,6 +81,7 @@ class VirtualCircuit(PrimaryModel):
) )
prerequisite_models = ( prerequisite_models = (
'circuits.ProviderNetwork', 'circuits.ProviderNetwork',
'circuits.VirtualCircuitType',
) )
class Meta: class Meta:

View File

@ -100,3 +100,14 @@ class VirtualCircuitTerminationIndex(SearchIndex):
('description', 500), ('description', 500),
) )
display_attrs = ('virtual_circuit', 'role', 'description') display_attrs = ('virtual_circuit', 'role', 'description')
@register_search
class VirtualCircuitTypeIndex(SearchIndex):
model = models.VirtualCircuitType
fields = (
('name', 100),
('slug', 110),
('description', 500),
)
display_attrs = ('description',)

View File

@ -45,7 +45,7 @@ class CircuitTypeTable(NetBoxTable):
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated', 'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated',
'actions', 'actions',
) )
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug') default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
@ -61,6 +61,10 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
linkify=True, linkify=True,
verbose_name=_('Account') verbose_name=_('Account')
) )
type = tables.Column(
verbose_name=_('Type'),
linkify=True
)
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
termination_a = columns.TemplateColumn( termination_a = columns.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK, template_code=CIRCUITTERMINATION_LINK,

View File

@ -8,9 +8,34 @@ from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
__all__ = ( __all__ = (
'VirtualCircuitTable', 'VirtualCircuitTable',
'VirtualCircuitTerminationTable', 'VirtualCircuitTerminationTable',
'VirtualCircuitTypeTable',
) )
class VirtualCircuitTypeTable(NetBoxTable):
name = tables.Column(
linkify=True,
verbose_name=_('Name'),
)
color = columns.ColorColumn()
tags = columns.TagColumn(
url_name='circuits:virtualcircuittype_list'
)
virtual_circuit_count = columns.LinkedCountColumn(
viewname='circuits:virtualcircuit_list',
url_params={'type_id': 'pk'},
verbose_name=_('Circuits')
)
class Meta(NetBoxTable.Meta):
model = VirtualCircuitType
fields = (
'pk', 'id', 'name', 'virtual_circuit_count', 'color', 'description', 'slug', 'tags', 'created',
'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'virtual_circuit_count', 'color', 'description')
class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
cid = tables.Column( cid = tables.Column(
linkify=True, linkify=True,
@ -29,6 +54,10 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
linkify=True, linkify=True,
verbose_name=_('Account') verbose_name=_('Account')
) )
type = tables.Column(
verbose_name=_('Type'),
linkify=True
)
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
termination_count = columns.LinkedCountColumn( termination_count = columns.LinkedCountColumn(
viewname='circuits:virtualcircuittermination_list', viewname='circuits:virtualcircuittermination_list',
@ -45,12 +74,12 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = VirtualCircuit model = VirtualCircuit
fields = ( fields = (
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'status', 'tenant', 'tenant_group', 'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
'description', 'comments', 'tags', 'created', 'last_updated', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'status', 'tenant', 'termination_count', 'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
'description', 'termination_count', 'description',
) )

View File

@ -409,6 +409,38 @@ class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
} }
class VirtualCircuitTypeTest(APIViewTestCases.APIViewTestCase):
model = VirtualCircuitType
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url', 'virtual_circuit_count']
create_data = (
{
'name': 'Virtual Circuit Type 4',
'slug': 'virtual-circuit-type-4',
},
{
'name': 'Virtual Circuit Type 5',
'slug': 'virtual-circuit-type-5',
},
{
'name': 'Virtual Circuit Type 6',
'slug': 'virtual-circuit-type-6',
},
)
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
class VirtualCircuitTest(APIViewTestCases.APIViewTestCase): class VirtualCircuitTest(APIViewTestCases.APIViewTestCase):
model = VirtualCircuit model = VirtualCircuit
brief_fields = ['cid', 'description', 'display', 'id', 'provider_network', 'url'] brief_fields = ['cid', 'description', 'display', 'id', 'provider_network', 'url']
@ -421,21 +453,28 @@ class VirtualCircuitTest(APIViewTestCases.APIViewTestCase):
provider = Provider.objects.create(name='Provider 1', slug='provider-1') provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1') provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1') provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = ( virtual_circuits = (
VirtualCircuit( VirtualCircuit(
provider_network=provider_network, provider_network=provider_network,
provider_account=provider_account, provider_account=provider_account,
type=virtual_circuit_type,
cid='Virtual Circuit 1' cid='Virtual Circuit 1'
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_network, provider_network=provider_network,
provider_account=provider_account, provider_account=provider_account,
type=virtual_circuit_type,
cid='Virtual Circuit 2' cid='Virtual Circuit 2'
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_network, provider_network=provider_network,
provider_account=provider_account, provider_account=provider_account,
type=virtual_circuit_type,
cid='Virtual Circuit 3' cid='Virtual Circuit 3'
), ),
) )
@ -446,18 +485,21 @@ class VirtualCircuitTest(APIViewTestCases.APIViewTestCase):
'cid': 'Virtual Circuit 4', 'cid': 'Virtual Circuit 4',
'provider_network': provider_network.pk, 'provider_network': provider_network.pk,
'provider_account': provider_account.pk, 'provider_account': provider_account.pk,
'type': virtual_circuit_type.pk,
'status': CircuitStatusChoices.STATUS_PLANNED, 'status': CircuitStatusChoices.STATUS_PLANNED,
}, },
{ {
'cid': 'Virtual Circuit 5', 'cid': 'Virtual Circuit 5',
'provider_network': provider_network.pk, 'provider_network': provider_network.pk,
'provider_account': provider_account.pk, 'provider_account': provider_account.pk,
'type': virtual_circuit_type.pk,
'status': CircuitStatusChoices.STATUS_PLANNED, 'status': CircuitStatusChoices.STATUS_PLANNED,
}, },
{ {
'cid': 'Virtual Circuit 6', 'cid': 'Virtual Circuit 6',
'provider_network': provider_network.pk, 'provider_network': provider_network.pk,
'provider_account': provider_account.pk, 'provider_account': provider_account.pk,
'type': virtual_circuit_type.pk,
'status': CircuitStatusChoices.STATUS_PLANNED, 'status': CircuitStatusChoices.STATUS_PLANNED,
}, },
] ]
@ -563,27 +605,35 @@ class VirtualCircuitTerminationTest(APIViewTestCases.APIViewTestCase):
provider = Provider.objects.create(name='Provider 1', slug='provider-1') provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1') provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1') provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = ( virtual_circuits = (
VirtualCircuit( VirtualCircuit(
provider_network=provider_network, provider_network=provider_network,
provider_account=provider_account, provider_account=provider_account,
cid='Virtual Circuit 1' cid='Virtual Circuit 1',
type=virtual_circuit_type
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_network, provider_network=provider_network,
provider_account=provider_account, provider_account=provider_account,
cid='Virtual Circuit 2' cid='Virtual Circuit 2',
type=virtual_circuit_type
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_network, provider_network=provider_network,
provider_account=provider_account, provider_account=provider_account,
cid='Virtual Circuit 3' cid='Virtual Circuit 3',
type=virtual_circuit_type
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_network, provider_network=provider_network,
provider_account=provider_account, provider_account=provider_account,
cid='Virtual Circuit 4' cid='Virtual Circuit 4',
type=virtual_circuit_type
), ),
) )
VirtualCircuit.objects.bulk_create(virtual_circuits) VirtualCircuit.objects.bulk_create(virtual_circuits)

View File

@ -656,12 +656,12 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
Provider(name='Provider 2', slug='provider-2'), Provider(name='Provider 2', slug='provider-2'),
Provider(name='Provider 3', slug='provider-3'), Provider(name='Provider 3', slug='provider-3'),
)) ))
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuits = ( circuits = (
Circuit(cid='Circuit 1', provider=providers[0], type=circuittype), Circuit(cid='Circuit 1', provider=providers[0], type=circuit_type),
Circuit(cid='Circuit 2', provider=providers[1], type=circuittype), Circuit(cid='Circuit 2', provider=providers[1], type=circuit_type),
Circuit(cid='Circuit 3', provider=providers[2], type=circuittype), Circuit(cid='Circuit 3', provider=providers[2], type=circuit_type),
) )
Circuit.objects.bulk_create(circuits) Circuit.objects.bulk_create(circuits)
@ -672,18 +672,25 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
ProviderNetwork.objects.bulk_create(provider_networks) ProviderNetwork.objects.bulk_create(provider_networks)
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = ( virtual_circuits = (
VirtualCircuit( VirtualCircuit(
provider_network=provider_networks[0], provider_network=provider_networks[0],
cid='Virtual Circuit 1' cid='Virtual Circuit 1',
type=virtual_circuit_type
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_networks[1], provider_network=provider_networks[1],
cid='Virtual Circuit 2' cid='Virtual Circuit 2',
type=virtual_circuit_type
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_networks[2], provider_network=provider_networks[2],
cid='Virtual Circuit 3' cid='Virtual Circuit 3',
type=virtual_circuit_type
), ),
) )
VirtualCircuit.objects.bulk_create(virtual_circuits) VirtualCircuit.objects.bulk_create(virtual_circuits)
@ -837,6 +844,36 @@ class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class VirtualCircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualCircuitType.objects.all()
filterset = VirtualCircuitTypeFilterSet
@classmethod
def setUpTestData(cls):
VirtualCircuitType.objects.bulk_create((
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1', description='foobar1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2', description='foobar2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
))
def test_q(self):
params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_name(self):
params = {'name': ['Virtual Circuit Type 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_slug(self):
params = {'slug': ['virtual-circuit-type-1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests): class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualCircuit.objects.all() queryset = VirtualCircuit.objects.all()
filterset = VirtualCircuitFilterSet filterset = VirtualCircuitFilterSet
@ -880,12 +917,20 @@ class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
ProviderNetwork.objects.bulk_create(provider_networks) ProviderNetwork.objects.bulk_create(provider_networks)
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
virutal_circuits = ( virutal_circuits = (
VirtualCircuit( VirtualCircuit(
provider_network=provider_networks[0], provider_network=provider_networks[0],
provider_account=provider_accounts[0], provider_account=provider_accounts[0],
tenant=tenants[0], tenant=tenants[0],
cid='Virtual Circuit 1', cid='Virtual Circuit 1',
type=virtual_circuit_types[0],
status=CircuitStatusChoices.STATUS_PLANNED, status=CircuitStatusChoices.STATUS_PLANNED,
description='virtualcircuit1', description='virtualcircuit1',
), ),
@ -894,6 +939,7 @@ class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
provider_account=provider_accounts[1], provider_account=provider_accounts[1],
tenant=tenants[1], tenant=tenants[1],
cid='Virtual Circuit 2', cid='Virtual Circuit 2',
type=virtual_circuit_types[1],
status=CircuitStatusChoices.STATUS_ACTIVE, status=CircuitStatusChoices.STATUS_ACTIVE,
description='virtualcircuit2', description='virtualcircuit2',
), ),
@ -902,6 +948,7 @@ class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
provider_account=provider_accounts[2], provider_account=provider_accounts[2],
tenant=tenants[2], tenant=tenants[2],
cid='Virtual Circuit 3', cid='Virtual Circuit 3',
type=virtual_circuit_types[2],
status=CircuitStatusChoices.STATUS_DEPROVISIONING, status=CircuitStatusChoices.STATUS_DEPROVISIONING,
description='virtualcircuit3', description='virtualcircuit3',
), ),
@ -933,6 +980,13 @@ class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]} params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
virtual_circuit_types = VirtualCircuitType.objects.all()[:2]
params = {'type_id': [virtual_circuit_types[0].pk, virtual_circuit_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'type': [virtual_circuit_types[0].slug, virtual_circuit_types[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_status(self): def test_status(self):
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]} params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -1029,22 +1083,29 @@ class VirtualCircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
ProviderAccount(provider=providers[2], account='Provider Account 3'), ProviderAccount(provider=providers[2], account='Provider Account 3'),
) )
ProviderAccount.objects.bulk_create(provider_accounts) ProviderAccount.objects.bulk_create(provider_accounts)
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = ( virtual_circuits = (
VirtualCircuit( VirtualCircuit(
provider_network=provider_networks[0], provider_network=provider_networks[0],
provider_account=provider_accounts[0], provider_account=provider_accounts[0],
cid='Virtual Circuit 1' cid='Virtual Circuit 1',
type=virtual_circuit_type
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_networks[1], provider_network=provider_networks[1],
provider_account=provider_accounts[1], provider_account=provider_accounts[1],
cid='Virtual Circuit 2' cid='Virtual Circuit 2',
type=virtual_circuit_type
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_networks[2], provider_network=provider_networks[2],
provider_account=provider_accounts[2], provider_account=provider_accounts[2],
cid='Virtual Circuit 3' cid='Virtual Circuit 3',
type=virtual_circuit_type
), ),
) )
VirtualCircuit.objects.bulk_create(virtual_circuits) VirtualCircuit.objects.bulk_create(virtual_circuits)

View File

@ -543,6 +543,47 @@ class CircuitGroupAssignmentTestCase(
} }
class VirtualCircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = VirtualCircuitType
@classmethod
def setUpTestData(cls):
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='circuit-type-2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='circuit-type-3'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Virtual Circuit Type X',
'slug': 'virtual-circuit-type-x',
'description': 'A new virtual circuit type',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,slug",
"Virtual Circuit Type 4,circuit-type-4",
"Virtual Circuit Type 5,circuit-type-5",
"Virtual Circuit Type 6,circuit-type-6",
)
cls.csv_update_data = (
"id,name,description",
f"{virtual_circuit_types[0].pk},Virtual Circuit Type 7,New description7",
f"{virtual_circuit_types[1].pk},Virtual Circuit Type 8,New description8",
f"{virtual_circuit_types[2].pk},Virtual Circuit Type 9,New description9",
)
cls.bulk_edit_data = {
'description': 'Foo',
}
class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = VirtualCircuit model = VirtualCircuit
@ -566,22 +607,30 @@ class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
ProviderAccount(provider=provider, account='Provider Account 2'), ProviderAccount(provider=provider, account='Provider Account 2'),
) )
ProviderAccount.objects.bulk_create(provider_accounts) ProviderAccount.objects.bulk_create(provider_accounts)
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
virtual_circuits = ( virtual_circuits = (
VirtualCircuit( VirtualCircuit(
provider_network=provider_networks[0], provider_network=provider_networks[0],
provider_account=provider_accounts[0], provider_account=provider_accounts[0],
cid='Virtual Circuit 1' cid='Virtual Circuit 1',
type=virtual_circuit_types[0]
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_networks[0], provider_network=provider_networks[0],
provider_account=provider_accounts[0], provider_account=provider_accounts[0],
cid='Virtual Circuit 2' cid='Virtual Circuit 2',
type=virtual_circuit_types[0]
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_networks[0], provider_network=provider_networks[0],
provider_account=provider_accounts[0], provider_account=provider_accounts[0],
cid='Virtual Circuit 3' cid='Virtual Circuit 3',
type=virtual_circuit_types[0]
), ),
) )
VirtualCircuit.objects.bulk_create(virtual_circuits) VirtualCircuit.objects.bulk_create(virtual_circuits)
@ -600,6 +649,7 @@ class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'cid': 'Virtual Circuit X', 'cid': 'Virtual Circuit X',
'provider_network': provider_networks[1].pk, 'provider_network': provider_networks[1].pk,
'provider_account': provider_accounts[1].pk, 'provider_account': provider_accounts[1].pk,
'type': virtual_circuit_types[1].pk,
'status': CircuitStatusChoices.STATUS_PLANNED, 'status': CircuitStatusChoices.STATUS_PLANNED,
'description': 'A new virtual circuit', 'description': 'A new virtual circuit',
'comments': 'Some comments', 'comments': 'Some comments',
@ -607,22 +657,41 @@ class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"cid,provider_network,provider_account,status", "cid,provider_network,provider_account,type,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 4,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
f"Virtual Circuit 6,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}", f"{CircuitStatusChoices.STATUS_PLANNED}"
),
(
f"Virtual Circuit 5,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
f"{CircuitStatusChoices.STATUS_PLANNED}"
),
(
f"Virtual Circuit 6,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
f"{CircuitStatusChoices.STATUS_PLANNED}"
),
) )
cls.csv_update_data = ( cls.csv_update_data = (
"id,cid,description,status", "id,cid,description,type,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[0].pk},Virtual Circuit A,New description,{virtual_circuit_types[1].name},"
f"{virtual_circuits[2].pk},Virtual Circuit C,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}", f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
),
(
f"{virtual_circuits[1].pk},Virtual Circuit B,New description,{virtual_circuit_types[1].name},"
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
),
(
f"{virtual_circuits[2].pk},Virtual Circuit C,New description,{virtual_circuit_types[1].name},"
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
),
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {
'provider_network': provider_networks[1].pk, 'provider_network': provider_networks[1].pk,
'provider_account': provider_accounts[1].pk, 'provider_account': provider_accounts[1].pk,
'type': virtual_circuit_types[1].pk,
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED, 'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
'description': 'New description', 'description': 'New description',
'comments': 'New comments', 'comments': 'New comments',
@ -636,6 +705,7 @@ class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
{{ {{
"cid": "Virtual Circuit 7", "cid": "Virtual Circuit 7",
"provider_network": "Provider Network 1", "provider_network": "Provider Network 1",
"type": "Virtual Circuit Type 1",
"status": "active", "status": "active",
"terminations": [ "terminations": [
{{ {{
@ -774,27 +844,35 @@ class VirtualCircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase)
provider = Provider.objects.create(name='Provider 1', slug='provider-1') provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1') provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1') provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = ( virtual_circuits = (
VirtualCircuit( VirtualCircuit(
provider_network=provider_network, provider_network=provider_network,
provider_account=provider_account, provider_account=provider_account,
cid='Virtual Circuit 1' cid='Virtual Circuit 1',
type=virtual_circuit_type
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_network, provider_network=provider_network,
provider_account=provider_account, provider_account=provider_account,
cid='Virtual Circuit 2' cid='Virtual Circuit 2',
type=virtual_circuit_type
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_network, provider_network=provider_network,
provider_account=provider_account, provider_account=provider_account,
cid='Virtual Circuit 3' cid='Virtual Circuit 3',
type=virtual_circuit_type
), ),
VirtualCircuit( VirtualCircuit(
provider_network=provider_network, provider_network=provider_network,
provider_account=provider_account, provider_account=provider_account,
cid='Virtual Circuit 4' cid='Virtual Circuit 4',
type=virtual_circuit_type
), ),
) )
VirtualCircuit.objects.bulk_create(virtual_circuits) VirtualCircuit.objects.bulk_create(virtual_circuits)

View File

@ -42,6 +42,9 @@ urlpatterns = [
path('virtual-circuits/delete/', views.VirtualCircuitBulkDeleteView.as_view(), name='virtualcircuit_bulk_delete'), path('virtual-circuits/delete/', views.VirtualCircuitBulkDeleteView.as_view(), name='virtualcircuit_bulk_delete'),
path('virtual-circuits/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuit'))), path('virtual-circuits/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuit'))),
path('virtual-circuit-types/', include(get_model_urls('circuits', 'virtualcircuittype', detail=False))),
path('virtual-circuit-types/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuittype'))),
# Virtual circuit terminations # Virtual circuit terminations
path( path(
'virtual-circuit-terminations/', 'virtual-circuit-terminations/',

View File

@ -579,6 +579,67 @@ class CircuitGroupAssignmentBulkDeleteView(generic.BulkDeleteView):
table = tables.CircuitGroupAssignmentTable table = tables.CircuitGroupAssignmentTable
#
# Virtual circuit Types
#
@register_model_view(VirtualCircuitType, 'list', path='', detail=False)
class VirtualCircuitTypeListView(generic.ObjectListView):
queryset = VirtualCircuitType.objects.annotate(
virtual_circuit_count=count_related(VirtualCircuit, 'type')
)
filterset = filtersets.VirtualCircuitTypeFilterSet
filterset_form = forms.VirtualCircuitTypeFilterForm
table = tables.VirtualCircuitTypeTable
@register_model_view(VirtualCircuitType)
class VirtualCircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VirtualCircuitType.objects.all()
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
@register_model_view(VirtualCircuitType, 'add', detail=False)
@register_model_view(VirtualCircuitType, 'edit')
class VirtualCircuitTypeEditView(generic.ObjectEditView):
queryset = VirtualCircuitType.objects.all()
form = forms.VirtualCircuitTypeForm
@register_model_view(VirtualCircuitType, 'delete')
class VirtualCircuitTypeDeleteView(generic.ObjectDeleteView):
queryset = VirtualCircuitType.objects.all()
@register_model_view(VirtualCircuitType, 'bulk_import', detail=False)
class VirtualCircuitTypeBulkImportView(generic.BulkImportView):
queryset = VirtualCircuitType.objects.all()
model_form = forms.VirtualCircuitTypeImportForm
@register_model_view(VirtualCircuitType, 'bulk_edit', path='edit', detail=False)
class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
queryset = VirtualCircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
filterset = filtersets.VirtualCircuitTypeFilterSet
table = tables.VirtualCircuitTypeTable
form = forms.VirtualCircuitTypeBulkEditForm
@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualCircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
filterset = filtersets.VirtualCircuitTypeFilterSet
table = tables.VirtualCircuitTypeTable
# #
# Virtual circuits # Virtual circuits
# #

View File

@ -1170,6 +1170,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
'virtualchassis', 'virtualchassis',
'virtualcircuit', 'virtualcircuit',
'virtualcircuittermination', 'virtualcircuittermination',
'virtualcircuittype',
'virtualdevicecontext', 'virtualdevicecontext',
'virtualdisk', 'virtualdisk',
'virtualmachine', 'virtualmachine',

View File

@ -286,6 +286,7 @@ CIRCUITS_MENU = Menu(
label=_('Virtual Circuits'), label=_('Virtual Circuits'),
items=( items=(
get_model_item('circuits', 'virtualcircuit', _('Virtual Circuits')), get_model_item('circuits', 'virtualcircuit', _('Virtual Circuits')),
get_model_item('circuits', 'virtualcircuittype', _('Virtual Circuit Types')),
get_model_item('circuits', 'virtualcircuittermination', _('Virtual Circuit Terminations')), get_model_item('circuits', 'virtualcircuittermination', _('Virtual Circuit Terminations')),
), ),
), ),

View File

@ -35,6 +35,10 @@
<th scope="row">{% trans "Circuit ID" %}</th> <th scope="row">{% trans "Circuit ID" %}</th>
<td>{{ object.cid }}</td> <td>{{ object.cid }}</td>
</tr> </tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.type|linkify }}</td>
</tr>
<tr> <tr>
<th scope="row">{% trans "Status" %}</th> <th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td> <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>

View File

@ -0,0 +1,55 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
{% if perms.circuits.add_virtualcircuit %}
<a href="{% url 'circuits:virtualcircuit_add' %}?type={{ object.pk }}" class="btn btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Virtual Circuit" %}
</a>
{% endif %}
{% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Virtual Circuit Type" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Color" %}</th>
<td>
{% if object.color %}
<span class="badge color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}