mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-29 11:56:25 -06:00
Merge branch 'feature' into 7699-cluster-location
This commit is contained in:
commit
45f29de89f
@ -21,13 +21,9 @@ Designates the termination as forming either the A or Z end of the circuit.
|
||||
|
||||
If selected, the circuit termination will be considered "connected" even if no cable has been connected to it in NetBox.
|
||||
|
||||
### Site
|
||||
### Termination
|
||||
|
||||
The [site](../dcim/site.md) with which this circuit termination is associated. Once created, a cable can be connected between the circuit termination and a device interface (or similar component).
|
||||
|
||||
### Provider Network
|
||||
|
||||
Circuits which do not connect to a site modeled by NetBox can instead be terminated to a [provider network](./providernetwork.md) representing an unknown network operated by a [provider](./provider.md).
|
||||
The [region](../dcim/region.md), [site group](../dcim/sitegroup.md), [site](../dcim/site.md), [location](../dcim/location.md) or [provider network](./providernetwork.md) with which this circuit termination is associated. Once created, a cable can be connected between the circuit termination and a device interface (or similar component).
|
||||
|
||||
### Port Speed
|
||||
|
||||
|
@ -142,3 +142,7 @@ The configured channel width of a wireless interface, in MHz. This is typically
|
||||
### Wireless LANs
|
||||
|
||||
The [wireless LANs](../wireless/wirelesslan.md) for which this interface carries traffic. (Valid for wireless interfaces only.)
|
||||
|
||||
### VLAN Translation Policy
|
||||
|
||||
The [VLAN translation policy](../ipam/vlantranslationpolicy.md) that applies to this interface (optional).
|
||||
|
26
docs/models/ipam/vlantranslationpolicy.md
Normal file
26
docs/models/ipam/vlantranslationpolicy.md
Normal file
@ -0,0 +1,26 @@
|
||||
# VLAN Translation Policies
|
||||
|
||||
VLAN translation is a feature that consists of VLAN translation policies and [VLAN translation rules](./vlantranslationrule.md). Many rules can belong to a policy, and each rule defines a mapping of a local to remote VLAN ID (VID). A policy can then be assigned to an [Interface](../dcim/interface.md) or [VMInterface](../virtualization/vminterface.md), and all VLAN translation rules associated with that policy will be visible in the interface details.
|
||||
|
||||
There are uniqueness constraints on `(policy, local_vid)` and on `(policy, remote_vid)` in the `VLANTranslationRule` model. Thus, you cannot have multiple rules linked to the same policy that have the same local VID or the same remote VID. A set of policies and rules might look like this:
|
||||
|
||||
Policy 1:
|
||||
- Rule: 100 -> 200
|
||||
- Rule: 101 -> 201
|
||||
|
||||
Policy 2:
|
||||
- Rule: 100 -> 300
|
||||
- Rule: 101 -> 301
|
||||
|
||||
However this is not allowed:
|
||||
|
||||
Policy 3:
|
||||
- Rule: 100 -> 200
|
||||
- Rule: 100 -> 300
|
||||
|
||||
|
||||
## Fields
|
||||
|
||||
### Name
|
||||
|
||||
A unique human-friendly name.
|
19
docs/models/ipam/vlantranslationrule.md
Normal file
19
docs/models/ipam/vlantranslationrule.md
Normal file
@ -0,0 +1,19 @@
|
||||
# VLAN Translation Rules
|
||||
|
||||
A VLAN translation rule represents a one-to-one mapping of a local VLAN ID (VID) to a remote VID. Many rules can belong to a single policy.
|
||||
|
||||
See [VLAN translation policies](./vlantranslationpolicy.md) for an overview of the VLAN Translation feature.
|
||||
|
||||
## Fields
|
||||
|
||||
### Policy
|
||||
|
||||
The [VLAN Translation Policy](./vlantranslationpolicy.md) to which this rule belongs.
|
||||
|
||||
### Local VID
|
||||
|
||||
VLAN ID (1-4094) in the local network which is to be translated to a remote VID.
|
||||
|
||||
### Remote VID
|
||||
|
||||
VLAN ID (1-4094) in the remote network to which the local VID will be translated.
|
@ -56,3 +56,7 @@ The tagged VLANs which are configured to be carried by this interface. Valid onl
|
||||
### VRF
|
||||
|
||||
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
|
||||
|
||||
### VLAN Translation Policy
|
||||
|
||||
The [VLAN translation policy](../ipam/vlantranslationpolicy.md) that applies to this interface (optional).
|
||||
|
@ -1,11 +1,16 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices
|
||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType
|
||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||
from dcim.api.serializers_.sites import SiteSerializer
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
||||
|
||||
@ -33,16 +38,33 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
|
||||
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
site = SiteSerializer(nested=True, allow_null=True)
|
||||
termination_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(
|
||||
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
),
|
||||
allow_null=True,
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
termination = serializers.SerializerMethodField(read_only=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'id', 'url', 'display_url', 'display', 'termination_type', 'termination_id', 'termination', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'description',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_termination(self, obj):
|
||||
if obj.termination_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.termination)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.termination, nested=True, context=context).data
|
||||
|
||||
|
||||
class CircuitGroupSerializer(NetBoxModelSerializer):
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
@ -95,18 +117,35 @@ class CircuitSerializer(NetBoxModelSerializer):
|
||||
|
||||
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
site = SiteSerializer(nested=True, required=False, allow_null=True)
|
||||
termination_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(
|
||||
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
),
|
||||
allow_null=True,
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
termination = serializers.SerializerMethodField(read_only=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed',
|
||||
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'termination_type', 'termination_id', 'termination', 'provider_network', 'port_speed',
|
||||
'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end',
|
||||
'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_termination(self, obj):
|
||||
if obj.termination_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.termination)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.termination, nested=True, context=context).data
|
||||
|
||||
|
||||
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
|
4
netbox/circuits/constants.py
Normal file
4
netbox/circuits/constants.py
Normal file
@ -0,0 +1,4 @@
|
||||
# models values for ContentTypes which may be CircuitTermination termination types
|
||||
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
|
||||
'region', 'sitegroup', 'site', 'location', 'providernetwork',
|
||||
)
|
@ -3,11 +3,11 @@ from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.filtersets import CabledObjectFilterSet
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@ -26,37 +26,37 @@ __all__ = (
|
||||
class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
field_name='circuits__terminations___region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
field_name='circuits__terminations___region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
field_name='circuits__terminations___site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
field_name='circuits__terminations___site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site',
|
||||
field_name='circuits__terminations___site',
|
||||
queryset=Site.objects.all(),
|
||||
label=_('Site'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site__slug',
|
||||
field_name='circuits__terminations___site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
@ -173,7 +173,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__provider_network',
|
||||
field_name='terminations___provider_network',
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
@ -193,37 +193,37 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
field_name='terminations___region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
field_name='terminations___region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
field_name='terminations___site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
field_name='terminations___site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site',
|
||||
field_name='terminations___site',
|
||||
queryset=Site.objects.all(),
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site__slug',
|
||||
field_name='terminations___site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
@ -263,18 +263,60 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
queryset=Circuit.objects.all(),
|
||||
label=_('Circuit'),
|
||||
)
|
||||
termination_type = ContentTypeFilter()
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='_site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='_site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
field_name='_site',
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site__slug',
|
||||
field_name='_site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='_location',
|
||||
lookup_expr='in',
|
||||
label=_('Location (ID)'),
|
||||
)
|
||||
location = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='_location',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Location (slug)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
field_name='_provider_network',
|
||||
label=_('ProviderNetwork (ID)'),
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -292,7 +334,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = (
|
||||
'id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected',
|
||||
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected',
|
||||
'pp_info', 'cable_end',
|
||||
)
|
||||
|
||||
|
@ -1,17 +1,23 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices
|
||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, NumberWithOptions
|
||||
from utilities.forms import add_blank_choice, get_field_value
|
||||
from utilities.forms.fields import (
|
||||
ColorField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
@ -197,15 +203,18 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
termination_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
|
||||
required=False,
|
||||
label=_('Termination type')
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider Network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False
|
||||
termination = DynamicModelChoiceField(
|
||||
label=_('Termination'),
|
||||
queryset=Site.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
port_speed = forms.IntegerField(
|
||||
required=False,
|
||||
@ -225,15 +234,26 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'description',
|
||||
TabbedGroups(
|
||||
FieldSet('site', name=_('Site')),
|
||||
FieldSet('provider_network', name=_('Provider Network')),
|
||||
),
|
||||
'termination_type', 'termination',
|
||||
'mark_connected', name=_('Circuit Termination')
|
||||
),
|
||||
FieldSet('port_speed', 'upstream_speed', name=_('Termination Details')),
|
||||
)
|
||||
nullable_fields = ('description')
|
||||
nullable_fields = ('description', 'termination')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if termination_type_id := get_field_value(self, 'termination_type'):
|
||||
try:
|
||||
termination_type = ContentType.objects.get(pk=termination_type_id)
|
||||
model = termination_type.model_class()
|
||||
self.fields['termination'].queryset = model.objects.all()
|
||||
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['termination'].disabled = False
|
||||
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
@ -1,13 +1,14 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.constants import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
|
||||
|
||||
__all__ = (
|
||||
'CircuitImportForm',
|
||||
@ -127,17 +128,10 @@ class BaseCircuitTerminationImportForm(forms.ModelForm):
|
||||
label=_('Termination'),
|
||||
choices=CircuitTerminationSideChoices,
|
||||
)
|
||||
site = CSVModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
provider_network = CSVModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
termination_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
required=False,
|
||||
label=_('Termination type (app & model)')
|
||||
)
|
||||
|
||||
|
||||
@ -145,9 +139,12 @@ class CircuitTerminationImportRelatedForm(BaseCircuitTerminationImportForm):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description'
|
||||
]
|
||||
labels = {
|
||||
'termination_id': _('Termination ID'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTerminationImportForm):
|
||||
@ -155,9 +152,12 @@ class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTermination
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description', 'tags'
|
||||
]
|
||||
labels = {
|
||||
'termination_id': _('Termination ID'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupImportForm(NetBoxModelImportForm):
|
||||
|
@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
@ -207,18 +207,29 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('circuit_id', 'term_side', name=_('Circuit')),
|
||||
FieldSet('provider_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('provider_id', name=_('Provider')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Termination')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
label=_('Location')
|
||||
)
|
||||
circuit_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False,
|
||||
|
@ -1,14 +1,19 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
|
||||
from circuits.constants import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
from utilities.forms.rendering import FieldSet, InlineFields
|
||||
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
|
||||
__all__ = (
|
||||
'CircuitForm',
|
||||
@ -144,26 +149,24 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
queryset=Circuit.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
termination_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
widget=HTMXSelect(),
|
||||
required=False,
|
||||
selector=True
|
||||
label=_('Termination type')
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
termination = DynamicModelChoiceField(
|
||||
label=_('Termination'),
|
||||
queryset=Site.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'circuit', 'term_side', 'description', 'tags',
|
||||
TabbedGroups(
|
||||
FieldSet('site', name=_('Site')),
|
||||
FieldSet('provider_network', name=_('Provider Network')),
|
||||
),
|
||||
'termination_type', 'termination',
|
||||
'mark_connected', name=_('Circuit Termination')
|
||||
),
|
||||
FieldSet('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', name=_('Termination Details')),
|
||||
@ -172,7 +175,7 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed', 'upstream_speed',
|
||||
'circuit', 'term_side', 'termination_type', 'mark_connected', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
@ -184,6 +187,36 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
if instance is not None and instance.termination:
|
||||
initial['termination'] = instance.termination
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if termination_type_id := get_field_value(self, 'termination_type'):
|
||||
try:
|
||||
termination_type = ContentType.objects.get(pk=termination_type_id)
|
||||
model = termination_type.model_class()
|
||||
self.fields['termination'].queryset = model.objects.all()
|
||||
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['termination'].disabled = False
|
||||
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
if self.instance and termination_type_id != self.instance.termination_type_id:
|
||||
self.initial['termination'] = None
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Assign the selected termination (if any)
|
||||
self.instance.termination = self.cleaned_data.get('termination')
|
||||
|
||||
|
||||
class CircuitGroupForm(TenancyForm, NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Annotated, List
|
||||
from typing import Annotated, List, Union
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
@ -59,13 +59,21 @@ class ProviderNetworkType(NetBoxObjectType):
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitTermination,
|
||||
fields='__all__',
|
||||
exclude=('termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'),
|
||||
filters=CircuitTerminationFilter
|
||||
)
|
||||
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
provider_network: Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')] | None
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
@strawberry_django.field
|
||||
def termination(self) -> Annotated[Union[
|
||||
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
|
||||
], strawberry.union("CircuitTerminationTerminationType")] | None:
|
||||
return self.termination
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
@ -0,0 +1,56 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def copy_site_assignments(apps, schema_editor):
|
||||
"""
|
||||
Copy site ForeignKey values to the Termination GFK.
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
Site = apps.get_model('dcim', 'Site')
|
||||
|
||||
CircuitTermination.objects.filter(site__isnull=False).update(
|
||||
termination_type=ContentType.objects.get_for_model(Site),
|
||||
termination_id=models.F('site_id')
|
||||
)
|
||||
|
||||
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
|
||||
CircuitTermination.objects.filter(provider_network__isnull=False).update(
|
||||
termination_type=ContentType.objects.get_for_model(ProviderNetwork),
|
||||
termination_id=models.F('provider_network_id')
|
||||
)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0046_charfield_null_choices'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0193_poweroutlet_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='termination_id',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='termination_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
limit_choices_to=models.Q(('model__in', ('region', 'sitegroup', 'site', 'location', 'providernetwork'))),
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
),
|
||||
),
|
||||
|
||||
# Copy over existing site assignments
|
||||
migrations.RunPython(
|
||||
code=copy_site_assignments,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
@ -0,0 +1,90 @@
|
||||
# Generated by Django 5.0.9 on 2024-10-21 17:34
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def populate_denormalized_fields(apps, schema_editor):
|
||||
"""
|
||||
Copy site ForeignKey values to the Termination GFK.
|
||||
"""
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
|
||||
terminations = CircuitTermination.objects.filter(site__isnull=False).prefetch_related('site')
|
||||
for termination in terminations:
|
||||
termination._region_id = termination.site.region_id
|
||||
termination._site_group_id = termination.site.group_id
|
||||
termination._site_id = termination.site_id
|
||||
# Note: Location cannot be set prior to migration
|
||||
|
||||
CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0047_circuittermination__termination'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_location',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.location',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_region',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.region',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_site',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.site',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_site_group',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.sitegroup',
|
||||
),
|
||||
),
|
||||
|
||||
# Populate denormalized FK values
|
||||
migrations.RunPython(
|
||||
code=populate_denormalized_fields,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
|
||||
# Delete the site ForeignKey
|
||||
migrations.RemoveField(
|
||||
model_name='circuittermination',
|
||||
name='site',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='circuittermination',
|
||||
old_name='provider_network',
|
||||
new_name='_provider_network',
|
||||
),
|
||||
]
|
@ -1,9 +1,13 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.constants import *
|
||||
from dcim.models import CabledObjectModel
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.mixins import DistanceMixin
|
||||
@ -230,22 +234,24 @@ class CircuitTermination(
|
||||
term_side = models.CharField(
|
||||
max_length=1,
|
||||
choices=CircuitTerminationSideChoices,
|
||||
verbose_name=_('termination')
|
||||
verbose_name=_('termination side')
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
termination_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
limit_choices_to=Q(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
provider_network = models.ForeignKey(
|
||||
to='circuits.ProviderNetwork',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
termination_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
termination = GenericForeignKey(
|
||||
ct_field='termination_type',
|
||||
fk_field='termination_id'
|
||||
)
|
||||
port_speed = models.PositiveIntegerField(
|
||||
verbose_name=_('port speed (Kbps)'),
|
||||
blank=True,
|
||||
@ -276,6 +282,43 @@ class CircuitTermination(
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Cached associations to enable efficient filtering
|
||||
_provider_network = models.ForeignKey(
|
||||
to='circuits.ProviderNetwork',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_location = models.ForeignKey(
|
||||
to='dcim.Location',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_region = models.ForeignKey(
|
||||
to='dcim.Region',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_site_group = models.ForeignKey(
|
||||
to='dcim.SiteGroup',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['circuit', 'term_side']
|
||||
constraints = (
|
||||
@ -297,10 +340,35 @@ class CircuitTermination(
|
||||
super().clean()
|
||||
|
||||
# Must define either site *or* provider network
|
||||
if self.site is None and self.provider_network is None:
|
||||
raise ValidationError(_("A circuit termination must attach to either a site or a provider network."))
|
||||
if self.site and self.provider_network:
|
||||
raise ValidationError(_("A circuit termination cannot attach to both a site and a provider network."))
|
||||
if self.termination is None:
|
||||
raise ValidationError(_("A circuit termination must attach to termination."))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Cache objects associated with the terminating object (for filtering)
|
||||
self.cache_related_objects()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def cache_related_objects(self):
|
||||
self._provider_network = self._region = self._site_group = self._site = self._location = None
|
||||
if self.termination_type:
|
||||
termination_type = self.termination_type.model_class()
|
||||
if termination_type == apps.get_model('dcim', 'region'):
|
||||
self._region = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'sitegroup'):
|
||||
self._site_group = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'site'):
|
||||
self._region = self.termination.region
|
||||
self._site_group = self.termination.group
|
||||
self._site = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'location'):
|
||||
self._region = self.termination.site.region
|
||||
self._site_group = self.termination.site.group
|
||||
self._site = self.termination.site
|
||||
self._location = self.termination
|
||||
elif termination_type == apps.get_model('circuits', 'providernetwork'):
|
||||
self._provider_network = self.termination
|
||||
cache_related_objects.alters_data = True
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
@ -314,7 +382,7 @@ class CircuitTermination(
|
||||
def get_peer_termination(self):
|
||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||
try:
|
||||
return CircuitTermination.objects.prefetch_related('site').get(
|
||||
return CircuitTermination.objects.prefetch_related('termination').get(
|
||||
circuit=self.circuit,
|
||||
term_side=peer_side
|
||||
)
|
||||
|
@ -18,10 +18,8 @@ __all__ = (
|
||||
|
||||
|
||||
CIRCUITTERMINATION_LINK = """
|
||||
{% if value.site %}
|
||||
<a href="{{ value.site.get_absolute_url }}">{{ value.site }}</a>
|
||||
{% elif value.provider_network %}
|
||||
<a href="{{ value.provider_network.get_absolute_url }}">{{ value.provider_network }}</a>
|
||||
{% if value.termination %}
|
||||
<a href="{{ value.termination.get_absolute_url }}">{{ value.termination }}</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -63,12 +61,12 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_a = tables.TemplateColumn(
|
||||
termination_a = columns.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
orderable=False,
|
||||
verbose_name=_('Side A')
|
||||
)
|
||||
termination_z = tables.TemplateColumn(
|
||||
termination_z = columns.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
orderable=False,
|
||||
verbose_name=_('Side Z')
|
||||
@ -110,22 +108,54 @@ class CircuitTerminationTable(NetBoxTable):
|
||||
linkify=True,
|
||||
accessor='circuit.provider'
|
||||
)
|
||||
term_side = tables.Column(
|
||||
verbose_name=_('Side')
|
||||
)
|
||||
termination_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Termination Type'),
|
||||
)
|
||||
termination = tables.Column(
|
||||
verbose_name=_('Termination Point'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
# Termination types
|
||||
site = tables.Column(
|
||||
verbose_name=_('Site'),
|
||||
linkify=True
|
||||
linkify=True,
|
||||
accessor='_site'
|
||||
)
|
||||
site_group = tables.Column(
|
||||
verbose_name=_('Site Group'),
|
||||
linkify=True,
|
||||
accessor='_sitegroup'
|
||||
)
|
||||
region = tables.Column(
|
||||
verbose_name=_('Region'),
|
||||
linkify=True,
|
||||
accessor='_region'
|
||||
)
|
||||
location = tables.Column(
|
||||
verbose_name=_('Location'),
|
||||
linkify=True,
|
||||
accessor='_location'
|
||||
)
|
||||
provider_network = tables.Column(
|
||||
verbose_name=_('Provider Network'),
|
||||
linkify=True
|
||||
linkify=True,
|
||||
accessor='_provider_network'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitTermination
|
||||
fields = (
|
||||
'pk', 'id', 'circuit', 'provider', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'created', 'last_updated', 'actions',
|
||||
'pk', 'id', 'circuit', 'provider', 'term_side', 'termination_type', 'termination', 'site_group', 'region',
|
||||
'site', 'location', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
'description', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'circuit', 'provider', 'term_side', 'termination_type', 'termination', 'description',
|
||||
)
|
||||
default_columns = ('pk', 'id', 'circuit', 'provider', 'term_side', 'description')
|
||||
|
||||
|
||||
class CircuitGroupTable(NetBoxTable):
|
||||
|
@ -181,10 +181,10 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, provider_network=provider_networks[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, provider_network=provider_networks[1]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, termination=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, termination=provider_networks[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, termination=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, termination=provider_networks[1]),
|
||||
)
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
@ -192,13 +192,15 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_A,
|
||||
'site': sites[0].pk,
|
||||
'termination_type': 'dcim.site',
|
||||
'termination_id': sites[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_Z,
|
||||
'provider_network': provider_networks[0].pk,
|
||||
'termination_type': 'circuits.providernetwork',
|
||||
'termination_id': provider_networks[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
]
|
||||
|
@ -70,10 +70,12 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
CircuitTermination.objects.bulk_create((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
|
||||
))
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[0], term_side='A'),
|
||||
)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
@ -233,14 +235,15 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = ((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[2], termination=sites[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], termination=provider_networks[2], term_side='A'),
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
@ -384,18 +387,19 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = ((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC', description='foobar1'),
|
||||
CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF', description='foobar2'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
|
||||
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[6], provider_network=provider_networks[0], term_side='A', mark_connected=True),
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC', description='foobar1'),
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF', description='foobar2'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
|
||||
CircuitTermination(circuit=circuits[2], termination=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
|
||||
CircuitTermination(circuit=circuits[2], termination=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
|
||||
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], termination=provider_networks[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[6], termination=provider_networks[0], term_side='A', mark_connected=True),
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
Cable(a_terminations=[circuit_terminations[0]], b_terminations=[circuit_terminations[1]]).save()
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import datetime
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
@ -190,27 +191,31 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||
def test_bulk_import_objects_with_terminations(self):
|
||||
json_data = """
|
||||
site = Site.objects.first()
|
||||
json_data = f"""
|
||||
[
|
||||
{
|
||||
{{
|
||||
"cid": "Circuit 7",
|
||||
"provider": "Provider 1",
|
||||
"type": "Circuit Type 1",
|
||||
"status": "active",
|
||||
"description": "Testing Import",
|
||||
"terminations": [
|
||||
{
|
||||
{{
|
||||
"term_side": "A",
|
||||
"site": "Site 1"
|
||||
},
|
||||
{
|
||||
"termination_type": "dcim.site",
|
||||
"termination_id": "{site.pk}"
|
||||
}},
|
||||
{{
|
||||
"term_side": "Z",
|
||||
"site": "Site 1"
|
||||
}
|
||||
"termination_type": "dcim.site",
|
||||
"termination_id": "{site.pk}"
|
||||
}}
|
||||
]
|
||||
}
|
||||
}}
|
||||
]
|
||||
"""
|
||||
|
||||
initial_count = self._get_queryset().count()
|
||||
data = {
|
||||
'data': json_data,
|
||||
@ -336,7 +341,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
class TestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = CircuitTermination
|
||||
|
||||
@classmethod
|
||||
@ -359,24 +364,27 @@ class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], term_side='A', site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side='Z', site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='A', site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='Z', site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[0], term_side='A', termination=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side='Z', termination=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='A', termination=sites[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='Z', termination=sites[1]),
|
||||
)
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
cls.form_data = {
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': 'A',
|
||||
'site': sites[2].pk,
|
||||
'termination_type': ContentType.objects.get_for_model(Site).pk,
|
||||
'termination': sites[2].pk,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
site = sites[0].pk
|
||||
cls.csv_data = (
|
||||
"circuit,term_side,site,description",
|
||||
"Circuit 3,A,Site 1,Foo",
|
||||
"Circuit 3,Z,Site 1,Bar",
|
||||
"circuit,term_side,termination_type,termination_id,description",
|
||||
f"Circuit 3,A,dcim.site,{site},Foo",
|
||||
f"Circuit 3,Z,dcim.site,{site},Bar",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
|
@ -158,7 +158,7 @@ class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
instance,
|
||||
extra=(
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___provider_network=instance),
|
||||
'provider_network_id',
|
||||
),
|
||||
),
|
||||
@ -257,8 +257,7 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class CircuitListView(generic.ObjectListView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'tenant__group', 'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
'tenant__group', 'termination_a__termination', 'termination_z__termination',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
filterset_form = forms.CircuitFilterForm
|
||||
@ -298,8 +297,7 @@ class CircuitBulkImportView(generic.BulkImportView):
|
||||
|
||||
class CircuitBulkEditView(generic.BulkEditView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
'tenant__group', 'termination_a__termination', 'termination_z__termination',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
@ -308,8 +306,7 @@ class CircuitBulkEditView(generic.BulkEditView):
|
||||
|
||||
class CircuitBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
'tenant__group', 'termination_a__termination', 'termination_z__termination',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
|
@ -8,7 +8,7 @@ from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
|
||||
RearPort, VirtualDeviceContext,
|
||||
)
|
||||
from ipam.api.serializers_.vlans import VLANSerializer
|
||||
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
|
||||
from ipam.api.serializers_.vrfs import VRFSerializer
|
||||
from ipam.models import VLAN
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
@ -196,6 +196,7 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
vlan_translation_policy = VLANTranslationPolicySerializer(nested=True, required=False, allow_null=True)
|
||||
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
||||
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
|
||||
wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True)
|
||||
@ -225,7 +226,7 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link',
|
||||
'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
|
||||
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', 'vlan_translation_policy'
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||
|
||||
|
@ -8,7 +8,7 @@ from circuits.models import CircuitTermination
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.filtersets import PrimaryIPFilterSet
|
||||
from ipam.models import ASN, IPAddress, VRF
|
||||
from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.filtersets import (
|
||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
|
||||
@ -1630,6 +1630,17 @@ class CommonInterfaceFilterSet(django_filters.FilterSet):
|
||||
to_field_name='identifier',
|
||||
label=_('L2VPN'),
|
||||
)
|
||||
vlan_translation_policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vlan_translation_policy',
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
label=_('VLAN Translation Policy (ID)'),
|
||||
)
|
||||
vlan_translation_policy = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vlan_translation_policy__name',
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
to_field_name='name',
|
||||
label=_('VLAN Translation Policy'),
|
||||
)
|
||||
|
||||
def filter_vlan_id(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
|
@ -7,7 +7,7 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
|
||||
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from users.models import User
|
||||
@ -1382,6 +1382,11 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
required=False,
|
||||
label=_('WWN')
|
||||
)
|
||||
vlan_translation_policy = DynamicModelChoiceField(
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
required=False,
|
||||
label=_('VLAN Translation Policy')
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
@ -1391,7 +1396,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
|
||||
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
|
||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||
FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')),
|
||||
FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vlan_translation_policy', name=_('802.1Q Switching')),
|
||||
FieldSet(
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
|
||||
name=_('Wireless')
|
||||
@ -1404,7 +1409,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
|
||||
'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
|
||||
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
|
||||
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', 'vlan_translation_policy',
|
||||
]
|
||||
widgets = {
|
||||
'speed': NumberWithOptions(
|
||||
|
@ -385,6 +385,7 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
|
||||
wireless_link: Annotated["WirelessLinkType", strawberry.lazy('wireless.graphql.types')] | None
|
||||
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
|
||||
vdcs: List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]
|
||||
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
||||
@ -465,6 +466,10 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
|
||||
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
||||
return self._clusters.all()
|
||||
|
||||
@strawberry_django.field
|
||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||
return self.circuit_terminations.all()
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Manufacturer,
|
||||
@ -712,6 +717,10 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
||||
return self._clusters.all()
|
||||
|
||||
@strawberry_django.field
|
||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||
return self.circuit_terminations.all()
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Site,
|
||||
@ -734,12 +743,17 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
|
||||
locations: List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]
|
||||
asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
|
||||
circuit_terminations: List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
|
||||
clusters: List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]
|
||||
vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
@strawberry_django.field
|
||||
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
||||
return self._clusters.all()
|
||||
|
||||
@strawberry_django.field
|
||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||
return self.circuit_terminations.all()
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.SiteGroup,
|
||||
@ -760,6 +774,10 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
||||
return self._clusters.all()
|
||||
|
||||
@strawberry_django.field
|
||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||
return self.circuit_terminations.all()
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualChassis,
|
||||
|
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.0.9 on 2024-10-11 19:45
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0194_charfield_null_choices'),
|
||||
('ipam', '0074_vlantranslationpolicy_vlantranslationrule'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='vlan_translation_policy',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.vlantranslationpolicy'),
|
||||
),
|
||||
]
|
@ -344,7 +344,7 @@ class CableTermination(ChangeLoggedModel):
|
||||
)
|
||||
|
||||
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
|
||||
if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
|
||||
if self.termination_type.model == 'circuittermination' and self.termination._provider_network is not None:
|
||||
raise ValidationError(_("Circuit terminations attached to a provider network may not be cabled."))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@ -690,19 +690,19 @@ class CablePath(models.Model):
|
||||
).first()
|
||||
if circuit_termination is None:
|
||||
break
|
||||
elif circuit_termination.provider_network:
|
||||
elif circuit_termination._provider_network:
|
||||
# Circuit terminates to a ProviderNetwork
|
||||
path.extend([
|
||||
[object_to_path_node(circuit_termination)],
|
||||
[object_to_path_node(circuit_termination.provider_network)],
|
||||
[object_to_path_node(circuit_termination._provider_network)],
|
||||
])
|
||||
is_complete = True
|
||||
break
|
||||
elif circuit_termination.site and not circuit_termination.cable:
|
||||
# Circuit terminates to a Site
|
||||
elif circuit_termination.termination and not circuit_termination.cable:
|
||||
# Circuit terminates to a Region/Site/etc.
|
||||
path.extend([
|
||||
[object_to_path_node(circuit_termination)],
|
||||
[object_to_path_node(circuit_termination.site)],
|
||||
[object_to_path_node(circuit_termination.termination)],
|
||||
])
|
||||
break
|
||||
|
||||
|
@ -547,6 +547,13 @@ class BaseInterface(models.Model):
|
||||
blank=True,
|
||||
verbose_name=_('bridge interface')
|
||||
)
|
||||
vlan_translation_policy = models.ForeignKey(
|
||||
to='ipam.VLANTranslationPolicy',
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_('VLAN Translation Policy'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
@ -1167,7 +1167,7 @@ class CablePathTestCase(TestCase):
|
||||
[IF1] --C1-- [CT1]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
@ -1198,7 +1198,7 @@ class CablePathTestCase(TestCase):
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
@ -1214,7 +1214,7 @@ class CablePathTestCase(TestCase):
|
||||
)
|
||||
|
||||
# Create CT2
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
|
||||
|
||||
# Check for partial path to site
|
||||
self.assertPathExists(
|
||||
@ -1266,7 +1266,7 @@ class CablePathTestCase(TestCase):
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
|
||||
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
@ -1282,7 +1282,7 @@ class CablePathTestCase(TestCase):
|
||||
)
|
||||
|
||||
# Create CT2
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
|
||||
|
||||
# Check for partial path to site
|
||||
self.assertPathExists(
|
||||
@ -1335,8 +1335,8 @@ class CablePathTestCase(TestCase):
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
site2 = Site.objects.create(name='Site 2', slug='site-2')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=site2, term_side='Z')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=site2, term_side='Z')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
@ -1365,8 +1365,8 @@ class CablePathTestCase(TestCase):
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
providernetwork = ProviderNetwork.objects.create(name='Provider Network 1', provider=self.circuit.provider)
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, provider_network=providernetwork, term_side='Z')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=providernetwork, term_side='Z')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
@ -1413,8 +1413,8 @@ class CablePathTestCase(TestCase):
|
||||
frontport2_2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
|
||||
)
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
@ -1499,10 +1499,10 @@ class CablePathTestCase(TestCase):
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
circuit2 = Circuit.objects.create(provider=self.circuit.provider, type=self.circuit.type, cid='Circuit 2')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||
circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, site=self.site, term_side='A')
|
||||
circuittermination4 = CircuitTermination.objects.create(circuit=circuit2, site=self.site, term_side='Z')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
|
||||
circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, termination=self.site, term_side='A')
|
||||
circuittermination4 = CircuitTermination.objects.create(circuit=circuit2, termination=self.site, term_side='Z')
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
|
@ -4,7 +4,7 @@ from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
from dcim.choices import *
|
||||
from dcim.filtersets import *
|
||||
from dcim.models import *
|
||||
from ipam.models import ASN, IPAddress, RIR, VRF
|
||||
from ipam.models import ASN, IPAddress, RIR, VLANTranslationPolicy, VRF
|
||||
from netbox.choices import ColorChoices, WeightUnitChoices
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.models import User
|
||||
@ -3669,6 +3669,13 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
)
|
||||
VirtualDeviceContext.objects.bulk_create(vdcs)
|
||||
|
||||
vlan_translation_policies = (
|
||||
VLANTranslationPolicy(name='Policy 1'),
|
||||
VLANTranslationPolicy(name='Policy 2'),
|
||||
VLANTranslationPolicy(name='Policy 3'),
|
||||
)
|
||||
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||
|
||||
interfaces = (
|
||||
Interface(
|
||||
device=devices[0],
|
||||
@ -3686,7 +3693,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
speed=1000000,
|
||||
duplex='half',
|
||||
poe_mode=InterfacePoEModeChoices.MODE_PSE,
|
||||
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
|
||||
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF,
|
||||
vlan_translation_policy=vlan_translation_policies[0],
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
@ -3711,7 +3719,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
speed=1000000,
|
||||
duplex='full',
|
||||
poe_mode=InterfacePoEModeChoices.MODE_PD,
|
||||
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
|
||||
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF,
|
||||
vlan_translation_policy=vlan_translation_policies[0],
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
@ -3729,7 +3738,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
speed=100000,
|
||||
duplex='half',
|
||||
poe_mode=InterfacePoEModeChoices.MODE_PSE,
|
||||
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
|
||||
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT,
|
||||
vlan_translation_policy=vlan_translation_policies[1],
|
||||
),
|
||||
Interface(
|
||||
device=devices[4],
|
||||
@ -3742,7 +3752,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
speed=100000,
|
||||
duplex='full',
|
||||
poe_mode=InterfacePoEModeChoices.MODE_PD,
|
||||
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
|
||||
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT,
|
||||
vlan_translation_policy=vlan_translation_policies[1],
|
||||
),
|
||||
Interface(
|
||||
device=devices[4],
|
||||
@ -4016,6 +4027,13 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
params = {'vdc_identifier': vdc.values_list('identifier', flat=True)}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_vlan_translation_policy(self):
|
||||
vlan_translation_policies = VLANTranslationPolicy.objects.all()[:2]
|
||||
params = {'vlan_translation_policy_id': [vlan_translation_policies[0].pk, vlan_translation_policies[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'vlan_translation_policy': [vlan_translation_policies[0].name, vlan_translation_policies[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = FrontPort.objects.all()
|
||||
@ -5117,7 +5135,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
circuit = Circuit.objects.create(cid='Circuit 1', provider=provider, type=circuit_type)
|
||||
circuit_termination = CircuitTermination.objects.create(circuit=circuit, term_side='A', site=sites[0])
|
||||
circuit_termination = CircuitTermination.objects.create(circuit=circuit, term_side='A', termination=sites[0])
|
||||
|
||||
# Cables
|
||||
cables = (
|
||||
@ -5290,9 +5308,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
def test_site(self):
|
||||
site = Site.objects.all()[:2]
|
||||
params = {'site_id': [site[0].pk, site[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11)
|
||||
params = {'site': [site[0].slug, site[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11)
|
||||
|
||||
def test_tenant(self):
|
||||
tenant = Tenant.objects.all()[:2]
|
||||
|
@ -763,9 +763,9 @@ class CableTestCase(TestCase):
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
circuit1 = Circuit.objects.create(provider=provider, type=circuittype, cid='1')
|
||||
circuit2 = Circuit.objects.create(provider=provider, type=circuittype, cid='2')
|
||||
CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A')
|
||||
CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z')
|
||||
CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A')
|
||||
CircuitTermination.objects.create(circuit=circuit1, termination=site, term_side='A')
|
||||
CircuitTermination.objects.create(circuit=circuit1, termination=site, term_side='Z')
|
||||
CircuitTermination.objects.create(circuit=circuit2, termination=provider_network, term_side='A')
|
||||
|
||||
def test_cable_creation(self):
|
||||
"""
|
||||
|
@ -11,14 +11,14 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import View
|
||||
from jinja2.exceptions import TemplateError
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from extras.views import ObjectConfigContextView
|
||||
from ipam.models import ASN, IPAddress, VLANGroup
|
||||
from ipam.tables import InterfaceVLANTable
|
||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||
from netbox.views import generic
|
||||
from tenancy.views import ObjectContactsView
|
||||
@ -242,6 +242,10 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
extra=(
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___region=instance).distinct(),
|
||||
'region_id'
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
@ -324,6 +328,10 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
extra=(
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___site_group=instance).distinct(),
|
||||
'site_group_id'
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
@ -404,8 +412,10 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
scope_id=instance.pk
|
||||
), 'site'),
|
||||
(ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'),
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(),
|
||||
'site_id'),
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___site=instance).distinct(),
|
||||
'site_id'
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
@ -475,7 +485,17 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
def get_extra_context(self, request, instance):
|
||||
locations = instance.get_descendants(include_self=True)
|
||||
return {
|
||||
'related_models': self.get_related_models(request, locations, [CableTermination]),
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
locations,
|
||||
[CableTermination],
|
||||
(
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___location=instance).distinct(),
|
||||
'location_id'
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@ -2580,11 +2600,20 @@ class InterfaceView(generic.ObjectView):
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# Get VLAN translation rules
|
||||
vlan_translation_table = None
|
||||
if instance.vlan_translation_policy:
|
||||
vlan_translation_table = VLANTranslationRuleTable(
|
||||
data=instance.vlan_translation_policy.rules.all(),
|
||||
orderable=False
|
||||
)
|
||||
|
||||
return {
|
||||
'vdc_table': vdc_table,
|
||||
'bridge_interfaces_table': bridge_interfaces_tables,
|
||||
'child_interfaces_table': child_interfaces_tables,
|
||||
'vlan_table': vlan_table,
|
||||
'vlan_translation_table': vlan_translation_table,
|
||||
}
|
||||
|
||||
|
||||
|
@ -1172,6 +1172,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
'virtualmachine',
|
||||
'vlan',
|
||||
'vlangroup',
|
||||
'vlantranslationpolicy',
|
||||
'vlantranslationrule',
|
||||
'vminterface',
|
||||
'vrf',
|
||||
'webhook',
|
||||
|
@ -1180,7 +1180,8 @@ class ScriptView(BaseScriptView):
|
||||
data=form.cleaned_data,
|
||||
request=copy_safe_request(request),
|
||||
job_timeout=script.python_class.job_timeout,
|
||||
commit=form.cleaned_data.pop('_commit')
|
||||
commit=form.cleaned_data.pop('_commit'),
|
||||
name=script.name
|
||||
)
|
||||
|
||||
return redirect('extras:script_result', job_pk=job.pk)
|
||||
|
@ -5,7 +5,7 @@ from rest_framework import serializers
|
||||
from dcim.api.serializers_.sites import SiteSerializer
|
||||
from ipam.choices import *
|
||||
from ipam.constants import VLANGROUP_SCOPE_TYPES
|
||||
from ipam.models import VLAN, VLANGroup
|
||||
from ipam.models import VLAN, VLANGroup, VLANTranslationPolicy, VLANTranslationRule
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, IntegerRangeSerializer, RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
@ -18,6 +18,8 @@ __all__ = (
|
||||
'CreateAvailableVLANSerializer',
|
||||
'VLANGroupSerializer',
|
||||
'VLANSerializer',
|
||||
'VLANTranslationPolicySerializer',
|
||||
'VLANTranslationRuleSerializer',
|
||||
)
|
||||
|
||||
|
||||
@ -110,3 +112,19 @@ class CreateAvailableVLANSerializer(NetBoxModelSerializer):
|
||||
def validate(self, data):
|
||||
# Bypass model validation since we don't have a VID yet
|
||||
return data
|
||||
|
||||
|
||||
class VLANTranslationRuleSerializer(NetBoxModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = VLANTranslationRule
|
||||
fields = ['id', 'policy', 'local_vid', 'remote_vid']
|
||||
|
||||
|
||||
class VLANTranslationPolicySerializer(NetBoxModelSerializer):
|
||||
rules = VLANTranslationRuleSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VLANTranslationPolicy
|
||||
fields = ['id', 'url', 'name', 'description', 'display', 'rules']
|
||||
brief_fields = ('id', 'url', 'name', 'description', 'display')
|
||||
|
@ -21,6 +21,8 @@ router.register('fhrp-groups', views.FHRPGroupViewSet)
|
||||
router.register('fhrp-group-assignments', views.FHRPGroupAssignmentViewSet)
|
||||
router.register('vlan-groups', views.VLANGroupViewSet)
|
||||
router.register('vlans', views.VLANViewSet)
|
||||
router.register('vlan-translation-policies', views.VLANTranslationPolicyViewSet)
|
||||
router.register('vlan-translation-rules', views.VLANTranslationRuleViewSet)
|
||||
router.register('service-templates', views.ServiceTemplateViewSet)
|
||||
router.register('services', views.ServiceViewSet)
|
||||
|
||||
|
@ -143,6 +143,18 @@ class VLANViewSet(NetBoxModelViewSet):
|
||||
filterset_class = filtersets.VLANFilterSet
|
||||
|
||||
|
||||
class VLANTranslationPolicyViewSet(NetBoxModelViewSet):
|
||||
queryset = VLANTranslationPolicy.objects.all()
|
||||
serializer_class = serializers.VLANTranslationPolicySerializer
|
||||
filterset_class = filtersets.VLANTranslationPolicyFilterSet
|
||||
|
||||
|
||||
class VLANTranslationRuleViewSet(NetBoxModelViewSet):
|
||||
queryset = VLANTranslationRule.objects.all()
|
||||
serializer_class = serializers.VLANTranslationRuleSerializer
|
||||
filterset_class = filtersets.VLANTranslationRuleFilterSet
|
||||
|
||||
|
||||
class ServiceTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = ServiceTemplate.objects.all()
|
||||
serializer_class = serializers.ServiceTemplateSerializer
|
||||
|
@ -37,6 +37,8 @@ __all__ = (
|
||||
'ServiceTemplateFilterSet',
|
||||
'VLANFilterSet',
|
||||
'VLANGroupFilterSet',
|
||||
'VLANTranslationPolicyFilterSet',
|
||||
'VLANTranslationRuleFilterSet',
|
||||
'VRFFilterSet',
|
||||
)
|
||||
|
||||
@ -1104,6 +1106,53 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class VLANTranslationPolicyFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VLANTranslationPolicy
|
||||
fields = ('id', 'name', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class VLANTranslationRuleFilterSet(NetBoxModelFilterSet):
|
||||
policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
label=_('VLAN Translation Policy (ID)'),
|
||||
)
|
||||
policy = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='policy__name',
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
to_field_name='name',
|
||||
label=_('VLAN Translation Policy (name)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLANTranslationRule
|
||||
fields = ('id', 'policy_id', 'policy', 'local_vid', 'remote_vid', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(policy__name__icontains=value)
|
||||
)
|
||||
try:
|
||||
int_value = int(value.strip())
|
||||
qs_filter |= Q(local_vid=int_value)
|
||||
qs_filter |= Q(remote_vid=int_value)
|
||||
except ValueError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class ServiceTemplateFilterSet(NetBoxModelFilterSet):
|
||||
port = NumericArrayFilter(
|
||||
field_name='ports',
|
||||
|
@ -34,6 +34,8 @@ __all__ = (
|
||||
'ServiceTemplateBulkEditForm',
|
||||
'VLANBulkEditForm',
|
||||
'VLANGroupBulkEditForm',
|
||||
'VLANTranslationPolicyBulkEditForm',
|
||||
'VLANTranslationRuleBulkEditForm',
|
||||
'VRFBulkEditForm',
|
||||
)
|
||||
|
||||
@ -537,6 +539,36 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
|
||||
)
|
||||
|
||||
|
||||
class VLANTranslationPolicyBulkEditForm(NetBoxModelBulkEditForm):
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
model = VLANTranslationPolicy
|
||||
fieldsets = (
|
||||
FieldSet('description'),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
|
||||
|
||||
class VLANTranslationRuleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
policy = DynamicModelChoiceField(
|
||||
label=_('Policy'),
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
local_vid = forms.IntegerField(required=False)
|
||||
remote_vid = forms.IntegerField(required=False)
|
||||
|
||||
model = VLANTranslationRule
|
||||
fieldsets = (
|
||||
FieldSet('policy', 'local_vid', 'remote_vid'),
|
||||
)
|
||||
fields = ('policy', 'local_vid', 'remote_vid')
|
||||
|
||||
|
||||
class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
|
||||
protocol = forms.ChoiceField(
|
||||
label=_('Protocol'),
|
||||
|
@ -29,6 +29,8 @@ __all__ = (
|
||||
'ServiceTemplateImportForm',
|
||||
'VLANImportForm',
|
||||
'VLANGroupImportForm',
|
||||
'VLANTranslationPolicyImportForm',
|
||||
'VLANTranslationRuleImportForm',
|
||||
'VRFImportForm',
|
||||
)
|
||||
|
||||
@ -465,6 +467,20 @@ class VLANImportForm(NetBoxModelImportForm):
|
||||
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'comments', 'tags')
|
||||
|
||||
|
||||
class VLANTranslationPolicyImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = VLANTranslationPolicy
|
||||
fields = ('name', 'description', 'tags')
|
||||
|
||||
|
||||
class VLANTranslationRuleImportForm(NetBoxModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = VLANTranslationRule
|
||||
fields = ('policy', 'local_vid', 'remote_vid')
|
||||
|
||||
|
||||
class ServiceTemplateImportForm(NetBoxModelImportForm):
|
||||
protocol = CSVChoiceField(
|
||||
label=_('Protocol'),
|
||||
|
@ -28,6 +28,8 @@ __all__ = (
|
||||
'ServiceTemplateFilterForm',
|
||||
'VLANFilterForm',
|
||||
'VLANGroupFilterForm',
|
||||
'VLANTranslationPolicyFilterForm',
|
||||
'VLANTranslationRuleFilterForm',
|
||||
'VRFFilterForm',
|
||||
)
|
||||
|
||||
@ -461,6 +463,43 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VLANTranslationPolicyFilterForm(NetBoxModelFilterSetForm):
|
||||
model = VLANTranslationPolicy
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', name=_('Attributes')),
|
||||
)
|
||||
name = forms.CharField(
|
||||
required=False,
|
||||
label=_('Name')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VLANTranslationRuleFilterForm(NetBoxModelFilterSetForm):
|
||||
model = VLANTranslationRule
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('policy_id', 'local_vid', 'remote_vid', name=_('Attributes')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
policy_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
required=False,
|
||||
label=_('VLAN Translation Policy')
|
||||
)
|
||||
local_vid = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
label=_('Local VLAN ID')
|
||||
)
|
||||
remote_vid = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
label=_('Remote VLAN ID')
|
||||
)
|
||||
|
||||
|
||||
class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = VLAN
|
||||
fieldsets = (
|
||||
|
@ -41,6 +41,8 @@ __all__ = (
|
||||
'ServiceTemplateForm',
|
||||
'VLANForm',
|
||||
'VLANGroupForm',
|
||||
'VLANTranslationPolicyForm',
|
||||
'VLANTranslationRuleForm',
|
||||
'VRFForm',
|
||||
)
|
||||
|
||||
@ -691,6 +693,37 @@ class VLANForm(TenancyForm, NetBoxModelForm):
|
||||
]
|
||||
|
||||
|
||||
class VLANTranslationPolicyForm(NetBoxModelForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'description', 'tags', name=_('VLAN Translation Policy')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLANTranslationPolicy
|
||||
fields = [
|
||||
'name', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class VLANTranslationRuleForm(NetBoxModelForm):
|
||||
policy = DynamicModelChoiceField(
|
||||
label=_('Policy'),
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('policy', 'local_vid', 'remote_vid', 'description', 'tags', name=_('VLAN Translation Rule')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLANTranslationRule
|
||||
fields = [
|
||||
'policy', 'local_vid', 'remote_vid', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class ServiceTemplateForm(NetBoxModelForm):
|
||||
ports = NumericArrayField(
|
||||
label=_('Ports'),
|
||||
|
@ -19,6 +19,8 @@ __all__ = (
|
||||
'ServiceTemplateFilter',
|
||||
'VLANFilter',
|
||||
'VLANGroupFilter',
|
||||
'VLANTranslationPolicyFilter',
|
||||
'VLANTranslationRuleFilter',
|
||||
'VRFFilter',
|
||||
)
|
||||
|
||||
@ -113,6 +115,18 @@ class VLANGroupFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLANTranslationPolicy, lookups=True)
|
||||
@autotype_decorator(filtersets.VLANTranslationPolicyFilterSet)
|
||||
class VLANTranslationPolicyFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLANTranslationRule, lookups=True)
|
||||
@autotype_decorator(filtersets.VLANTranslationRuleFilterSet)
|
||||
class VLANTranslationRuleFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VRF, lookups=True)
|
||||
@autotype_decorator(filtersets.VRFFilterSet)
|
||||
class VRFFilter(BaseFilterMixin):
|
||||
|
@ -53,5 +53,11 @@ class IPAMQuery:
|
||||
vlan_group: VLANGroupType = strawberry_django.field()
|
||||
vlan_group_list: List[VLANGroupType] = strawberry_django.field()
|
||||
|
||||
vlan_translation_policy: VLANTranslationPolicyType = strawberry_django.field()
|
||||
vlan_translation_policy_list: List[VLANTranslationPolicyType] = strawberry_django.field()
|
||||
|
||||
vlan_translation_rule: VLANTranslationRuleType = strawberry_django.field()
|
||||
vlan_translation_rule_list: List[VLANTranslationRuleType] = strawberry_django.field()
|
||||
|
||||
vrf: VRFType = strawberry_django.field()
|
||||
vrf_list: List[VRFType] = strawberry_django.field()
|
||||
|
@ -27,6 +27,8 @@ __all__ = (
|
||||
'ServiceTemplateType',
|
||||
'VLANType',
|
||||
'VLANGroupType',
|
||||
'VLANTranslationPolicyType',
|
||||
'VLANTranslationRuleType',
|
||||
'VRFType',
|
||||
)
|
||||
|
||||
@ -274,6 +276,24 @@ class VLANGroupType(OrganizationalObjectType):
|
||||
return self.scope
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VLANTranslationPolicy,
|
||||
fields='__all__',
|
||||
filters=VLANTranslationPolicyFilter
|
||||
)
|
||||
class VLANTranslationPolicyType(NetBoxObjectType):
|
||||
rules: List[Annotated["VLANTranslationRuleType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VLANTranslationRule,
|
||||
fields='__all__',
|
||||
filters=VLANTranslationRuleFilter
|
||||
)
|
||||
class VLANTranslationRuleType(NetBoxObjectType):
|
||||
policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] = strawberry_django.field(select_related=["policy"])
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VRF,
|
||||
fields='__all__',
|
||||
|
@ -0,0 +1,62 @@
|
||||
# Generated by Django 5.0.9 on 2024-10-11 19:45
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0121_customfield_related_object_filter'),
|
||||
('ipam', '0073_charfield_null_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VLANTranslationPolicy',
|
||||
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)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'VLAN translation policy',
|
||||
'verbose_name_plural': 'VLAN translation policies',
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VLANTranslationRule',
|
||||
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)),
|
||||
('local_vid', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)])),
|
||||
('remote_vid', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)])),
|
||||
('policy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='ipam.vlantranslationpolicy')),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'VLAN translation rule',
|
||||
'ordering': ('policy', 'local_vid',),
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='vlantranslationrule',
|
||||
constraint=models.UniqueConstraint(fields=('policy', 'local_vid'), name='ipam_vlantranslationrule_unique_policy_local_vid'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='vlantranslationrule',
|
||||
constraint=models.UniqueConstraint(fields=('policy', 'remote_vid'), name='ipam_vlantranslationrule_unique_policy_remote_vid'),
|
||||
),
|
||||
]
|
@ -10,13 +10,15 @@ from dcim.models import Interface
|
||||
from ipam.choices import *
|
||||
from ipam.constants import *
|
||||
from ipam.querysets import VLANQuerySet, VLANGroupQuerySet
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from netbox.models import OrganizationalModel, PrimaryModel, NetBoxModel
|
||||
from utilities.data import check_ranges_overlap, ranges_to_string
|
||||
from virtualization.models import VMInterface
|
||||
|
||||
__all__ = (
|
||||
'VLAN',
|
||||
'VLANGroup',
|
||||
'VLANTranslationPolicy',
|
||||
'VLANTranslationRule',
|
||||
)
|
||||
|
||||
|
||||
@ -273,3 +275,73 @@ class VLAN(PrimaryModel):
|
||||
@property
|
||||
def l2vpn_termination(self):
|
||||
return self.l2vpn_terminations.first()
|
||||
|
||||
|
||||
class VLANTranslationPolicy(PrimaryModel):
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=100,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('VLAN translation policy')
|
||||
verbose_name_plural = _('VLAN translation policies')
|
||||
ordering = ('name',)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class VLANTranslationRule(NetBoxModel):
|
||||
policy = models.ForeignKey(
|
||||
to=VLANTranslationPolicy,
|
||||
related_name='rules',
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
description = models.CharField(
|
||||
verbose_name=_('description'),
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
local_vid = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('Local VLAN ID'),
|
||||
validators=(
|
||||
MinValueValidator(VLAN_VID_MIN),
|
||||
MaxValueValidator(VLAN_VID_MAX)
|
||||
),
|
||||
help_text=_("Numeric VLAN ID (1-4094)")
|
||||
)
|
||||
remote_vid = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('Remote VLAN ID'),
|
||||
validators=(
|
||||
MinValueValidator(VLAN_VID_MIN),
|
||||
MaxValueValidator(VLAN_VID_MAX)
|
||||
),
|
||||
help_text=_("Numeric VLAN ID (1-4094)")
|
||||
)
|
||||
prerequisite_models = (
|
||||
'ipam.VLANTranslationPolicy',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('VLAN translation rule')
|
||||
ordering = ('policy', 'local_vid',)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('policy', 'local_vid'),
|
||||
name='%(app_label)s_%(class)s_unique_policy_local_vid'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('policy', 'remote_vid'),
|
||||
name='%(app_label)s_%(class)s_unique_policy_remote_vid'
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.local_vid} -> {self.remote_vid} ({self.policy})'
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
objectchange.related_object = self.policy
|
||||
return objectchange
|
||||
|
@ -160,6 +160,27 @@ class VLANGroupIndex(SearchIndex):
|
||||
display_attrs = ('scope_type', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class VLANTranslationPolicyIndex(SearchIndex):
|
||||
model = models.VLANTranslationPolicy
|
||||
fields = (
|
||||
('name', 100),
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@register_search
|
||||
class VLANTranslationRuleIndex(SearchIndex):
|
||||
model = models.VLANTranslationRule
|
||||
fields = (
|
||||
('policy', 100),
|
||||
('local_vid', 200),
|
||||
('remote_vid', 200),
|
||||
)
|
||||
display_attrs = ('policy', 'local_vid', 'remote_vid')
|
||||
|
||||
|
||||
@register_search
|
||||
class VRFIndex(SearchIndex):
|
||||
model = models.VRF
|
||||
|
@ -16,6 +16,8 @@ __all__ = (
|
||||
'VLANMembersTable',
|
||||
'VLANTable',
|
||||
'VLANVirtualMachinesTable',
|
||||
'VLANTranslationPolicyTable',
|
||||
'VLANTranslationRuleTable',
|
||||
)
|
||||
|
||||
AVAILABLE_LABEL = mark_safe('<span class="badge text-bg-success">Available</span>')
|
||||
@ -244,3 +246,54 @@ class InterfaceVLANTable(NetBoxTable):
|
||||
def __init__(self, interface, *args, **kwargs):
|
||||
self.interface = interface
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
#
|
||||
# VLAN Translation
|
||||
#
|
||||
|
||||
class VLANTranslationPolicyTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
description = tables.Column(
|
||||
verbose_name=_('Description'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='ipam:vlantranslationpolicy_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VLANTranslationPolicy
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'description', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'description')
|
||||
|
||||
|
||||
class VLANTranslationRuleTable(NetBoxTable):
|
||||
policy = tables.Column(
|
||||
verbose_name=_('Policy'),
|
||||
linkify=True
|
||||
)
|
||||
local_vid = tables.Column(
|
||||
verbose_name=_('Local VID'),
|
||||
linkify=True
|
||||
)
|
||||
remote_vid = tables.Column(
|
||||
verbose_name=_('Remote VID'),
|
||||
)
|
||||
description = tables.Column(
|
||||
verbose_name=_('Description'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='ipam:vlantranslationrule_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VLANTranslationRule
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'policy', 'local_vid', 'remote_vid', 'description', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'policy', 'local_vid', 'remote_vid', 'description')
|
||||
|
@ -1020,6 +1020,112 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
|
||||
self.assertTrue(content['detail'].startswith('Unable to delete object.'))
|
||||
|
||||
|
||||
class VLANTranslationPolicyTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VLANTranslationPolicy
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'url',]
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
vlan_translation_policies = (
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 1',
|
||||
description='foobar1',
|
||||
),
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 2',
|
||||
description='foobar2',
|
||||
),
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 3',
|
||||
description='foobar3',
|
||||
),
|
||||
)
|
||||
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Policy 4',
|
||||
'description': 'foobar4',
|
||||
},
|
||||
{
|
||||
'name': 'Policy 5',
|
||||
'description': 'foobar5',
|
||||
},
|
||||
{
|
||||
'name': 'Policy 6',
|
||||
'description': 'foobar6',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class VLANTranslationRuleTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VLANTranslationRule
|
||||
brief_fields = ['id', 'local_vid', 'policy', 'remote_vid',]
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
vlan_translation_policies = (
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 1',
|
||||
description='foobar1',
|
||||
),
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 2',
|
||||
description='foobar2',
|
||||
),
|
||||
)
|
||||
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||
|
||||
vlan_translation_rules = (
|
||||
VLANTranslationRule(
|
||||
policy=vlan_translation_policies[0],
|
||||
local_vid=100,
|
||||
remote_vid=200,
|
||||
description='foo',
|
||||
),
|
||||
VLANTranslationRule(
|
||||
policy=vlan_translation_policies[0],
|
||||
local_vid=101,
|
||||
remote_vid=201,
|
||||
description='bar',
|
||||
),
|
||||
VLANTranslationRule(
|
||||
policy=vlan_translation_policies[1],
|
||||
local_vid=102,
|
||||
remote_vid=202,
|
||||
description='baz',
|
||||
),
|
||||
)
|
||||
VLANTranslationRule.objects.bulk_create(vlan_translation_rules)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'policy': vlan_translation_policies[0].pk,
|
||||
'local_vid': 300,
|
||||
'remote_vid': 400,
|
||||
},
|
||||
{
|
||||
'policy': vlan_translation_policies[0].pk,
|
||||
'local_vid': 301,
|
||||
'remote_vid': 401,
|
||||
},
|
||||
{
|
||||
'policy': vlan_translation_policies[1].pk,
|
||||
'local_vid': 302,
|
||||
'remote_vid': 402,
|
||||
},
|
||||
]
|
||||
|
||||
cls.bulk_update_data = {
|
||||
'policy': vlan_translation_policies[1].pk,
|
||||
}
|
||||
|
||||
|
||||
class ServiceTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ServiceTemplate
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'ports', 'protocol', 'url']
|
||||
|
@ -1899,6 +1899,99 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class VLANTranslationPolicyTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VLANTranslationPolicy.objects.all()
|
||||
filterset = VLANTranslationPolicyFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
vlan_translation_policies = (
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 1',
|
||||
description='foobar1',
|
||||
),
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 2',
|
||||
description='foobar2',
|
||||
),
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 3',
|
||||
description='foobar3',
|
||||
),
|
||||
)
|
||||
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Policy 1', 'Policy 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class VLANTranslationRuleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VLANTranslationRule.objects.all()
|
||||
filterset = VLANTranslationRuleFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
vlan_translation_policies = (
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 1',
|
||||
description='foobar1',
|
||||
),
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 2',
|
||||
description='foobar2',
|
||||
),
|
||||
)
|
||||
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||
|
||||
vlan_translation_rules = (
|
||||
VLANTranslationRule(
|
||||
policy=vlan_translation_policies[0],
|
||||
local_vid=100,
|
||||
remote_vid=200,
|
||||
description='foo',
|
||||
),
|
||||
VLANTranslationRule(
|
||||
policy=vlan_translation_policies[0],
|
||||
local_vid=101,
|
||||
remote_vid=201,
|
||||
description='bar',
|
||||
),
|
||||
VLANTranslationRule(
|
||||
policy=vlan_translation_policies[1],
|
||||
local_vid=100,
|
||||
remote_vid=200,
|
||||
description='baz',
|
||||
),
|
||||
)
|
||||
VLANTranslationRule.objects.bulk_create(vlan_translation_rules)
|
||||
|
||||
def test_policy_id(self):
|
||||
policies = VLANTranslationPolicy.objects.all()[:2]
|
||||
params = {'policy_id': [policies[0].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'policy': [policies[0].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_local_vid(self):
|
||||
params = {'local_vid': [100]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_remote_vid(self):
|
||||
params = {'remote_vid': [200]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foo', 'bar']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ServiceTemplate.objects.all()
|
||||
filterset = ServiceTemplateFilterSet
|
||||
|
@ -863,6 +863,121 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class VLANTranslationPolicyTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VLANTranslationPolicy
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
vlan_translation_policies = (
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 1',
|
||||
description='foobar1',
|
||||
),
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 2',
|
||||
description='foobar2',
|
||||
),
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 3',
|
||||
description='foobar3',
|
||||
),
|
||||
)
|
||||
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Policy999',
|
||||
'description': 'A new VLAN Translation Policy',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,description",
|
||||
"Policy101,foobar1",
|
||||
"Policy102,foobar2",
|
||||
"Policy103,foobar3",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,description",
|
||||
f"{vlan_translation_policies[0].pk},Policy101,New description 1",
|
||||
f"{vlan_translation_policies[1].pk},Policy102,New description 2",
|
||||
f"{vlan_translation_policies[2].pk},Policy103,New description 3",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class VLANTranslationRuleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VLANTranslationRule
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
vlan_translation_policies = (
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 1',
|
||||
description='foobar1',
|
||||
),
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 2',
|
||||
description='foobar2',
|
||||
),
|
||||
VLANTranslationPolicy(
|
||||
name='Policy 3',
|
||||
description='foobar3',
|
||||
),
|
||||
)
|
||||
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||
|
||||
vlan_translation_rules = (
|
||||
VLANTranslationRule(
|
||||
policy=vlan_translation_policies[0],
|
||||
local_vid=100,
|
||||
remote_vid=200,
|
||||
),
|
||||
VLANTranslationRule(
|
||||
policy=vlan_translation_policies[0],
|
||||
local_vid=101,
|
||||
remote_vid=201,
|
||||
),
|
||||
VLANTranslationRule(
|
||||
policy=vlan_translation_policies[1],
|
||||
local_vid=102,
|
||||
remote_vid=202,
|
||||
),
|
||||
)
|
||||
VLANTranslationRule.objects.bulk_create(vlan_translation_rules)
|
||||
|
||||
cls.form_data = {
|
||||
'policy': vlan_translation_policies[0].pk,
|
||||
'local_vid': 300,
|
||||
'remote_vid': 400,
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"policy,local_vid,remote_vid",
|
||||
f"{vlan_translation_policies[0].pk},103,203",
|
||||
f"{vlan_translation_policies[0].pk},104,204",
|
||||
f"{vlan_translation_policies[1].pk},105,205",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,policy,local_vid,remote_vid",
|
||||
f"{vlan_translation_rules[0].pk},{vlan_translation_policies[1].pk},105,205",
|
||||
f"{vlan_translation_rules[1].pk},{vlan_translation_policies[1].pk},106,206",
|
||||
f"{vlan_translation_rules[2].pk},{vlan_translation_policies[0].pk},107,207",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'policy': vlan_translation_policies[2].pk,
|
||||
}
|
||||
|
||||
|
||||
class ServiceTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = ServiceTemplate
|
||||
|
||||
|
@ -116,6 +116,22 @@ urlpatterns = [
|
||||
path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||
path('vlans/<int:pk>/', include(get_model_urls('ipam', 'vlan'))),
|
||||
|
||||
# VLAN Translation Policies
|
||||
path('vlan-translation-policies/', views.VLANTranslationPolicyListView.as_view(), name='vlantranslationpolicy_list'),
|
||||
path('vlan-translation-policies/add/', views.VLANTranslationPolicyEditView.as_view(), name='vlantranslationpolicy_add'),
|
||||
path('vlan-translation-policies/import/', views.VLANTranslationPolicyBulkImportView.as_view(), name='vlantranslationpolicy_import'),
|
||||
path('vlan-translation-policies/edit/', views.VLANTranslationPolicyBulkEditView.as_view(), name='vlantranslationpolicy_bulk_edit'),
|
||||
path('vlan-translation-policies/delete/', views.VLANTranslationPolicyBulkDeleteView.as_view(), name='vlantranslationpolicy_bulk_delete'),
|
||||
path('vlan-translation-policies/<int:pk>/', include(get_model_urls('ipam', 'vlantranslationpolicy'))),
|
||||
|
||||
# VLAN Translation Rules
|
||||
path('vlan-translation-rules/', views.VLANTranslationRuleListView.as_view(), name='vlantranslationrule_list'),
|
||||
path('vlan-translation-rules/add/', views.VLANTranslationRuleEditView.as_view(), name='vlantranslationrule_add'),
|
||||
path('vlan-translation-rules/import/', views.VLANTranslationRuleBulkImportView.as_view(), name='vlantranslationrule_import'),
|
||||
path('vlan-translation-rules/edit/', views.VLANTranslationRuleBulkEditView.as_view(), name='vlantranslationrule_bulk_edit'),
|
||||
path('vlan-translation-rules/delete/', views.VLANTranslationRuleBulkDeleteView.as_view(), name='vlantranslationrule_bulk_delete'),
|
||||
path('vlan-translation-rules/<int:pk>/', include(get_model_urls('ipam', 'vlantranslationrule'))),
|
||||
|
||||
# Service templates
|
||||
path('service-templates/', views.ServiceTemplateListView.as_view(), name='servicetemplate_list'),
|
||||
path('service-templates/add/', views.ServiceTemplateEditView.as_view(), name='servicetemplate_add'),
|
||||
|
@ -3,12 +3,13 @@ from django.db.models import Prefetch
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.models import Provider
|
||||
from dcim.filtersets import InterfaceFilterSet
|
||||
from dcim.forms import InterfaceFilterForm
|
||||
from dcim.models import Interface, Site
|
||||
from ipam.tables import VLANTranslationRuleTable
|
||||
from netbox.views import generic
|
||||
from tenancy.views import ObjectContactsView
|
||||
from utilities.query import count_related
|
||||
@ -986,6 +987,110 @@ class VLANGroupVLANsView(generic.ObjectChildrenView):
|
||||
return queryset
|
||||
|
||||
|
||||
#
|
||||
# VLAN Translation Policies
|
||||
#
|
||||
|
||||
class VLANTranslationPolicyListView(generic.ObjectListView):
|
||||
queryset = VLANTranslationPolicy.objects.all()
|
||||
filterset = filtersets.VLANTranslationPolicyFilterSet
|
||||
filterset_form = forms.VLANTranslationPolicyFilterForm
|
||||
table = tables.VLANTranslationPolicyTable
|
||||
|
||||
|
||||
@register_model_view(VLANTranslationPolicy)
|
||||
class VLANTranslationPolicyView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = VLANTranslationPolicy.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
vlan_translation_table = VLANTranslationRuleTable(
|
||||
data=instance.rules.all(),
|
||||
orderable=False
|
||||
)
|
||||
return {
|
||||
'vlan_translation_table': vlan_translation_table,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(VLANTranslationPolicy, 'edit')
|
||||
class VLANTranslationPolicyEditView(generic.ObjectEditView):
|
||||
queryset = VLANTranslationPolicy.objects.all()
|
||||
form = forms.VLANTranslationPolicyForm
|
||||
|
||||
|
||||
@register_model_view(VLANTranslationPolicy, 'delete')
|
||||
class VLANTranslationPolicyDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VLANTranslationPolicy.objects.all()
|
||||
|
||||
|
||||
class VLANTranslationPolicyBulkImportView(generic.BulkImportView):
|
||||
queryset = VLANTranslationPolicy.objects.all()
|
||||
model_form = forms.VLANTranslationPolicyImportForm
|
||||
|
||||
|
||||
class VLANTranslationPolicyBulkEditView(generic.BulkEditView):
|
||||
queryset = VLANTranslationPolicy.objects.all()
|
||||
filterset = filtersets.VLANTranslationPolicyFilterSet
|
||||
table = tables.VLANTranslationPolicyTable
|
||||
form = forms.VLANTranslationPolicyBulkEditForm
|
||||
|
||||
|
||||
class VLANTranslationPolicyBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VLANTranslationPolicy.objects.all()
|
||||
filterset = filtersets.VLANTranslationPolicyFilterSet
|
||||
table = tables.VLANTranslationPolicyTable
|
||||
|
||||
|
||||
#
|
||||
# VLAN Translation Rules
|
||||
#
|
||||
|
||||
class VLANTranslationRuleListView(generic.ObjectListView):
|
||||
queryset = VLANTranslationRule.objects.all()
|
||||
filterset = filtersets.VLANTranslationRuleFilterSet
|
||||
filterset_form = forms.VLANTranslationRuleFilterForm
|
||||
table = tables.VLANTranslationRuleTable
|
||||
|
||||
|
||||
@register_model_view(VLANTranslationRule)
|
||||
class VLANTranslationRuleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = VLANTranslationRule.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(VLANTranslationRule, 'edit')
|
||||
class VLANTranslationRuleEditView(generic.ObjectEditView):
|
||||
queryset = VLANTranslationRule.objects.all()
|
||||
form = forms.VLANTranslationRuleForm
|
||||
|
||||
|
||||
@register_model_view(VLANTranslationRule, 'delete')
|
||||
class VLANTranslationRuleDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VLANTranslationRule.objects.all()
|
||||
|
||||
|
||||
class VLANTranslationRuleBulkImportView(generic.BulkImportView):
|
||||
queryset = VLANTranslationRule.objects.all()
|
||||
model_form = forms.VLANTranslationRuleImportForm
|
||||
|
||||
|
||||
class VLANTranslationRuleBulkEditView(generic.BulkEditView):
|
||||
queryset = VLANTranslationRule.objects.all()
|
||||
filterset = filtersets.VLANTranslationRuleFilterSet
|
||||
table = tables.VLANTranslationRuleTable
|
||||
form = forms.VLANTranslationRuleBulkEditForm
|
||||
|
||||
|
||||
class VLANTranslationRuleBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VLANTranslationRule.objects.all()
|
||||
filterset = filtersets.VLANTranslationRuleFilterSet
|
||||
table = tables.VLANTranslationRuleTable
|
||||
|
||||
|
||||
#
|
||||
# FHRP groups
|
||||
#
|
||||
|
@ -194,6 +194,8 @@ IPAM_MENU = Menu(
|
||||
items=(
|
||||
get_model_item('ipam', 'vlan', _('VLANs')),
|
||||
get_model_item('ipam', 'vlangroup', _('VLAN Groups')),
|
||||
get_model_item('ipam', 'vlantranslationpolicy', _('VLAN Translation Policies')),
|
||||
get_model_item('ipam', 'vlantranslationrule', _('VLAN Translation Rules')),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
|
@ -4,7 +4,7 @@ from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import View
|
||||
|
||||
from core.models import Job, ObjectChange
|
||||
|
@ -1,18 +1,19 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if termination.site %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>
|
||||
{% if termination.site.region %}
|
||||
{{ termination.site.region|linkify }} /
|
||||
{% endif %}
|
||||
{{ termination.site|linkify }}
|
||||
</td>
|
||||
<th scope="row">{% trans "Termination point" %}</th>
|
||||
{% if termination.termination %}
|
||||
<td>
|
||||
{{ termination.termination|linkify }}
|
||||
<div class="fs-5 text-muted">{% trans termination.termination_type.name|bettertitle %}</div>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Termination" %}</th>
|
||||
<th scope="row">{% trans "Connection" %}</th>
|
||||
<td>
|
||||
{% if termination.mark_connected %}
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||
@ -57,12 +58,6 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider Network" %}</th>
|
||||
<td>{{ termination.provider_network.provider|linkify }} / {{ termination.provider_network|linkify }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Speed" %}</th>
|
||||
<td>
|
||||
|
@ -133,6 +133,10 @@
|
||||
<th scope="row">{% trans "VRF" %}</th>
|
||||
<td>{{ object.vrf|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VLAN Translation" %}</th>
|
||||
<td>{{ object.vlan_translation_policy|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% if not object.is_virtual %}
|
||||
@ -355,6 +359,13 @@
|
||||
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||
</div>
|
||||
</div>
|
||||
{% if object.vlan_translation_policy %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% include 'inc/panel_table.html' with table=vlan_translation_table heading="VLAN Translation" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if object.is_bridge %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
|
@ -37,101 +37,104 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</h2>
|
||||
{% if module.scripts %}
|
||||
<table class="table table-hover scripts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Last Run" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for script in module.scripts.all %}
|
||||
{% with last_job=script.get_latest_jobs|first %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if script.is_executable %}
|
||||
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
|
||||
{% with scripts=module.scripts.all %}
|
||||
{% if scripts %}
|
||||
<table class="table table-hover scripts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Last Run" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for script in scripts %}
|
||||
{% with last_job=script.get_latest_jobs|first %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if script.is_executable %}
|
||||
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
|
||||
<span class="text-danger">
|
||||
<i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ script.python_class.Meta.description|markdown|placeholder }}</td>
|
||||
{% if last_job %}
|
||||
<td>
|
||||
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% badge last_job.get_status_display last_job.get_status_color %}
|
||||
</td>
|
||||
{% else %}
|
||||
<a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
|
||||
<span class="text-danger">
|
||||
<i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i>
|
||||
</span>
|
||||
<td class="text-muted">{% trans "Never" %}</td>
|
||||
<td>{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ script.python_class.Meta.description|markdown|placeholder }}</td>
|
||||
<td>
|
||||
{% if request.user|can_run:script and script.is_executable %}
|
||||
<div class="float-end d-print-none">
|
||||
<form action="{% url 'extras:script' script.pk %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" name="_run" class="btn btn-primary btn-sm">
|
||||
{% if last_job %}
|
||||
<i class="mdi mdi-replay"></i> {% trans "Run Again" %}
|
||||
{% else %}
|
||||
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if last_job %}
|
||||
<td>
|
||||
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% badge last_job.get_status_display last_job.get_status_color %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="text-muted">{% trans "Never" %}</td>
|
||||
<td>{{ ''|placeholder }}</td>
|
||||
{% for test_name, data in last_job.data.tests.items %}
|
||||
<tr>
|
||||
<td colspan="4" class="method">
|
||||
<span class="ps-3">{{ test_name }}</span>
|
||||
</td>
|
||||
<td class="text-end text-nowrap script-stats">
|
||||
<span class="badge text-bg-success">{{ data.success }}</span>
|
||||
<span class="badge text-bg-info">{{ data.info }}</span>
|
||||
<span class="badge text-bg-warning">{{ data.warning }}</span>
|
||||
<span class="badge text-bg-danger">{{ data.failure }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% elif not last_job.data.log %}
|
||||
{# legacy #}
|
||||
{% for method, stats in last_job.data.items %}
|
||||
<tr>
|
||||
<td colspan="4" class="method">
|
||||
<span class="ps-3">{{ method }}</span>
|
||||
</td>
|
||||
<td class="text-end text-nowrap report-stats">
|
||||
<span class="badge bg-success">{{ stats.success }}</span>
|
||||
<span class="badge bg-info">{{ stats.info }}</span>
|
||||
<span class="badge bg-warning">{{ stats.warning }}</span>
|
||||
<span class="badge bg-danger">{{ stats.failure }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if request.user|can_run:script and script.is_executable %}
|
||||
<div class="float-end d-print-none">
|
||||
<form action="{% url 'extras:script' script.pk %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" name="_run" class="btn btn-primary btn-sm">
|
||||
{% if last_job %}
|
||||
<i class="mdi mdi-replay"></i> {% trans "Run Again" %}
|
||||
{% else %}
|
||||
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if last_job %}
|
||||
{% for test_name, data in last_job.data.tests.items %}
|
||||
<tr>
|
||||
<td colspan="4" class="method">
|
||||
<span class="ps-3">{{ test_name }}</span>
|
||||
</td>
|
||||
<td class="text-end text-nowrap script-stats">
|
||||
<span class="badge text-bg-success">{{ data.success }}</span>
|
||||
<span class="badge text-bg-info">{{ data.info }}</span>
|
||||
<span class="badge text-bg-warning">{{ data.warning }}</span>
|
||||
<span class="badge text-bg-danger">{{ data.failure }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% elif not last_job.data.log %}
|
||||
{# legacy #}
|
||||
{% for method, stats in last_job.data.items %}
|
||||
<tr>
|
||||
<td colspan="4" class="method">
|
||||
<span class="ps-3">{{ method }}</span>
|
||||
</td>
|
||||
<td class="text-end text-nowrap report-stats">
|
||||
<span class="badge bg-success">{{ stats.success }}</span>
|
||||
<span class="badge bg-info">{{ stats.info }}</span>
|
||||
<span class="badge bg-warning">{{ stats.warning }}</span>
|
||||
<span class="badge bg-danger">{{ stats.failure }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="mdi mdi-alert"></i> Could not load scripts from {{ module.name }}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="mdi mdi-alert"></i>
|
||||
{% blocktrans with module=module.name %}Could not load scripts from module {{ module }}{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
|
55
netbox/templates/ipam/vlantranslationpolicy.html
Normal file
55
netbox/templates/ipam/vlantranslationpolicy.html
Normal file
@ -0,0 +1,55 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-4">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "VLAN Translation Policy" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-8">
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "VLAN Translation Rules" %}
|
||||
{% if perms.ipam.add_vlantranslationrule %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'ipam:vlantranslationrule_add' %}?device={{ object.device.pk }}&policy={{ 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 Rule" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'ipam:vlantranslationrule_list' policy_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
45
netbox/templates/ipam/vlantranslationrule.html
Normal file
45
netbox/templates/ipam/vlantranslationrule.html
Normal file
@ -0,0 +1,45 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-4">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "VLAN Translation Rule" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Policy" %}</th>
|
||||
<td>{{ object.policy|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Local VID" %}</th>
|
||||
<td>{{ object.local_vid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Remote VID" %}</th>
|
||||
<td>{{ object.remote_vid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-8">
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% 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">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -67,6 +67,10 @@
|
||||
<th scope="row">{% trans "Tunnel" %}</th>
|
||||
<td>{{ object.tunnel_termination.tunnel|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VLAN Translation" %}</th>
|
||||
<td>{{ object.vlan_translation_policy|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
@ -100,6 +104,13 @@
|
||||
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||
</div>
|
||||
</div>
|
||||
{% if object.vlan_translation_policy %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% include 'inc/panel_table.html' with table=vlan_translation_table heading="VLAN Translation" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% include 'inc/panel_table.html' with table=child_interfaces_table heading="Child Interfaces" %}
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.views import generic
|
||||
from utilities.query import count_related
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-28 19:20+0000\n"
|
||||
"POT-Creation-Date: 2024-10-29 21:00+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -8,7 +8,7 @@ from dcim.api.serializers_.sites import SiteSerializer
|
||||
from dcim.choices import InterfaceModeChoices
|
||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||
from ipam.api.serializers_.ip import IPAddressSerializer
|
||||
from ipam.api.serializers_.vlans import VLANSerializer
|
||||
from ipam.api.serializers_.vlans import VLANSerializer, VLANTranslationPolicySerializer
|
||||
from ipam.api.serializers_.vrfs import VRFSerializer
|
||||
from ipam.models import VLAN
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
@ -89,6 +89,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
vlan_translation_policy = VLANTranslationPolicySerializer(nested=True, required=False, allow_null=True)
|
||||
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
||||
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
|
||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||
@ -105,6 +106,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
||||
'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu',
|
||||
'mac_address', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'l2vpn_termination',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups',
|
||||
'vlan_translation_policy',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
|
||||
|
||||
|
@ -7,7 +7,7 @@ from dcim.forms.common import InterfaceCommonForm
|
||||
from dcim.forms.mixins import ScopedForm
|
||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.models import IPAddress, VLAN, VLANGroup, VRF
|
||||
from ipam.models import IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import ConfirmationForm
|
||||
@ -339,20 +339,25 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
|
||||
required=False,
|
||||
label=_('VRF')
|
||||
)
|
||||
vlan_translation_policy = DynamicModelChoiceField(
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
required=False,
|
||||
label=_('VLAN Translation Policy')
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('virtual_machine', 'name', 'description', 'tags', name=_('Interface')),
|
||||
FieldSet('vrf', 'mac_address', name=_('Addressing')),
|
||||
FieldSet('mtu', 'enabled', name=_('Operation')),
|
||||
FieldSet('parent', 'bridge', name=_('Related Interfaces')),
|
||||
FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')),
|
||||
FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vlan_translation_policy', name=_('802.1Q Switching')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VMInterface
|
||||
fields = [
|
||||
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
|
||||
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
|
||||
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', 'vlan_translation_policy',
|
||||
]
|
||||
labels = {
|
||||
'mode': '802.1Q Mode',
|
||||
|
@ -107,6 +107,7 @@ class VMInterfaceType(IPAddressesMixin, ComponentType):
|
||||
bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
|
||||
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
||||
bridge_interfaces: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]
|
||||
|
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.0.9 on 2024-10-11 19:45
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0074_vlantranslationpolicy_vlantranslationrule'),
|
||||
('virtualization', '0041_charfield_null_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vminterface',
|
||||
name='vlan_translation_policy',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.vlantranslationpolicy'),
|
||||
),
|
||||
]
|
@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('virtualization', '0041_charfield_null_choices'),
|
||||
('virtualization', '0042_vminterface_vlan_translation_policy'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -21,7 +21,7 @@ def populate_denormalized_fields(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('virtualization', '0042_cluster_scope'),
|
||||
('virtualization', '0043_cluster_scope'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -1,7 +1,7 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||
from ipam.models import IPAddress, VRF
|
||||
from ipam.models import IPAddress, VLANTranslationPolicy, VRF
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
||||
from virtualization.choices import *
|
||||
@ -563,6 +563,13 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
VirtualMachine.objects.bulk_create(vms)
|
||||
|
||||
vlan_translation_policies = (
|
||||
VLANTranslationPolicy(name='Policy 1'),
|
||||
VLANTranslationPolicy(name='Policy 2'),
|
||||
VLANTranslationPolicy(name='Policy 3'),
|
||||
)
|
||||
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||
|
||||
interfaces = (
|
||||
VMInterface(
|
||||
virtual_machine=vms[0],
|
||||
@ -571,7 +578,8 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
mtu=100,
|
||||
mac_address='00-00-00-00-00-01',
|
||||
vrf=vrfs[0],
|
||||
description='foobar1'
|
||||
description='foobar1',
|
||||
vlan_translation_policy=vlan_translation_policies[0],
|
||||
),
|
||||
VMInterface(
|
||||
virtual_machine=vms[1],
|
||||
@ -580,7 +588,8 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
mtu=200,
|
||||
mac_address='00-00-00-00-00-02',
|
||||
vrf=vrfs[1],
|
||||
description='foobar2'
|
||||
description='foobar2',
|
||||
vlan_translation_policy=vlan_translation_policies[0],
|
||||
),
|
||||
VMInterface(
|
||||
virtual_machine=vms[2],
|
||||
@ -660,6 +669,13 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_vlan_translation_policy(self):
|
||||
vlan_translation_policies = VLANTranslationPolicy.objects.all()[:2]
|
||||
params = {'vlan_translation_policy_id': [vlan_translation_policies[0].pk, vlan_translation_policies[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'vlan_translation_policy': [vlan_translation_policies[0].name, vlan_translation_policies[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class VirtualDiskTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualDisk.objects.all()
|
||||
|
@ -6,7 +6,7 @@ from django.db.models import Prefetch, Sum
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.base import RedirectView
|
||||
from jinja2.exceptions import TemplateError
|
||||
|
||||
@ -16,7 +16,7 @@ from dcim.models import Device
|
||||
from dcim.tables import DeviceTable
|
||||
from extras.views import ObjectConfigContextView
|
||||
from ipam.models import IPAddress
|
||||
from ipam.tables import InterfaceVLANTable
|
||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||
from netbox.views import generic
|
||||
from tenancy.views import ObjectContactsView
|
||||
@ -516,6 +516,14 @@ class VMInterfaceView(generic.ObjectView):
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# Get VLAN translation rules
|
||||
vlan_translation_table = None
|
||||
if instance.vlan_translation_policy:
|
||||
vlan_translation_table = VLANTranslationRuleTable(
|
||||
data=instance.vlan_translation_policy.rules.all(),
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# Get assigned VLANs and annotate whether each is tagged or untagged
|
||||
vlans = []
|
||||
if instance.untagged_vlan is not None:
|
||||
@ -533,6 +541,7 @@ class VMInterfaceView(generic.ObjectView):
|
||||
return {
|
||||
'child_interfaces_table': child_interfaces_tables,
|
||||
'vlan_table': vlan_table,
|
||||
'vlan_translation_table': vlan_translation_table,
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user