mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-29 11:56:25 -06:00
Merge branch 'feature' into 18023-model-list-view-registration
This commit is contained in:
commit
b59f4206a7
33
docs/models/circuits/virtualcircuit.md
Normal file
33
docs/models/circuits/virtualcircuit.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Virtual Circuits
|
||||||
|
|
||||||
|
A virtual circuit can connect two or more interfaces atop a set of decoupled physical connections. For example, it's very common to form a virtual connection between two virtual interfaces, each of which is bound to a physical interface on its respective device and physically connected to a [provider network](./providernetwork.md) via an independent [physical circuit](./circuit.md).
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
### Provider Network
|
||||||
|
|
||||||
|
The [provider network](./providernetwork.md) across which the virtual circuit is formed.
|
||||||
|
|
||||||
|
### Provider Account
|
||||||
|
|
||||||
|
The [provider account](./provideraccount.md) with which the virtual circuit is associated (if any).
|
||||||
|
|
||||||
|
### Circuit ID
|
||||||
|
|
||||||
|
The unique identifier assigned to the virtual circuit by its [provider](./provider.md).
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
The operational status of the virtual circuit. By default, the following statuses are available:
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
|----------------|
|
||||||
|
| Planned |
|
||||||
|
| Provisioning |
|
||||||
|
| Active |
|
||||||
|
| Offline |
|
||||||
|
| Deprovisioning |
|
||||||
|
| Decommissioned |
|
||||||
|
|
||||||
|
!!! tip "Custom circuit statuses"
|
||||||
|
Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
21
docs/models/circuits/virtualcircuittermination.md
Normal file
21
docs/models/circuits/virtualcircuittermination.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Virtual Circuit Terminations
|
||||||
|
|
||||||
|
This model represents the connection of a virtual [interface](../dcim/interface.md) to a [virtual circuit](./virtualcircuit.md).
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
### Virtual Circuit
|
||||||
|
|
||||||
|
The [virtual circuit](./virtualcircuit.md) to which the interface is connected.
|
||||||
|
|
||||||
|
### Interface
|
||||||
|
|
||||||
|
The [interface](../dcim/interface.md) connected to the virtual circuit.
|
||||||
|
|
||||||
|
### Role
|
||||||
|
|
||||||
|
The functional role of the termination. This depends on the virtual circuit's topology, which is typically either peer-to-peer or hub-and-spoke (multipoint). Valid choices include:
|
||||||
|
|
||||||
|
* Peer
|
||||||
|
* Hub
|
||||||
|
* Spoke
|
@ -45,9 +45,12 @@ The operation duplex (full, half, or auto).
|
|||||||
|
|
||||||
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
|
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
|
||||||
|
|
||||||
### MAC Address
|
### Primary MAC Address
|
||||||
|
|
||||||
The 48-bit MAC address (for Ethernet interfaces).
|
The [MAC address](./macaddress.md) assigned to this interface which is designated as its primary.
|
||||||
|
|
||||||
|
!!! note "Changed in NetBox v4.2"
|
||||||
|
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](./macaddress.md) object.
|
||||||
|
|
||||||
### WWN
|
### WWN
|
||||||
|
|
||||||
|
11
docs/models/dcim/macaddress.md
Normal file
11
docs/models/dcim/macaddress.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# MAC Addresses
|
||||||
|
|
||||||
|
A MAC address object in NetBox comprises a single Ethernet link layer address, and represents a MAC address as reported by or assigned to a network interface. MAC addresses can be assigned to [device](../dcim/device.md) and [virtual machine](../virtualization/virtualmachine.md) interfaces. A MAC address can be specified as the primary MAC address for a given device or VM interface.
|
||||||
|
|
||||||
|
Most interfaces have only a single MAC address, hard-coded at the factory. However, on some devices (particularly virtual interfaces) it is possible to assign additional MAC addresses or change existing ones. For this reason NetBox allows multiple MACAddress objects to be assigned to a single interface.
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
### MAC Address
|
||||||
|
|
||||||
|
The 48-bit MAC address, in colon-hexadecimal notation (e.g. `aa:bb:cc:11:22:33`).
|
@ -27,9 +27,12 @@ An interface on the same VM with which this interface is bridged.
|
|||||||
|
|
||||||
If not selected, this interface will be treated as disabled/inoperative.
|
If not selected, this interface will be treated as disabled/inoperative.
|
||||||
|
|
||||||
### MAC Address
|
### Primary MAC Address
|
||||||
|
|
||||||
The 48-bit MAC address (for Ethernet interfaces).
|
The [MAC address](./macaddress.md) assigned to this interface which is designated as its primary.
|
||||||
|
|
||||||
|
!!! note "Changed in NetBox v4.2"
|
||||||
|
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](./macaddress.md) object.
|
||||||
|
|
||||||
### MTU
|
### MTU
|
||||||
|
|
||||||
|
@ -174,6 +174,8 @@ nav:
|
|||||||
- Provider: 'models/circuits/provider.md'
|
- Provider: 'models/circuits/provider.md'
|
||||||
- Provider Account: 'models/circuits/provideraccount.md'
|
- Provider Account: 'models/circuits/provideraccount.md'
|
||||||
- Provider Network: 'models/circuits/providernetwork.md'
|
- Provider Network: 'models/circuits/providernetwork.md'
|
||||||
|
- Virtual Circuit: 'models/circuits/virtualcircuit.md'
|
||||||
|
- Virtual Circuit Termination: 'models/circuits/virtualcircuittermination.md'
|
||||||
- Core:
|
- Core:
|
||||||
- DataFile: 'models/core/datafile.md'
|
- DataFile: 'models/core/datafile.md'
|
||||||
- DataSource: 'models/core/datasource.md'
|
- DataSource: 'models/core/datasource.md'
|
||||||
|
@ -2,9 +2,13 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices
|
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
||||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||||
from circuits.models import Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType
|
from circuits.models import (
|
||||||
|
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
|
||||||
|
VirtualCircuitTermination,
|
||||||
|
)
|
||||||
|
from dcim.api.serializers_.device_components import InterfaceSerializer
|
||||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||||
@ -20,6 +24,8 @@ __all__ = (
|
|||||||
'CircuitGroupSerializer',
|
'CircuitGroupSerializer',
|
||||||
'CircuitTerminationSerializer',
|
'CircuitTerminationSerializer',
|
||||||
'CircuitTypeSerializer',
|
'CircuitTypeSerializer',
|
||||||
|
'VirtualCircuitSerializer',
|
||||||
|
'VirtualCircuitTerminationSerializer',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -156,3 +162,32 @@ class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
|||||||
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
|
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
|
brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitSerializer(NetBoxModelSerializer):
|
||||||
|
provider_network = ProviderNetworkSerializer(nested=True)
|
||||||
|
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||||
|
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||||
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualCircuit
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'status', 'tenant',
|
||||||
|
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||||
|
virtual_circuit = VirtualCircuitSerializer(nested=True)
|
||||||
|
role = ChoiceField(choices=VirtualCircuitTerminationRoleChoices, required=False)
|
||||||
|
interface = InterfaceSerializer(nested=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualCircuitTermination
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display_url', 'display', 'virtual_circuit', 'role', 'interface', 'description', 'tags',
|
||||||
|
'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'virtual_circuit', 'role', 'interface', 'description')
|
||||||
|
@ -17,5 +17,9 @@ router.register('circuit-terminations', views.CircuitTerminationViewSet)
|
|||||||
router.register('circuit-groups', views.CircuitGroupViewSet)
|
router.register('circuit-groups', views.CircuitGroupViewSet)
|
||||||
router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet)
|
router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet)
|
||||||
|
|
||||||
|
# Virtual circuits
|
||||||
|
router.register('virtual-circuits', views.VirtualCircuitViewSet)
|
||||||
|
router.register('virtual-circuit-terminations', views.VirtualCircuitTerminationViewSet)
|
||||||
|
|
||||||
app_name = 'circuits-api'
|
app_name = 'circuits-api'
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
@ -93,3 +93,23 @@ class ProviderNetworkViewSet(NetBoxModelViewSet):
|
|||||||
queryset = ProviderNetwork.objects.all()
|
queryset = ProviderNetwork.objects.all()
|
||||||
serializer_class = serializers.ProviderNetworkSerializer
|
serializer_class = serializers.ProviderNetworkSerializer
|
||||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual circuits
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualCircuitViewSet(NetBoxModelViewSet):
|
||||||
|
queryset = VirtualCircuit.objects.all()
|
||||||
|
serializer_class = serializers.VirtualCircuitSerializer
|
||||||
|
filterset_class = filtersets.VirtualCircuitFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual circuit terminations
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||||
|
queryset = VirtualCircuitTermination.objects.all()
|
||||||
|
serializer_class = serializers.VirtualCircuitTerminationSerializer
|
||||||
|
filterset_class = filtersets.VirtualCircuitTerminationFilterSet
|
||||||
|
@ -92,3 +92,19 @@ class CircuitPriorityChoices(ChoiceSet):
|
|||||||
(PRIORITY_TERTIARY, _('Tertiary')),
|
(PRIORITY_TERTIARY, _('Tertiary')),
|
||||||
(PRIORITY_INACTIVE, _('Inactive')),
|
(PRIORITY_INACTIVE, _('Inactive')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual circuits
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationRoleChoices(ChoiceSet):
|
||||||
|
ROLE_PEER = 'peer'
|
||||||
|
ROLE_HUB = 'hub'
|
||||||
|
ROLE_SPOKE = 'spoke'
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(ROLE_PEER, _('Peer'), 'green'),
|
||||||
|
(ROLE_HUB, _('Hub'), 'blue'),
|
||||||
|
(ROLE_SPOKE, _('Spoke'), 'orange'),
|
||||||
|
]
|
||||||
|
@ -3,7 +3,7 @@ from django.db.models import Q
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.filtersets import CabledObjectFilterSet
|
from dcim.filtersets import CabledObjectFilterSet
|
||||||
from dcim.models import Location, Region, Site, SiteGroup
|
from dcim.models import Interface, Location, Region, Site, SiteGroup
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||||
@ -20,6 +20,8 @@ __all__ = (
|
|||||||
'ProviderNetworkFilterSet',
|
'ProviderNetworkFilterSet',
|
||||||
'ProviderAccountFilterSet',
|
'ProviderAccountFilterSet',
|
||||||
'ProviderFilterSet',
|
'ProviderFilterSet',
|
||||||
|
'VirtualCircuitFilterSet',
|
||||||
|
'VirtualCircuitTerminationFilterSet',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -404,3 +406,108 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
|||||||
Q(circuit__cid__icontains=value) |
|
Q(circuit__cid__icontains=value) |
|
||||||
Q(group__name__icontains=value)
|
Q(group__name__icontains=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||||
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='provider_network__provider',
|
||||||
|
queryset=Provider.objects.all(),
|
||||||
|
label=_('Provider (ID)'),
|
||||||
|
)
|
||||||
|
provider = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='provider_network__provider__slug',
|
||||||
|
queryset=Provider.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label=_('Provider (slug)'),
|
||||||
|
)
|
||||||
|
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='provider_account',
|
||||||
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
label=_('Provider account (ID)'),
|
||||||
|
)
|
||||||
|
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='provider_account__account',
|
||||||
|
queryset=Provider.objects.all(),
|
||||||
|
to_field_name='account',
|
||||||
|
label=_('Provider account (account)'),
|
||||||
|
)
|
||||||
|
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
label=_('Provider network (ID)'),
|
||||||
|
)
|
||||||
|
status = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=CircuitStatusChoices,
|
||||||
|
null_value=None
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualCircuit
|
||||||
|
fields = ('id', 'cid', 'description')
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(cid__icontains=value) |
|
||||||
|
Q(description__icontains=value) |
|
||||||
|
Q(comments__icontains=value)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label=_('Search'),
|
||||||
|
)
|
||||||
|
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=VirtualCircuit.objects.all(),
|
||||||
|
label=_('Virtual circuit'),
|
||||||
|
)
|
||||||
|
role = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=VirtualCircuitTerminationRoleChoices,
|
||||||
|
null_value=None
|
||||||
|
)
|
||||||
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='virtual_circuit__provider_network__provider',
|
||||||
|
queryset=Provider.objects.all(),
|
||||||
|
label=_('Provider (ID)'),
|
||||||
|
)
|
||||||
|
provider = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='virtual_circuit__provider_network__provider__slug',
|
||||||
|
queryset=Provider.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label=_('Provider (slug)'),
|
||||||
|
)
|
||||||
|
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='virtual_circuit__provider_account',
|
||||||
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
label=_('Provider account (ID)'),
|
||||||
|
)
|
||||||
|
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='virtual_circuit__provider_account__account',
|
||||||
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
to_field_name='account',
|
||||||
|
label=_('Provider account (account)'),
|
||||||
|
)
|
||||||
|
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
field_name='virtual_circuit__provider_network',
|
||||||
|
label=_('Provider network (ID)'),
|
||||||
|
)
|
||||||
|
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
field_name='interface',
|
||||||
|
label=_('Interface (ID)'),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualCircuitTermination
|
||||||
|
fields = ('id', 'interface_id', 'description')
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(virtual_circuit__cid__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
).distinct()
|
||||||
|
@ -3,7 +3,9 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices
|
from circuits.choices import (
|
||||||
|
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices,
|
||||||
|
)
|
||||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
@ -28,6 +30,8 @@ __all__ = (
|
|||||||
'ProviderBulkEditForm',
|
'ProviderBulkEditForm',
|
||||||
'ProviderAccountBulkEditForm',
|
'ProviderAccountBulkEditForm',
|
||||||
'ProviderNetworkBulkEditForm',
|
'ProviderNetworkBulkEditForm',
|
||||||
|
'VirtualCircuitBulkEditForm',
|
||||||
|
'VirtualCircuitTerminationBulkEditForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -291,3 +295,62 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
FieldSet('circuit', 'priority'),
|
FieldSet('circuit', 'priority'),
|
||||||
)
|
)
|
||||||
nullable_fields = ('priority',)
|
nullable_fields = ('priority',)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
|
provider_network = DynamicModelChoiceField(
|
||||||
|
label=_('Provider network'),
|
||||||
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
provider_account = DynamicModelChoiceField(
|
||||||
|
label=_('Provider account'),
|
||||||
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
status = forms.ChoiceField(
|
||||||
|
label=_('Status'),
|
||||||
|
choices=add_blank_choice(CircuitStatusChoices),
|
||||||
|
required=False,
|
||||||
|
initial=''
|
||||||
|
)
|
||||||
|
tenant = DynamicModelChoiceField(
|
||||||
|
label=_('Tenant'),
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
label=_('Description'),
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
comments = CommentField()
|
||||||
|
|
||||||
|
model = VirtualCircuit
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('provider_network', 'provider_account', 'status', 'description', name=_('Virtual circuit')),
|
||||||
|
FieldSet('tenant', name=_('Tenancy')),
|
||||||
|
)
|
||||||
|
nullable_fields = (
|
||||||
|
'provider_account', 'tenant', 'description', 'comments',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
|
role = forms.ChoiceField(
|
||||||
|
label=_('Role'),
|
||||||
|
choices=add_blank_choice(VirtualCircuitTerminationRoleChoices),
|
||||||
|
required=False,
|
||||||
|
initial=''
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
label=_('Description'),
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
model = VirtualCircuitTermination
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('role', 'description'),
|
||||||
|
)
|
||||||
|
nullable_fields = ('description',)
|
||||||
|
@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from circuits.choices import *
|
from circuits.choices import *
|
||||||
from circuits.constants import *
|
from circuits.constants import *
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
|
from dcim.models import Interface
|
||||||
from netbox.choices import DistanceUnitChoices
|
from netbox.choices import DistanceUnitChoices
|
||||||
from netbox.forms import NetBoxModelImportForm
|
from netbox.forms import NetBoxModelImportForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -20,6 +21,9 @@ __all__ = (
|
|||||||
'ProviderImportForm',
|
'ProviderImportForm',
|
||||||
'ProviderAccountImportForm',
|
'ProviderAccountImportForm',
|
||||||
'ProviderNetworkImportForm',
|
'ProviderNetworkImportForm',
|
||||||
|
'VirtualCircuitImportForm',
|
||||||
|
'VirtualCircuitTerminationImportForm',
|
||||||
|
'VirtualCircuitTerminationImportRelatedForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -179,3 +183,73 @@ class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitGroupAssignment
|
model = CircuitGroupAssignment
|
||||||
fields = ('circuit', 'group', 'priority')
|
fields = ('circuit', 'group', 'priority')
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitImportForm(NetBoxModelImportForm):
|
||||||
|
provider_network = CSVModelChoiceField(
|
||||||
|
label=_('Provider network'),
|
||||||
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text=_('The network to which this virtual circuit belongs')
|
||||||
|
)
|
||||||
|
provider_account = CSVModelChoiceField(
|
||||||
|
label=_('Provider account'),
|
||||||
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
to_field_name='account',
|
||||||
|
help_text=_('Assigned provider account (if any)'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
status = CSVChoiceField(
|
||||||
|
label=_('Status'),
|
||||||
|
choices=CircuitStatusChoices,
|
||||||
|
help_text=_('Operational status')
|
||||||
|
)
|
||||||
|
tenant = CSVModelChoiceField(
|
||||||
|
label=_('Tenant'),
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text=_('Assigned tenant')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualCircuit
|
||||||
|
fields = [
|
||||||
|
'cid', 'provider_network', 'provider_account', 'status', 'tenant', 'description', 'comments', 'tags',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BaseVirtualCircuitTerminationImportForm(forms.ModelForm):
|
||||||
|
virtual_circuit = CSVModelChoiceField(
|
||||||
|
label=_('Virtual circuit'),
|
||||||
|
queryset=VirtualCircuit.objects.all(),
|
||||||
|
to_field_name='cid',
|
||||||
|
)
|
||||||
|
role = CSVChoiceField(
|
||||||
|
label=_('Role'),
|
||||||
|
choices=VirtualCircuitTerminationRoleChoices,
|
||||||
|
help_text=_('Operational role')
|
||||||
|
)
|
||||||
|
interface = CSVModelChoiceField(
|
||||||
|
label=_('Interface'),
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
to_field_name='pk',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationImportRelatedForm(BaseVirtualCircuitTerminationImportForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualCircuitTermination
|
||||||
|
fields = [
|
||||||
|
'virtual_circuit', 'role', 'interface', 'description',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationImportForm(NetBoxModelImportForm, BaseVirtualCircuitTerminationImportForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualCircuitTermination
|
||||||
|
fields = [
|
||||||
|
'virtual_circuit', 'role', 'interface', 'description', 'tags',
|
||||||
|
]
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices
|
from circuits.choices import (
|
||||||
|
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices,
|
||||||
|
VirtualCircuitTerminationRoleChoices,
|
||||||
|
)
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.models import Location, Region, Site, SiteGroup
|
from dcim.models import Location, Region, Site, SiteGroup
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
@ -22,6 +25,8 @@ __all__ = (
|
|||||||
'ProviderFilterForm',
|
'ProviderFilterForm',
|
||||||
'ProviderAccountFilterForm',
|
'ProviderAccountFilterForm',
|
||||||
'ProviderNetworkFilterForm',
|
'ProviderNetworkFilterForm',
|
||||||
|
'VirtualCircuitFilterForm',
|
||||||
|
'VirtualCircuitTerminationFilterForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -292,3 +297,74 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
|
model = VirtualCircuit
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||||
|
FieldSet('status', name=_('Attributes')),
|
||||||
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
|
||||||
|
provider_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Provider.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Provider')
|
||||||
|
)
|
||||||
|
provider_account_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'provider_id': '$provider_id'
|
||||||
|
},
|
||||||
|
label=_('Provider account')
|
||||||
|
)
|
||||||
|
provider_network_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'provider_id': '$provider_id'
|
||||||
|
},
|
||||||
|
label=_('Provider network')
|
||||||
|
)
|
||||||
|
status = forms.MultipleChoiceField(
|
||||||
|
label=_('Status'),
|
||||||
|
choices=CircuitStatusChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||||
|
model = VirtualCircuitTermination
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('virtual_circuit_id', 'role', name=_('Virtual circuit')),
|
||||||
|
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||||
|
)
|
||||||
|
virtual_circuit_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=VirtualCircuit.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Virtual circuit')
|
||||||
|
)
|
||||||
|
role = forms.MultipleChoiceField(
|
||||||
|
label=_('Role'),
|
||||||
|
choices=VirtualCircuitTerminationRoleChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
provider_network_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'provider_id': '$provider_id'
|
||||||
|
},
|
||||||
|
label=_('Provider network')
|
||||||
|
)
|
||||||
|
provider_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Provider.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Provider')
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
|
from circuits.choices import (
|
||||||
|
CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices, VirtualCircuitTerminationRoleChoices,
|
||||||
|
)
|
||||||
from circuits.constants import *
|
from circuits.constants import *
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.models import Site
|
from dcim.models import Interface, Site
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from utilities.forms import get_field_value
|
from utilities.forms import get_field_value
|
||||||
from utilities.forms.fields import CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
from utilities.forms.fields import (
|
||||||
|
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
|
||||||
|
)
|
||||||
from utilities.forms.rendering import FieldSet, InlineFields
|
from utilities.forms.rendering import FieldSet, InlineFields
|
||||||
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
||||||
from utilities.templatetags.builtins.filters import bettertitle
|
from utilities.templatetags.builtins.filters import bettertitle
|
||||||
@ -24,6 +29,8 @@ __all__ = (
|
|||||||
'ProviderForm',
|
'ProviderForm',
|
||||||
'ProviderAccountForm',
|
'ProviderAccountForm',
|
||||||
'ProviderNetworkForm',
|
'ProviderNetworkForm',
|
||||||
|
'VirtualCircuitForm',
|
||||||
|
'VirtualCircuitTerminationForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -50,7 +57,9 @@ class ProviderForm(NetBoxModelForm):
|
|||||||
class ProviderAccountForm(NetBoxModelForm):
|
class ProviderAccountForm(NetBoxModelForm):
|
||||||
provider = DynamicModelChoiceField(
|
provider = DynamicModelChoiceField(
|
||||||
label=_('Provider'),
|
label=_('Provider'),
|
||||||
queryset=Provider.objects.all()
|
queryset=Provider.objects.all(),
|
||||||
|
selector=True,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
@ -64,7 +73,9 @@ class ProviderAccountForm(NetBoxModelForm):
|
|||||||
class ProviderNetworkForm(NetBoxModelForm):
|
class ProviderNetworkForm(NetBoxModelForm):
|
||||||
provider = DynamicModelChoiceField(
|
provider = DynamicModelChoiceField(
|
||||||
label=_('Provider'),
|
label=_('Provider'),
|
||||||
queryset=Provider.objects.all()
|
queryset=Provider.objects.all(),
|
||||||
|
selector=True,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
@ -97,7 +108,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
|||||||
provider = DynamicModelChoiceField(
|
provider = DynamicModelChoiceField(
|
||||||
label=_('Provider'),
|
label=_('Provider'),
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
selector=True
|
selector=True,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
provider_account = DynamicModelChoiceField(
|
provider_account = DynamicModelChoiceField(
|
||||||
label=_('Provider account'),
|
label=_('Provider account'),
|
||||||
@ -108,7 +120,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
type = DynamicModelChoiceField(
|
type = DynamicModelChoiceField(
|
||||||
queryset=CircuitType.objects.all()
|
queryset=CircuitType.objects.all(),
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
@ -249,3 +262,66 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'group', 'circuit', 'priority', 'tags',
|
'group', 'circuit', 'priority', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
|
||||||
|
provider_network = DynamicModelChoiceField(
|
||||||
|
label=_('Provider network'),
|
||||||
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
selector=True
|
||||||
|
)
|
||||||
|
provider_account = DynamicModelChoiceField(
|
||||||
|
label=_('Provider account'),
|
||||||
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
comments = CommentField()
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet(
|
||||||
|
'provider_network', 'provider_account', 'cid', 'status', 'description', 'tags', name=_('Virtual circuit'),
|
||||||
|
),
|
||||||
|
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualCircuit
|
||||||
|
fields = [
|
||||||
|
'cid', 'provider_network', 'provider_account', 'status', 'description', 'tenant_group', 'tenant',
|
||||||
|
'comments', 'tags',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationForm(NetBoxModelForm):
|
||||||
|
virtual_circuit = DynamicModelChoiceField(
|
||||||
|
label=_('Virtual circuit'),
|
||||||
|
queryset=VirtualCircuit.objects.all(),
|
||||||
|
selector=True
|
||||||
|
)
|
||||||
|
role = forms.ChoiceField(
|
||||||
|
choices=VirtualCircuitTerminationRoleChoices,
|
||||||
|
widget=HTMXSelect(),
|
||||||
|
label=_('Role')
|
||||||
|
)
|
||||||
|
interface = DynamicModelChoiceField(
|
||||||
|
label=_('Interface'),
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
selector=True,
|
||||||
|
query_params={
|
||||||
|
'kind': 'virtual',
|
||||||
|
'virtual_circuit_termination_id': 'null',
|
||||||
|
},
|
||||||
|
context={
|
||||||
|
'parent': 'device',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('virtual_circuit', 'role', 'interface', 'description', 'tags'),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualCircuitTermination
|
||||||
|
fields = [
|
||||||
|
'virtual_circuit', 'role', 'interface', 'description', 'tags',
|
||||||
|
]
|
||||||
|
@ -4,14 +4,16 @@ from circuits import filtersets, models
|
|||||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CircuitTerminationFilter',
|
|
||||||
'CircuitFilter',
|
'CircuitFilter',
|
||||||
'CircuitGroupAssignmentFilter',
|
'CircuitGroupAssignmentFilter',
|
||||||
'CircuitGroupFilter',
|
'CircuitGroupFilter',
|
||||||
|
'CircuitTerminationFilter',
|
||||||
'CircuitTypeFilter',
|
'CircuitTypeFilter',
|
||||||
'ProviderFilter',
|
'ProviderFilter',
|
||||||
'ProviderAccountFilter',
|
'ProviderAccountFilter',
|
||||||
'ProviderNetworkFilter',
|
'ProviderNetworkFilter',
|
||||||
|
'VirtualCircuitFilter',
|
||||||
|
'VirtualCircuitTerminationFilter',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -61,3 +63,15 @@ class ProviderAccountFilter(BaseFilterMixin):
|
|||||||
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
|
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
|
||||||
class ProviderNetworkFilter(BaseFilterMixin):
|
class ProviderNetworkFilter(BaseFilterMixin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
|
||||||
|
@autotype_decorator(filtersets.VirtualCircuitFilterSet)
|
||||||
|
class VirtualCircuitFilter(BaseFilterMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
|
||||||
|
@autotype_decorator(filtersets.VirtualCircuitTerminationFilterSet)
|
||||||
|
class VirtualCircuitTerminationFilter(BaseFilterMixin):
|
||||||
|
pass
|
||||||
|
@ -31,3 +31,9 @@ class CircuitsQuery:
|
|||||||
|
|
||||||
provider_network: ProviderNetworkType = strawberry_django.field()
|
provider_network: ProviderNetworkType = strawberry_django.field()
|
||||||
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
|
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
|
||||||
|
|
||||||
|
virtual_circuit: VirtualCircuitType = strawberry_django.field()
|
||||||
|
virtual_circuit_list: List[VirtualCircuitType] = strawberry_django.field()
|
||||||
|
|
||||||
|
virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field()
|
||||||
|
virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field()
|
||||||
|
@ -19,6 +19,8 @@ __all__ = (
|
|||||||
'ProviderType',
|
'ProviderType',
|
||||||
'ProviderAccountType',
|
'ProviderAccountType',
|
||||||
'ProviderNetworkType',
|
'ProviderNetworkType',
|
||||||
|
'VirtualCircuitTerminationType',
|
||||||
|
'VirtualCircuitType',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -120,3 +122,32 @@ class CircuitGroupType(OrganizationalObjectType):
|
|||||||
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||||
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
||||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(
|
||||||
|
models.VirtualCircuitTermination,
|
||||||
|
fields='__all__',
|
||||||
|
filters=VirtualCircuitTerminationFilter
|
||||||
|
)
|
||||||
|
class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
|
||||||
|
virtual_circuit: Annotated[
|
||||||
|
"VirtualCircuitType",
|
||||||
|
strawberry.lazy('circuits.graphql.types')
|
||||||
|
] = strawberry_django.field(select_related=["virtual_circuit"])
|
||||||
|
interface: Annotated[
|
||||||
|
"InterfaceType",
|
||||||
|
strawberry.lazy('dcim.graphql.types')
|
||||||
|
] = strawberry_django.field(select_related=["interface"])
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(
|
||||||
|
models.VirtualCircuit,
|
||||||
|
fields='__all__',
|
||||||
|
filters=VirtualCircuitFilter
|
||||||
|
)
|
||||||
|
class VirtualCircuitType(NetBoxObjectType):
|
||||||
|
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
|
||||||
|
provider_account: ProviderAccountType | None
|
||||||
|
tenant: TenantType | None
|
||||||
|
|
||||||
|
terminations: List[VirtualCircuitTerminationType]
|
||||||
|
67
netbox/circuits/migrations/0050_virtual_circuits.py
Normal file
67
netbox/circuits/migrations/0050_virtual_circuits.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
import taggit.managers
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import utilities.json
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0049_natural_ordering'),
|
||||||
|
('dcim', '0196_qinq_svlan'),
|
||||||
|
('extras', '0122_charfield_null_choices'),
|
||||||
|
('tenancy', '0016_charfield_null_choices'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VirtualCircuit',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('comments', models.TextField(blank=True)),
|
||||||
|
('cid', models.CharField(max_length=100)),
|
||||||
|
('status', models.CharField(default='active', max_length=50)),
|
||||||
|
('provider_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_circuits', to='circuits.provideraccount')),
|
||||||
|
('provider_network', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='virtual_circuits', to='circuits.providernetwork')),
|
||||||
|
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_circuits', to='tenancy.tenant')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'circuit',
|
||||||
|
'verbose_name_plural': 'circuits',
|
||||||
|
'ordering': ['provider_network', 'provider_account', 'cid'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VirtualCircuitTermination',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||||
|
('role', models.CharField(default='peer', max_length=50)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('interface', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='virtual_circuit_termination', to='dcim.interface')),
|
||||||
|
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
('virtual_circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.virtualcircuit')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'virtual circuit termination',
|
||||||
|
'verbose_name_plural': 'virtual circuit terminations',
|
||||||
|
'ordering': ['virtual_circuit', 'role', 'pk'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='virtualcircuit',
|
||||||
|
constraint=models.UniqueConstraint(fields=('provider_network', 'cid'), name='circuits_virtualcircuit_unique_provider_network_cid'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='virtualcircuit',
|
||||||
|
constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_virtualcircuit_unique_provideraccount_cid'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,2 +1,3 @@
|
|||||||
from .circuits import *
|
from .circuits import *
|
||||||
from .providers import *
|
from .providers import *
|
||||||
|
from .virtual_circuits import *
|
||||||
|
164
netbox/circuits/models/virtual_circuits.py
Normal file
164
netbox/circuits/models/virtual_circuits.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from circuits.choices import *
|
||||||
|
from netbox.models import ChangeLoggedModel, PrimaryModel
|
||||||
|
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'VirtualCircuit',
|
||||||
|
'VirtualCircuitTermination',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuit(PrimaryModel):
|
||||||
|
"""
|
||||||
|
A virtual connection between two or more endpoints, delivered across one or more physical circuits.
|
||||||
|
"""
|
||||||
|
cid = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
verbose_name=_('circuit ID'),
|
||||||
|
help_text=_('Unique circuit ID')
|
||||||
|
)
|
||||||
|
provider_network = models.ForeignKey(
|
||||||
|
to='circuits.ProviderNetwork',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='virtual_circuits'
|
||||||
|
)
|
||||||
|
provider_account = models.ForeignKey(
|
||||||
|
to='circuits.ProviderAccount',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='virtual_circuits',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
verbose_name=_('status'),
|
||||||
|
max_length=50,
|
||||||
|
choices=CircuitStatusChoices,
|
||||||
|
default=CircuitStatusChoices.STATUS_ACTIVE
|
||||||
|
)
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
to='tenancy.Tenant',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='virtual_circuits',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
clone_fields = (
|
||||||
|
'provider_network', 'provider_account', 'status', 'tenant', 'description',
|
||||||
|
)
|
||||||
|
prerequisite_models = (
|
||||||
|
'circuits.ProviderNetwork',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['provider_network', 'provider_account', 'cid']
|
||||||
|
constraints = (
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('provider_network', 'cid'),
|
||||||
|
name='%(app_label)s_%(class)s_unique_provider_network_cid'
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('provider_account', 'cid'),
|
||||||
|
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
verbose_name = _('virtual circuit')
|
||||||
|
verbose_name_plural = _('virtual circuits')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.cid
|
||||||
|
|
||||||
|
def get_status_color(self):
|
||||||
|
return CircuitStatusChoices.colors.get(self.status)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
if self.provider_account and self.provider_network.provider != self.provider_account.provider:
|
||||||
|
raise ValidationError({
|
||||||
|
'provider_account': "The assigned account must belong to the provider of the assigned network."
|
||||||
|
})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def provider(self):
|
||||||
|
return self.provider_network.provider
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTermination(
|
||||||
|
CustomFieldsMixin,
|
||||||
|
CustomLinksMixin,
|
||||||
|
TagsMixin,
|
||||||
|
ChangeLoggedModel
|
||||||
|
):
|
||||||
|
virtual_circuit = models.ForeignKey(
|
||||||
|
to='circuits.VirtualCircuit',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='terminations'
|
||||||
|
)
|
||||||
|
role = models.CharField(
|
||||||
|
verbose_name=_('role'),
|
||||||
|
max_length=50,
|
||||||
|
choices=VirtualCircuitTerminationRoleChoices,
|
||||||
|
default=VirtualCircuitTerminationRoleChoices.ROLE_PEER
|
||||||
|
)
|
||||||
|
interface = models.OneToOneField(
|
||||||
|
to='dcim.Interface',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='virtual_circuit_termination'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
verbose_name=_('description'),
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['virtual_circuit', 'role', 'pk']
|
||||||
|
verbose_name = _('virtual circuit termination')
|
||||||
|
verbose_name_plural = _('virtual circuit terminations')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.virtual_circuit}: {self.get_role_display()} termination'
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('circuits:virtualcircuittermination', args=[self.pk])
|
||||||
|
|
||||||
|
def get_role_color(self):
|
||||||
|
return VirtualCircuitTerminationRoleChoices.colors.get(self.role)
|
||||||
|
|
||||||
|
def to_objectchange(self, action):
|
||||||
|
objectchange = super().to_objectchange(action)
|
||||||
|
objectchange.related_object = self.virtual_circuit
|
||||||
|
return objectchange
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent_object(self):
|
||||||
|
return self.virtual_circuit
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def peer_terminations(self):
|
||||||
|
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_PEER:
|
||||||
|
return self.virtual_circuit.terminations.exclude(pk=self.pk).filter(
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER
|
||||||
|
)
|
||||||
|
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_HUB:
|
||||||
|
return self.virtual_circuit.terminations.filter(
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE
|
||||||
|
)
|
||||||
|
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_SPOKE:
|
||||||
|
return self.virtual_circuit.terminations.filter(
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
if self.interface and not self.interface.is_virtual:
|
||||||
|
raise ValidationError("Virtual circuits may be terminated only to virtual interfaces.")
|
@ -80,3 +80,23 @@ class ProviderNetworkIndex(SearchIndex):
|
|||||||
('comments', 5000),
|
('comments', 5000),
|
||||||
)
|
)
|
||||||
display_attrs = ('provider', 'service_id', 'description')
|
display_attrs = ('provider', 'service_id', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_search
|
||||||
|
class VirtualCircuitIndex(SearchIndex):
|
||||||
|
model = models.VirtualCircuit
|
||||||
|
fields = (
|
||||||
|
('cid', 100),
|
||||||
|
('description', 500),
|
||||||
|
('comments', 5000),
|
||||||
|
)
|
||||||
|
display_attrs = ('provider', 'provider_network', 'provider_account', 'status', 'tenant', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_search
|
||||||
|
class VirtualCircuitTerminationIndex(SearchIndex):
|
||||||
|
model = models.VirtualCircuitTermination
|
||||||
|
fields = (
|
||||||
|
('description', 500),
|
||||||
|
)
|
||||||
|
display_attrs = ('virtual_circuit', 'role', 'description')
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
from .circuits import *
|
from .circuits import *
|
||||||
from .columns import *
|
from .columns import *
|
||||||
from .providers import *
|
from .providers import *
|
||||||
|
from .virtual_circuits import *
|
||||||
|
95
netbox/circuits/tables/virtual_circuits.py
Normal file
95
netbox/circuits/tables/virtual_circuits.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import django_tables2 as tables
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from circuits.models import *
|
||||||
|
from netbox.tables import NetBoxTable, columns
|
||||||
|
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'VirtualCircuitTable',
|
||||||
|
'VirtualCircuitTerminationTable',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||||
|
cid = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Circuit ID')
|
||||||
|
)
|
||||||
|
provider = tables.Column(
|
||||||
|
accessor=tables.A('provider_network__provider'),
|
||||||
|
verbose_name=_('Provider'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
provider_network = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Provider network')
|
||||||
|
)
|
||||||
|
provider_account = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Account')
|
||||||
|
)
|
||||||
|
status = columns.ChoiceFieldColumn()
|
||||||
|
termination_count = columns.LinkedCountColumn(
|
||||||
|
viewname='circuits:virtualcircuittermination_list',
|
||||||
|
url_params={'virtual_circuit_id': 'pk'},
|
||||||
|
verbose_name=_('Terminations')
|
||||||
|
)
|
||||||
|
comments = columns.MarkdownColumn(
|
||||||
|
verbose_name=_('Comments')
|
||||||
|
)
|
||||||
|
tags = columns.TagColumn(
|
||||||
|
url_name='circuits:virtualcircuit_list'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = VirtualCircuit
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'status', 'tenant', 'tenant_group',
|
||||||
|
'description', 'comments', 'tags', 'created', 'last_updated',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'status', 'tenant', 'termination_count',
|
||||||
|
'description',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationTable(NetBoxTable):
|
||||||
|
virtual_circuit = tables.Column(
|
||||||
|
verbose_name=_('Virtual circuit'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
provider = tables.Column(
|
||||||
|
accessor=tables.A('virtual_circuit__provider_network__provider'),
|
||||||
|
verbose_name=_('Provider'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
provider_network = tables.Column(
|
||||||
|
accessor=tables.A('virtual_circuit__provider_network'),
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Provider network')
|
||||||
|
)
|
||||||
|
provider_account = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Account')
|
||||||
|
)
|
||||||
|
role = columns.ChoiceFieldColumn()
|
||||||
|
device = tables.Column(
|
||||||
|
accessor=tables.A('interface__device'),
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Device')
|
||||||
|
)
|
||||||
|
interface = tables.Column(
|
||||||
|
verbose_name=_('Interface'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = VirtualCircuitTermination
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'virtual_circuit', 'provider', 'provider_network', 'provider_account', 'role', 'interfaces',
|
||||||
|
'description', 'created', 'last_updated', 'actions',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'id', 'virtual_circuit', 'role', 'device', 'interface', 'description',
|
||||||
|
)
|
@ -2,7 +2,8 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from circuits.choices import *
|
from circuits.choices import *
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.models import Site
|
from dcim.choices import InterfaceTypeChoices
|
||||||
|
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
|
||||||
from ipam.models import ASN, RIR
|
from ipam.models import ASN, RIR
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
|
|
||||||
@ -397,3 +398,240 @@ class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'provider': providers[1].pk,
|
'provider': providers[1].pk,
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
model = VirtualCircuit
|
||||||
|
brief_fields = ['cid', 'description', 'display', 'id', 'provider_network', 'url']
|
||||||
|
bulk_update_data = {
|
||||||
|
'status': 'planned',
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||||
|
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||||
|
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||||
|
|
||||||
|
virtual_circuits = (
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_network,
|
||||||
|
provider_account=provider_account,
|
||||||
|
cid='Virtual Circuit 1'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_network,
|
||||||
|
provider_account=provider_account,
|
||||||
|
cid='Virtual Circuit 2'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_network,
|
||||||
|
provider_account=provider_account,
|
||||||
|
cid='Virtual Circuit 3'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||||
|
|
||||||
|
cls.create_data = [
|
||||||
|
{
|
||||||
|
'cid': 'Virtual Circuit 4',
|
||||||
|
'provider_network': provider_network.pk,
|
||||||
|
'provider_account': provider_account.pk,
|
||||||
|
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'cid': 'Virtual Circuit 5',
|
||||||
|
'provider_network': provider_network.pk,
|
||||||
|
'provider_account': provider_account.pk,
|
||||||
|
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'cid': 'Virtual Circuit 6',
|
||||||
|
'provider_network': provider_network.pk,
|
||||||
|
'provider_account': provider_account.pk,
|
||||||
|
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
model = VirtualCircuitTermination
|
||||||
|
brief_fields = ['description', 'display', 'id', 'interface', 'role', 'url', 'virtual_circuit']
|
||||||
|
bulk_update_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
|
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||||
|
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||||
|
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||||
|
|
||||||
|
devices = (
|
||||||
|
Device(site=site, name='hub', device_type=device_type, role=device_role),
|
||||||
|
Device(site=site, name='spoke1', device_type=device_type, role=device_role),
|
||||||
|
Device(site=site, name='spoke2', device_type=device_type, role=device_role),
|
||||||
|
Device(site=site, name='spoke3', device_type=device_type, role=device_role),
|
||||||
|
)
|
||||||
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
|
physical_interfaces = (
|
||||||
|
Interface(device=devices[0], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[1], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[2], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[3], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(physical_interfaces)
|
||||||
|
|
||||||
|
virtual_interfaces = (
|
||||||
|
# Point-to-point VCs
|
||||||
|
Interface(
|
||||||
|
device=devices[0],
|
||||||
|
name='eth0.1',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[0],
|
||||||
|
name='eth0.2',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[0],
|
||||||
|
name='eth0.3',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[1],
|
||||||
|
name='eth0.1',
|
||||||
|
parent=physical_interfaces[1],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[2],
|
||||||
|
name='eth0.1',
|
||||||
|
parent=physical_interfaces[2],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[3],
|
||||||
|
name='eth0.1',
|
||||||
|
parent=physical_interfaces[3],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
|
||||||
|
# Hub and spoke VCs
|
||||||
|
Interface(
|
||||||
|
device=devices[0],
|
||||||
|
name='eth0.9',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[1],
|
||||||
|
name='eth0.9',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[2],
|
||||||
|
name='eth0.9',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[3],
|
||||||
|
name='eth0.9',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(virtual_interfaces)
|
||||||
|
|
||||||
|
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||||
|
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||||
|
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||||
|
|
||||||
|
virtual_circuits = (
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_network,
|
||||||
|
provider_account=provider_account,
|
||||||
|
cid='Virtual Circuit 1'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_network,
|
||||||
|
provider_account=provider_account,
|
||||||
|
cid='Virtual Circuit 2'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_network,
|
||||||
|
provider_account=provider_account,
|
||||||
|
cid='Virtual Circuit 3'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_network,
|
||||||
|
provider_account=provider_account,
|
||||||
|
cid='Virtual Circuit 4'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||||
|
|
||||||
|
virtual_circuit_terminations = (
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[0],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[0]
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[0],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[3]
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[1],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[1]
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[1],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[4]
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[2],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[2]
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[2],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[5]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
|
||||||
|
|
||||||
|
cls.create_data = [
|
||||||
|
{
|
||||||
|
'virtual_circuit': virtual_circuits[3].pk,
|
||||||
|
'role': VirtualCircuitTerminationRoleChoices.ROLE_HUB,
|
||||||
|
'interface': virtual_interfaces[6].pk
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'virtual_circuit': virtual_circuits[3].pk,
|
||||||
|
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||||
|
'interface': virtual_interfaces[7].pk
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'virtual_circuit': virtual_circuits[3].pk,
|
||||||
|
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||||
|
'interface': virtual_interfaces[8].pk
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'virtual_circuit': virtual_circuits[3].pk,
|
||||||
|
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||||
|
'interface': virtual_interfaces[9].pk
|
||||||
|
},
|
||||||
|
]
|
||||||
|
@ -3,7 +3,8 @@ from django.test import TestCase
|
|||||||
from circuits.choices import *
|
from circuits.choices import *
|
||||||
from circuits.filtersets import *
|
from circuits.filtersets import *
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.models import Cable, Region, Site, SiteGroup
|
from dcim.choices import InterfaceTypeChoices
|
||||||
|
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Region, Site, SiteGroup
|
||||||
from ipam.models import ASN, RIR
|
from ipam.models import ASN, RIR
|
||||||
from netbox.choices import DistanceUnitChoices
|
from netbox.choices import DistanceUnitChoices
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
@ -678,3 +679,293 @@ class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
|
queryset = VirtualCircuit.objects.all()
|
||||||
|
filterset = VirtualCircuitFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
tenant_groups = (
|
||||||
|
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||||
|
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||||
|
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
|
||||||
|
)
|
||||||
|
for tenantgroup in tenant_groups:
|
||||||
|
tenantgroup.save()
|
||||||
|
|
||||||
|
tenants = (
|
||||||
|
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
|
||||||
|
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
|
||||||
|
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
|
||||||
|
)
|
||||||
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
|
providers = (
|
||||||
|
Provider(name='Provider 1', slug='provider-1'),
|
||||||
|
Provider(name='Provider 2', slug='provider-2'),
|
||||||
|
Provider(name='Provider 3', slug='provider-3'),
|
||||||
|
)
|
||||||
|
Provider.objects.bulk_create(providers)
|
||||||
|
|
||||||
|
provider_accounts = (
|
||||||
|
ProviderAccount(name='Provider Account 1', provider=providers[0], account='A'),
|
||||||
|
ProviderAccount(name='Provider Account 2', provider=providers[1], account='B'),
|
||||||
|
ProviderAccount(name='Provider Account 3', provider=providers[2], account='C'),
|
||||||
|
)
|
||||||
|
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||||
|
|
||||||
|
provider_networks = (
|
||||||
|
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
|
||||||
|
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||||
|
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
|
||||||
|
)
|
||||||
|
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||||
|
|
||||||
|
virutal_circuits = (
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[0],
|
||||||
|
provider_account=provider_accounts[0],
|
||||||
|
tenant=tenants[0],
|
||||||
|
cid='Virtual Circuit 1',
|
||||||
|
status=CircuitStatusChoices.STATUS_PLANNED,
|
||||||
|
description='virtualcircuit1',
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[1],
|
||||||
|
provider_account=provider_accounts[1],
|
||||||
|
tenant=tenants[1],
|
||||||
|
cid='Virtual Circuit 2',
|
||||||
|
status=CircuitStatusChoices.STATUS_ACTIVE,
|
||||||
|
description='virtualcircuit2',
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[2],
|
||||||
|
provider_account=provider_accounts[2],
|
||||||
|
tenant=tenants[2],
|
||||||
|
cid='Virtual Circuit 3',
|
||||||
|
status=CircuitStatusChoices.STATUS_DEPROVISIONING,
|
||||||
|
description='virtualcircuit3',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VirtualCircuit.objects.bulk_create(virutal_circuits)
|
||||||
|
|
||||||
|
def test_q(self):
|
||||||
|
params = {'q': 'virtualcircuit1'}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_cid(self):
|
||||||
|
params = {'cid': ['Virtual Circuit 1', 'Virtual Circuit 2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_provider(self):
|
||||||
|
providers = Provider.objects.all()[:2]
|
||||||
|
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_provider_account(self):
|
||||||
|
provider_accounts = ProviderAccount.objects.all()[:2]
|
||||||
|
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_provider_network(self):
|
||||||
|
provider_networks = ProviderNetwork.objects.all()[:2]
|
||||||
|
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_status(self):
|
||||||
|
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['virtualcircuit1', 'virtualcircuit2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_tenant(self):
|
||||||
|
tenants = Tenant.objects.all()[:2]
|
||||||
|
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_tenant_group(self):
|
||||||
|
tenant_groups = TenantGroup.objects.all()[:2]
|
||||||
|
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
|
queryset = VirtualCircuitTermination.objects.all()
|
||||||
|
filterset = VirtualCircuitTerminationFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
|
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||||
|
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||||
|
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||||
|
|
||||||
|
devices = (
|
||||||
|
Device(site=site, name='Device 1', device_type=device_type, role=device_role),
|
||||||
|
Device(site=site, name='Device 2', device_type=device_type, role=device_role),
|
||||||
|
Device(site=site, name='Device 3', device_type=device_type, role=device_role),
|
||||||
|
)
|
||||||
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
|
virtual_interfaces = (
|
||||||
|
# Device 1
|
||||||
|
Interface(
|
||||||
|
device=devices[0],
|
||||||
|
name='eth0.1',
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[0],
|
||||||
|
name='eth0.2',
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
# Device 2
|
||||||
|
Interface(
|
||||||
|
device=devices[1],
|
||||||
|
name='eth0.1',
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[1],
|
||||||
|
name='eth0.2',
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
# Device 3
|
||||||
|
Interface(
|
||||||
|
device=devices[2],
|
||||||
|
name='eth0.1',
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[2],
|
||||||
|
name='eth0.2',
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(virtual_interfaces)
|
||||||
|
|
||||||
|
providers = (
|
||||||
|
Provider(name='Provider 1', slug='provider-1'),
|
||||||
|
Provider(name='Provider 2', slug='provider-2'),
|
||||||
|
Provider(name='Provider 3', slug='provider-3'),
|
||||||
|
)
|
||||||
|
Provider.objects.bulk_create(providers)
|
||||||
|
provider_networks = (
|
||||||
|
ProviderNetwork(provider=providers[0], name='Provider Network 1'),
|
||||||
|
ProviderNetwork(provider=providers[1], name='Provider Network 2'),
|
||||||
|
ProviderNetwork(provider=providers[2], name='Provider Network 3'),
|
||||||
|
)
|
||||||
|
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||||
|
provider_accounts = (
|
||||||
|
ProviderAccount(provider=providers[0], account='Provider Account 1'),
|
||||||
|
ProviderAccount(provider=providers[1], account='Provider Account 2'),
|
||||||
|
ProviderAccount(provider=providers[2], account='Provider Account 3'),
|
||||||
|
)
|
||||||
|
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||||
|
|
||||||
|
virtual_circuits = (
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[0],
|
||||||
|
provider_account=provider_accounts[0],
|
||||||
|
cid='Virtual Circuit 1'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[1],
|
||||||
|
provider_account=provider_accounts[1],
|
||||||
|
cid='Virtual Circuit 2'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[2],
|
||||||
|
provider_account=provider_accounts[2],
|
||||||
|
cid='Virtual Circuit 3'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||||
|
|
||||||
|
virtual_circuit_terminations = (
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[0],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB,
|
||||||
|
interface=virtual_interfaces[0],
|
||||||
|
description='termination1'
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[0],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||||
|
interface=virtual_interfaces[3],
|
||||||
|
description='termination2'
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[1],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[1],
|
||||||
|
description='termination3'
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[1],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[4],
|
||||||
|
description='termination4'
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[2],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[2],
|
||||||
|
description='termination5'
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[2],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[5],
|
||||||
|
description='termination6'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
|
||||||
|
|
||||||
|
def test_q(self):
|
||||||
|
params = {'q': 'termination1'}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
params = {'description': ['termination1', 'termination2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_virtual_circuit_id(self):
|
||||||
|
virtual_circuits = VirtualCircuit.objects.filter()[:2]
|
||||||
|
params = {'virtual_circuit_id': [virtual_circuits[0].pk, virtual_circuits[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_provider(self):
|
||||||
|
providers = Provider.objects.all()[:2]
|
||||||
|
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_provider_network(self):
|
||||||
|
provider_networks = ProviderNetwork.objects.all()[:2]
|
||||||
|
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_provider_account(self):
|
||||||
|
provider_accounts = ProviderAccount.objects.all()[:2]
|
||||||
|
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
params = {'provider_account': [provider_accounts[0].account, provider_accounts[1].account]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_interface(self):
|
||||||
|
interfaces = Interface.objects.all()[:2]
|
||||||
|
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -7,7 +7,8 @@ from django.urls import reverse
|
|||||||
from circuits.choices import *
|
from circuits.choices import *
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from dcim.models import Cable, Interface, Site
|
from dcim.choices import InterfaceTypeChoices
|
||||||
|
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
|
||||||
from ipam.models import ASN, RIR
|
from ipam.models import ASN, RIR
|
||||||
from netbox.choices import ImportFormatChoices
|
from netbox.choices import ImportFormatChoices
|
||||||
from users.models import ObjectPermission
|
from users.models import ObjectPermission
|
||||||
@ -341,7 +342,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -518,3 +519,319 @@ class CircuitGroupAssignmentTestCase(
|
|||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
model = VirtualCircuit
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.add_permissions(
|
||||||
|
'circuits.add_virtualcircuittermination',
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||||
|
provider_networks = (
|
||||||
|
ProviderNetwork(provider=provider, name='Provider Network 1'),
|
||||||
|
ProviderNetwork(provider=provider, name='Provider Network 2'),
|
||||||
|
)
|
||||||
|
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||||
|
provider_accounts = (
|
||||||
|
ProviderAccount(provider=provider, account='Provider Account 1'),
|
||||||
|
ProviderAccount(provider=provider, account='Provider Account 2'),
|
||||||
|
)
|
||||||
|
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||||
|
|
||||||
|
virtual_circuits = (
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[0],
|
||||||
|
provider_account=provider_accounts[0],
|
||||||
|
cid='Virtual Circuit 1'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[0],
|
||||||
|
provider_account=provider_accounts[0],
|
||||||
|
cid='Virtual Circuit 2'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[0],
|
||||||
|
provider_account=provider_accounts[0],
|
||||||
|
cid='Virtual Circuit 3'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||||
|
|
||||||
|
device = create_test_device('Device 1')
|
||||||
|
interfaces = (
|
||||||
|
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||||
|
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||||
|
Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'cid': 'Virtual Circuit X',
|
||||||
|
'provider_network': provider_networks[1].pk,
|
||||||
|
'provider_account': provider_accounts[1].pk,
|
||||||
|
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||||
|
'description': 'A new virtual circuit',
|
||||||
|
'comments': 'Some comments',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"cid,provider_network,provider_account,status",
|
||||||
|
f"Virtual Circuit 4,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}",
|
||||||
|
f"Virtual Circuit 5,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}",
|
||||||
|
f"Virtual Circuit 6,Provider Network 1,Provider Account 1,{CircuitStatusChoices.STATUS_PLANNED}",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.csv_update_data = (
|
||||||
|
"id,cid,description,status",
|
||||||
|
f"{virtual_circuits[0].pk},Virtual Circuit A,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||||
|
f"{virtual_circuits[1].pk},Virtual Circuit B,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||||
|
f"{virtual_circuits[2].pk},Virtual Circuit C,New description,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'provider_network': provider_networks[1].pk,
|
||||||
|
'provider_account': provider_accounts[1].pk,
|
||||||
|
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
|
||||||
|
'description': 'New description',
|
||||||
|
'comments': 'New comments',
|
||||||
|
}
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||||
|
def test_bulk_import_objects_with_terminations(self):
|
||||||
|
interfaces = Interface.objects.filter(type=InterfaceTypeChoices.TYPE_VIRTUAL)
|
||||||
|
json_data = f"""
|
||||||
|
[
|
||||||
|
{{
|
||||||
|
"cid": "Virtual Circuit 7",
|
||||||
|
"provider_network": "Provider Network 1",
|
||||||
|
"status": "active",
|
||||||
|
"terminations": [
|
||||||
|
{{
|
||||||
|
"role": "hub",
|
||||||
|
"interface": {interfaces[0].pk}
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
"role": "spoke",
|
||||||
|
"interface": {interfaces[1].pk}
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
"role": "spoke",
|
||||||
|
"interface": {interfaces[2].pk}
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
|
initial_count = self._get_queryset().count()
|
||||||
|
data = {
|
||||||
|
'data': json_data,
|
||||||
|
'format': ImportFormatChoices.JSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Assign model-level permission
|
||||||
|
obj_perm = ObjectPermission(
|
||||||
|
name='Test permission',
|
||||||
|
actions=['add']
|
||||||
|
)
|
||||||
|
obj_perm.save()
|
||||||
|
obj_perm.users.add(self.user)
|
||||||
|
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
|
||||||
|
|
||||||
|
# Try GET with model-level permission
|
||||||
|
self.assertHttpStatus(self.client.get(self._get_url('import')), 200)
|
||||||
|
|
||||||
|
# Test POST with permission
|
||||||
|
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
|
||||||
|
self.assertEqual(self._get_queryset().count(), initial_count + 1)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
model = VirtualCircuitTermination
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
|
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||||
|
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||||
|
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||||
|
|
||||||
|
devices = (
|
||||||
|
Device(site=site, name='hub', device_type=device_type, role=device_role),
|
||||||
|
Device(site=site, name='spoke1', device_type=device_type, role=device_role),
|
||||||
|
Device(site=site, name='spoke2', device_type=device_type, role=device_role),
|
||||||
|
Device(site=site, name='spoke3', device_type=device_type, role=device_role),
|
||||||
|
)
|
||||||
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
|
physical_interfaces = (
|
||||||
|
Interface(device=devices[0], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[1], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[2], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[3], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(physical_interfaces)
|
||||||
|
|
||||||
|
virtual_interfaces = (
|
||||||
|
# Point-to-point VCs
|
||||||
|
Interface(
|
||||||
|
device=devices[0],
|
||||||
|
name='eth0.1',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[0],
|
||||||
|
name='eth0.2',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[0],
|
||||||
|
name='eth0.3',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[1],
|
||||||
|
name='eth0.1',
|
||||||
|
parent=physical_interfaces[1],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[2],
|
||||||
|
name='eth0.1',
|
||||||
|
parent=physical_interfaces[2],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[3],
|
||||||
|
name='eth0.1',
|
||||||
|
parent=physical_interfaces[3],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
|
||||||
|
# Hub and spoke VCs
|
||||||
|
Interface(
|
||||||
|
device=devices[0],
|
||||||
|
name='eth0.9',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[1],
|
||||||
|
name='eth0.9',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[2],
|
||||||
|
name='eth0.9',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
Interface(
|
||||||
|
device=devices[3],
|
||||||
|
name='eth0.9',
|
||||||
|
parent=physical_interfaces[0],
|
||||||
|
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(virtual_interfaces)
|
||||||
|
|
||||||
|
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||||
|
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||||
|
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||||
|
|
||||||
|
virtual_circuits = (
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_network,
|
||||||
|
provider_account=provider_account,
|
||||||
|
cid='Virtual Circuit 1'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_network,
|
||||||
|
provider_account=provider_account,
|
||||||
|
cid='Virtual Circuit 2'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_network,
|
||||||
|
provider_account=provider_account,
|
||||||
|
cid='Virtual Circuit 3'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_network,
|
||||||
|
provider_account=provider_account,
|
||||||
|
cid='Virtual Circuit 4'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||||
|
|
||||||
|
virtual_circuit_terminations = (
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[0],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[0]
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[0],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[3]
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[1],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[1]
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[1],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[4]
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[2],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[2]
|
||||||
|
),
|
||||||
|
VirtualCircuitTermination(
|
||||||
|
virtual_circuit=virtual_circuits[2],
|
||||||
|
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||||
|
interface=virtual_interfaces[5]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'virtual_circuit': virtual_circuits[3].pk,
|
||||||
|
'role': VirtualCircuitTerminationRoleChoices.ROLE_HUB,
|
||||||
|
'interface': virtual_interfaces[6].pk
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"virtual_circuit,role,interface,description",
|
||||||
|
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_HUB},{virtual_interfaces[6].pk},Hub",
|
||||||
|
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[7].pk},Spoke 1",
|
||||||
|
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[8].pk},Spoke 2",
|
||||||
|
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[9].pk},Spoke 3",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.csv_update_data = (
|
||||||
|
"id,role,description",
|
||||||
|
f"{virtual_circuit_terminations[0].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
|
||||||
|
f"{virtual_circuit_terminations[1].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
|
||||||
|
f"{virtual_circuit_terminations[2].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'description': 'New description',
|
||||||
|
}
|
||||||
|
@ -33,4 +33,20 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('circuit-group-assignments/', include(get_model_urls('circuits', 'circuitgroupassignment', detail=False))),
|
path('circuit-group-assignments/', include(get_model_urls('circuits', 'circuitgroupassignment', detail=False))),
|
||||||
path('circuit-group-assignments/<int:pk>/', include(get_model_urls('circuits', 'circuitgroupassignment'))),
|
path('circuit-group-assignments/<int:pk>/', include(get_model_urls('circuits', 'circuitgroupassignment'))),
|
||||||
|
|
||||||
|
# Virtual circuits
|
||||||
|
path('virtual-circuits/', views.VirtualCircuitListView.as_view(), name='virtualcircuit_list'),
|
||||||
|
path('virtual-circuits/add/', views.VirtualCircuitEditView.as_view(), name='virtualcircuit_add'),
|
||||||
|
path('virtual-circuits/import/', views.VirtualCircuitBulkImportView.as_view(), name='virtualcircuit_import'),
|
||||||
|
path('virtual-circuits/edit/', views.VirtualCircuitBulkEditView.as_view(), name='virtualcircuit_bulk_edit'),
|
||||||
|
path('virtual-circuits/delete/', views.VirtualCircuitBulkDeleteView.as_view(), name='virtualcircuit_bulk_delete'),
|
||||||
|
path('virtual-circuits/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuit'))),
|
||||||
|
|
||||||
|
# Virtual circuit terminations
|
||||||
|
path('virtual-circuit-terminations/', views.VirtualCircuitTerminationListView.as_view(), name='virtualcircuittermination_list'),
|
||||||
|
path('virtual-circuit-terminations/add/', views.VirtualCircuitTerminationEditView.as_view(), name='virtualcircuittermination_add'),
|
||||||
|
path('virtual-circuit-terminations/import/', views.VirtualCircuitTerminationBulkImportView.as_view(), name='virtualcircuittermination_import'),
|
||||||
|
path('virtual-circuit-terminations/edit/', views.VirtualCircuitTerminationBulkEditView.as_view(), name='virtualcircuittermination_bulk_edit'),
|
||||||
|
path('virtual-circuit-terminations/delete/', views.VirtualCircuitTerminationBulkDeleteView.as_view(), name='virtualcircuittermination_bulk_delete'),
|
||||||
|
path('virtual-circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuittermination'))),
|
||||||
]
|
]
|
||||||
|
@ -577,3 +577,109 @@ class CircuitGroupAssignmentBulkDeleteView(generic.BulkDeleteView):
|
|||||||
queryset = CircuitGroupAssignment.objects.all()
|
queryset = CircuitGroupAssignment.objects.all()
|
||||||
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
||||||
table = tables.CircuitGroupAssignmentTable
|
table = tables.CircuitGroupAssignmentTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual circuits
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualCircuitListView(generic.ObjectListView):
|
||||||
|
queryset = VirtualCircuit.objects.annotate(
|
||||||
|
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
||||||
|
)
|
||||||
|
filterset = filtersets.VirtualCircuitFilterSet
|
||||||
|
filterset_form = forms.VirtualCircuitFilterForm
|
||||||
|
table = tables.VirtualCircuitTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(VirtualCircuit)
|
||||||
|
class VirtualCircuitView(generic.ObjectView):
|
||||||
|
queryset = VirtualCircuit.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(VirtualCircuit, 'edit')
|
||||||
|
class VirtualCircuitEditView(generic.ObjectEditView):
|
||||||
|
queryset = VirtualCircuit.objects.all()
|
||||||
|
form = forms.VirtualCircuitForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(VirtualCircuit, 'delete')
|
||||||
|
class VirtualCircuitDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = VirtualCircuit.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = VirtualCircuit.objects.all()
|
||||||
|
model_form = forms.VirtualCircuitImportForm
|
||||||
|
additional_permissions = [
|
||||||
|
'circuits.add_virtualcircuittermination',
|
||||||
|
]
|
||||||
|
related_object_forms = {
|
||||||
|
'terminations': forms.VirtualCircuitTerminationImportRelatedForm,
|
||||||
|
}
|
||||||
|
|
||||||
|
def prep_related_object_data(self, parent, data):
|
||||||
|
data.update({'virtual_circuit': parent})
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = VirtualCircuit.objects.annotate(
|
||||||
|
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
||||||
|
)
|
||||||
|
filterset = filtersets.VirtualCircuitFilterSet
|
||||||
|
table = tables.VirtualCircuitTable
|
||||||
|
form = forms.VirtualCircuitBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = VirtualCircuit.objects.annotate(
|
||||||
|
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
||||||
|
)
|
||||||
|
filterset = filtersets.VirtualCircuitFilterSet
|
||||||
|
table = tables.VirtualCircuitTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual circuit terminations
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationListView(generic.ObjectListView):
|
||||||
|
queryset = VirtualCircuitTermination.objects.all()
|
||||||
|
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
||||||
|
filterset_form = forms.VirtualCircuitTerminationFilterForm
|
||||||
|
table = tables.VirtualCircuitTerminationTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(VirtualCircuitTermination)
|
||||||
|
class VirtualCircuitTerminationView(generic.ObjectView):
|
||||||
|
queryset = VirtualCircuitTermination.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(VirtualCircuitTermination, 'edit')
|
||||||
|
class VirtualCircuitTerminationEditView(generic.ObjectEditView):
|
||||||
|
queryset = VirtualCircuitTermination.objects.all()
|
||||||
|
form = forms.VirtualCircuitTerminationForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(VirtualCircuitTermination, 'delete')
|
||||||
|
class VirtualCircuitTerminationDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = VirtualCircuitTermination.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = VirtualCircuitTermination.objects.all()
|
||||||
|
model_form = forms.VirtualCircuitTerminationImportForm
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = VirtualCircuitTermination.objects.all()
|
||||||
|
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
||||||
|
table = tables.VirtualCircuitTerminationTable
|
||||||
|
form = forms.VirtualCircuitTerminationBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = VirtualCircuitTermination.objects.all()
|
||||||
|
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
||||||
|
table = tables.VirtualCircuitTerminationTable
|
||||||
|
@ -21,7 +21,7 @@ from wireless.choices import *
|
|||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
from .base import ConnectedEndpointsSerializer
|
from .base import ConnectedEndpointsSerializer
|
||||||
from .cables import CabledObjectSerializer
|
from .cables import CabledObjectSerializer
|
||||||
from .devices import DeviceSerializer, ModuleSerializer, VirtualDeviceContextSerializer
|
from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, VirtualDeviceContextSerializer
|
||||||
from .manufacturers import ManufacturerSerializer
|
from .manufacturers import ManufacturerSerializer
|
||||||
from .nested import NestedInterfaceSerializer
|
from .nested import NestedInterfaceSerializer
|
||||||
from .roles import InventoryItemRoleSerializer
|
from .roles import InventoryItemRoleSerializer
|
||||||
@ -210,24 +210,23 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
)
|
)
|
||||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||||
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
||||||
mac_address = serializers.CharField(
|
# Maintains backward compatibility with NetBox <v4.2
|
||||||
required=False,
|
mac_address = serializers.CharField(allow_null=True, read_only=True)
|
||||||
default=None,
|
primary_mac_address = MACAddressSerializer(nested=True, required=False, allow_null=True)
|
||||||
allow_blank=True,
|
mac_addresses = MACAddressSerializer(many=True, nested=True, read_only=True, allow_null=True)
|
||||||
allow_null=True
|
|
||||||
)
|
|
||||||
wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True)
|
wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled',
|
'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled',
|
||||||
'parent', 'bridge', 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description',
|
'parent', 'bridge', 'lag', 'mtu', 'mac_address', 'primary_mac_address', 'mac_addresses', 'speed', 'duplex',
|
||||||
'mode', 'rf_role', 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width',
|
'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'poe_mode', 'poe_type',
|
||||||
'tx_power', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'mark_connected',
|
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||||
'cable', 'cable_end', 'wireless_link', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf',
|
'vlan_translation_policy', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers',
|
||||||
'l2vpn_termination', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
|
'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
|
||||||
'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.models import Device, DeviceBay, Module, VirtualDeviceContext
|
from dcim.constants import MACADDRESS_ASSIGNMENT_MODELS
|
||||||
|
from dcim.models import Device, DeviceBay, MACAddress, Module, VirtualDeviceContext
|
||||||
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
|
||||||
from ipam.api.serializers_.ip import IPAddressSerializer
|
from ipam.api.serializers_.ip import IPAddressSerializer
|
||||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
from virtualization.api.serializers_.clusters import ClusterSerializer
|
from virtualization.api.serializers_.clusters import ClusterSerializer
|
||||||
from .devicetypes import *
|
from .devicetypes import *
|
||||||
from .platforms import PlatformSerializer
|
from .platforms import PlatformSerializer
|
||||||
@ -23,6 +26,7 @@ from .virtualchassis import VirtualChassisSerializer
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'DeviceSerializer',
|
'DeviceSerializer',
|
||||||
'DeviceWithConfigContextSerializer',
|
'DeviceWithConfigContextSerializer',
|
||||||
|
'MACAddressSerializer',
|
||||||
'ModuleSerializer',
|
'ModuleSerializer',
|
||||||
'VirtualDeviceContextSerializer',
|
'VirtualDeviceContextSerializer',
|
||||||
)
|
)
|
||||||
@ -153,3 +157,28 @@ class ModuleSerializer(NetBoxModelSerializer):
|
|||||||
'asset_tag', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
'asset_tag', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')
|
brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class MACAddressSerializer(NetBoxModelSerializer):
|
||||||
|
assigned_object_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(MACADDRESS_ASSIGNMENT_MODELS),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MACAddress
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display_url', 'display', 'mac_address', 'assigned_object_type', 'assigned_object',
|
||||||
|
'description', 'comments',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'mac_address', 'description')
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_assigned_object(self, obj):
|
||||||
|
if obj.assigned_object is None:
|
||||||
|
return None
|
||||||
|
serializer = get_serializer_for_model(obj.assigned_object)
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.assigned_object, nested=True, context=context).data
|
||||||
|
@ -56,6 +56,9 @@ router.register('inventory-items', views.InventoryItemViewSet)
|
|||||||
# Device component roles
|
# Device component roles
|
||||||
router.register('inventory-item-roles', views.InventoryItemRoleViewSet)
|
router.register('inventory-item-roles', views.InventoryItemRoleViewSet)
|
||||||
|
|
||||||
|
# Addressing
|
||||||
|
router.register('mac-addresses', views.MACAddressViewSet)
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
router.register('cables', views.CableViewSet)
|
router.register('cables', views.CableViewSet)
|
||||||
router.register('cable-terminations', views.CableTerminationViewSet)
|
router.register('cable-terminations', views.CableTerminationViewSet)
|
||||||
|
@ -499,6 +499,16 @@ class InventoryItemRoleViewSet(NetBoxModelViewSet):
|
|||||||
filterset_class = filtersets.InventoryItemRoleFilterSet
|
filterset_class = filtersets.InventoryItemRoleFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Addressing
|
||||||
|
#
|
||||||
|
|
||||||
|
class MACAddressViewSet(NetBoxModelViewSet):
|
||||||
|
queryset = MACAddress.objects.all()
|
||||||
|
serializer_class = serializers.MACAddressSerializer
|
||||||
|
filterset_class = filtersets.MACAddressFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
@ -128,3 +128,13 @@ COMPATIBLE_TERMINATION_TYPES = {
|
|||||||
LOCATION_SCOPE_TYPES = (
|
LOCATION_SCOPE_TYPES = (
|
||||||
'region', 'sitegroup', 'site', 'location',
|
'region', 'sitegroup', 'site', 'location',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# MAC addresses
|
||||||
|
#
|
||||||
|
|
||||||
|
MACADDRESS_ASSIGNMENT_MODELS = Q(
|
||||||
|
Q(app_label='dcim', model='interface') |
|
||||||
|
Q(app_label='virtualization', model='vminterface')
|
||||||
|
)
|
||||||
|
@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
|
|||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
|
||||||
from circuits.models import CircuitTermination
|
from circuits.models import CircuitTermination, VirtualCircuit, VirtualCircuitTermination
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.filtersets import PrimaryIPFilterSet
|
from ipam.filtersets import PrimaryIPFilterSet
|
||||||
@ -20,7 +20,7 @@ from utilities.filters import (
|
|||||||
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
||||||
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup, VMInterface, VirtualMachine
|
||||||
from vpn.models import L2VPN
|
from vpn.models import L2VPN
|
||||||
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
|
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
|
||||||
from wireless.models import WirelessLAN, WirelessLink
|
from wireless.models import WirelessLAN, WirelessLink
|
||||||
@ -52,6 +52,7 @@ __all__ = (
|
|||||||
'InventoryItemRoleFilterSet',
|
'InventoryItemRoleFilterSet',
|
||||||
'InventoryItemTemplateFilterSet',
|
'InventoryItemTemplateFilterSet',
|
||||||
'LocationFilterSet',
|
'LocationFilterSet',
|
||||||
|
'MACAddressFilterSet',
|
||||||
'ManufacturerFilterSet',
|
'ManufacturerFilterSet',
|
||||||
'ModuleBayFilterSet',
|
'ModuleBayFilterSet',
|
||||||
'ModuleBayTemplateFilterSet',
|
'ModuleBayTemplateFilterSet',
|
||||||
@ -1099,7 +1100,7 @@ class DeviceFilterSet(
|
|||||||
label=_('Is full depth'),
|
label=_('Is full depth'),
|
||||||
)
|
)
|
||||||
mac_address = MultiValueMACAddressFilter(
|
mac_address = MultiValueMACAddressFilter(
|
||||||
field_name='interfaces__mac_address',
|
field_name='interfaces__mac_addresses__mac_address',
|
||||||
label=_('MAC address'),
|
label=_('MAC address'),
|
||||||
)
|
)
|
||||||
serial = MultiValueCharFilter(
|
serial = MultiValueCharFilter(
|
||||||
@ -1598,6 +1599,87 @@ class PowerOutletFilterSet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MACAddressFilterSet(NetBoxModelFilterSet):
|
||||||
|
mac_address = MultiValueMACAddressFilter()
|
||||||
|
device = MultiValueCharFilter(
|
||||||
|
method='filter_device',
|
||||||
|
field_name='name',
|
||||||
|
label=_('Device (name)'),
|
||||||
|
)
|
||||||
|
device_id = MultiValueNumberFilter(
|
||||||
|
method='filter_device',
|
||||||
|
field_name='pk',
|
||||||
|
label=_('Device (ID)'),
|
||||||
|
)
|
||||||
|
virtual_machine = MultiValueCharFilter(
|
||||||
|
method='filter_virtual_machine',
|
||||||
|
field_name='name',
|
||||||
|
label=_('Virtual machine (name)'),
|
||||||
|
)
|
||||||
|
virtual_machine_id = MultiValueNumberFilter(
|
||||||
|
method='filter_virtual_machine',
|
||||||
|
field_name='pk',
|
||||||
|
label=_('Virtual machine (ID)'),
|
||||||
|
)
|
||||||
|
interface = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='interface__name',
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
label=_('Interface (name)'),
|
||||||
|
)
|
||||||
|
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='interface',
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
label=_('Interface (ID)'),
|
||||||
|
)
|
||||||
|
vminterface = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='vminterface__name',
|
||||||
|
queryset=VMInterface.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
label=_('VM interface (name)'),
|
||||||
|
)
|
||||||
|
vminterface_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='vminterface',
|
||||||
|
queryset=VMInterface.objects.all(),
|
||||||
|
label=_('VM interface (ID)'),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MACAddress
|
||||||
|
fields = ('id', 'description', 'assigned_object_type', 'assigned_object_id')
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(mac_address__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
def filter_device(self, queryset, name, value):
|
||||||
|
devices = Device.objects.filter(**{f'{name}__in': value})
|
||||||
|
if not devices.exists():
|
||||||
|
return queryset.none()
|
||||||
|
interface_ids = []
|
||||||
|
for device in devices:
|
||||||
|
interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
|
||||||
|
return queryset.filter(
|
||||||
|
interface__in=interface_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_virtual_machine(self, queryset, name, value):
|
||||||
|
virtual_machines = VirtualMachine.objects.filter(**{f'{name}__in': value})
|
||||||
|
if not virtual_machines.exists():
|
||||||
|
return queryset.none()
|
||||||
|
interface_ids = []
|
||||||
|
for vm in virtual_machines:
|
||||||
|
interface_ids.extend(vm.interfaces.values_list('id', flat=True))
|
||||||
|
return queryset.filter(
|
||||||
|
vminterface__in=interface_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CommonInterfaceFilterSet(django_filters.FilterSet):
|
class CommonInterfaceFilterSet(django_filters.FilterSet):
|
||||||
vlan_id = django_filters.CharFilter(
|
vlan_id = django_filters.CharFilter(
|
||||||
method='filter_vlan_id',
|
method='filter_vlan_id',
|
||||||
@ -1702,7 +1784,21 @@ class InterfaceFilterSet(
|
|||||||
duplex = django_filters.MultipleChoiceFilter(
|
duplex = django_filters.MultipleChoiceFilter(
|
||||||
choices=InterfaceDuplexChoices
|
choices=InterfaceDuplexChoices
|
||||||
)
|
)
|
||||||
mac_address = MultiValueMACAddressFilter()
|
mac_address = MultiValueMACAddressFilter(
|
||||||
|
field_name='mac_addresses__mac_address',
|
||||||
|
label=_('MAC Address')
|
||||||
|
)
|
||||||
|
primary_mac_address_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='primary_mac_address',
|
||||||
|
queryset=MACAddress.objects.all(),
|
||||||
|
label=_('Primary MAC address (ID)'),
|
||||||
|
)
|
||||||
|
primary_mac_address = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='primary_mac_address__mac_address',
|
||||||
|
queryset=MACAddress.objects.all(),
|
||||||
|
to_field_name='mac_address',
|
||||||
|
label=_('Primary MAC address'),
|
||||||
|
)
|
||||||
wwn = MultiValueWWNFilter()
|
wwn = MultiValueWWNFilter()
|
||||||
poe_mode = django_filters.MultipleChoiceFilter(
|
poe_mode = django_filters.MultipleChoiceFilter(
|
||||||
choices=InterfacePoEModeChoices
|
choices=InterfacePoEModeChoices
|
||||||
@ -1746,6 +1842,16 @@ class InterfaceFilterSet(
|
|||||||
queryset=WirelessLink.objects.all(),
|
queryset=WirelessLink.objects.all(),
|
||||||
label=_('Wireless link')
|
label=_('Wireless link')
|
||||||
)
|
)
|
||||||
|
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='virtual_circuit_termination__virtual_circuit',
|
||||||
|
queryset=VirtualCircuit.objects.all(),
|
||||||
|
label=_('Virtual circuit (ID)'),
|
||||||
|
)
|
||||||
|
virtual_circuit_termination_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='virtual_circuit_termination',
|
||||||
|
queryset=VirtualCircuitTermination.objects.all(),
|
||||||
|
label=_('Virtual circuit termination (ID)'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
|
@ -38,6 +38,7 @@ __all__ = (
|
|||||||
'InventoryItemRoleBulkEditForm',
|
'InventoryItemRoleBulkEditForm',
|
||||||
'InventoryItemTemplateBulkEditForm',
|
'InventoryItemTemplateBulkEditForm',
|
||||||
'LocationBulkEditForm',
|
'LocationBulkEditForm',
|
||||||
|
'MACAddressBulkEditForm',
|
||||||
'ManufacturerBulkEditForm',
|
'ManufacturerBulkEditForm',
|
||||||
'ModuleBulkEditForm',
|
'ModuleBulkEditForm',
|
||||||
'ModuleBayBulkEditForm',
|
'ModuleBayBulkEditForm',
|
||||||
@ -1392,9 +1393,9 @@ class PowerOutletBulkEditForm(
|
|||||||
class InterfaceBulkEditForm(
|
class InterfaceBulkEditForm(
|
||||||
ComponentBulkEditForm,
|
ComponentBulkEditForm,
|
||||||
form_from_model(Interface, [
|
form_from_model(Interface, [
|
||||||
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
|
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'wwn', 'mtu', 'mgmt_only', 'mark_connected',
|
||||||
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||||
'tx_power', 'wireless_lans'
|
'wireless_lans'
|
||||||
])
|
])
|
||||||
):
|
):
|
||||||
enabled = forms.NullBooleanField(
|
enabled = forms.NullBooleanField(
|
||||||
@ -1506,7 +1507,7 @@ class InterfaceBulkEditForm(
|
|||||||
model = Interface
|
model = Interface
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('module', 'type', 'label', 'speed', 'duplex', 'description'),
|
FieldSet('module', 'type', 'label', 'speed', 'duplex', 'description'),
|
||||||
FieldSet('vrf', 'mac_address', 'wwn', name=_('Addressing')),
|
FieldSet('vrf', 'wwn', name=_('Addressing')),
|
||||||
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
|
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
|
||||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||||
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
|
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
|
||||||
@ -1517,9 +1518,9 @@ class InterfaceBulkEditForm(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu',
|
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'wwn', 'vdcs', 'mtu', 'description',
|
||||||
'description', 'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||||
'tx_power', 'untagged_vlan', 'tagged_vlans', 'vrf', 'wireless_lans'
|
'untagged_vlan', 'tagged_vlans', 'vrf', 'wireless_lans'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -1719,3 +1720,22 @@ class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
FieldSet('device', 'status', 'tenant'),
|
FieldSet('device', 'status', 'tenant'),
|
||||||
)
|
)
|
||||||
nullable_fields = ('device', 'tenant', )
|
nullable_fields = ('device', 'tenant', )
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Addressing
|
||||||
|
#
|
||||||
|
|
||||||
|
class MACAddressBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
|
description = forms.CharField(
|
||||||
|
label=_('Description'),
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
comments = CommentField()
|
||||||
|
|
||||||
|
model = MACAddress
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('description'),
|
||||||
|
)
|
||||||
|
nullable_fields = ('description', 'comments')
|
||||||
|
@ -17,7 +17,7 @@ from utilities.forms.fields import (
|
|||||||
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVTypedChoiceField,
|
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVTypedChoiceField,
|
||||||
SlugField,
|
SlugField,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster, VMInterface, VirtualMachine
|
||||||
from wireless.choices import WirelessRoleChoices
|
from wireless.choices import WirelessRoleChoices
|
||||||
from .common import ModuleCommonForm
|
from .common import ModuleCommonForm
|
||||||
|
|
||||||
@ -34,6 +34,7 @@ __all__ = (
|
|||||||
'InventoryItemImportForm',
|
'InventoryItemImportForm',
|
||||||
'InventoryItemRoleImportForm',
|
'InventoryItemRoleImportForm',
|
||||||
'LocationImportForm',
|
'LocationImportForm',
|
||||||
|
'MACAddressImportForm',
|
||||||
'ManufacturerImportForm',
|
'ManufacturerImportForm',
|
||||||
'ModuleImportForm',
|
'ModuleImportForm',
|
||||||
'ModuleBayImportForm',
|
'ModuleBayImportForm',
|
||||||
@ -906,7 +907,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = (
|
fields = (
|
||||||
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
|
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
|
||||||
'mark_connected', 'mac_address', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
|
'mark_connected', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
|
||||||
'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
|
'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1167,6 +1168,90 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm):
|
|||||||
fields = ('name', 'slug', 'color', 'description')
|
fields = ('name', 'slug', 'color', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Addressing
|
||||||
|
#
|
||||||
|
|
||||||
|
class MACAddressImportForm(NetBoxModelImportForm):
|
||||||
|
device = CSVModelChoiceField(
|
||||||
|
label=_('Device'),
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text=_('Parent device of assigned interface (if any)')
|
||||||
|
)
|
||||||
|
virtual_machine = CSVModelChoiceField(
|
||||||
|
label=_('Virtual machine'),
|
||||||
|
queryset=VirtualMachine.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text=_('Parent VM of assigned interface (if any)')
|
||||||
|
)
|
||||||
|
interface = CSVModelChoiceField(
|
||||||
|
label=_('Interface'),
|
||||||
|
queryset=Interface.objects.none(), # Can also refer to VMInterface
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text=_('Assigned interface')
|
||||||
|
)
|
||||||
|
is_primary = forms.BooleanField(
|
||||||
|
label=_('Is primary'),
|
||||||
|
help_text=_('Make this the primary MAC address for the assigned interface'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MACAddress
|
||||||
|
fields = [
|
||||||
|
'mac_address', 'device', 'virtual_machine', 'interface', 'is_primary', 'description', 'comments', 'tags',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
|
||||||
|
# Limit interface queryset by assigned device
|
||||||
|
if data.get('device'):
|
||||||
|
self.fields['interface'].queryset = Interface.objects.filter(
|
||||||
|
**{f"device__{self.fields['device'].to_field_name}": data['device']}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Limit interface queryset by assigned device
|
||||||
|
elif data.get('virtual_machine'):
|
||||||
|
self.fields['interface'].queryset = VMInterface.objects.filter(
|
||||||
|
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
device = self.cleaned_data.get('device')
|
||||||
|
virtual_machine = self.cleaned_data.get('virtual_machine')
|
||||||
|
interface = self.cleaned_data.get('interface')
|
||||||
|
|
||||||
|
# Validate interface assignment
|
||||||
|
if interface and not device and not virtual_machine:
|
||||||
|
raise forms.ValidationError({
|
||||||
|
"interface": _("Must specify the parent device or VM when assigning an interface")
|
||||||
|
})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Set interface assignment
|
||||||
|
if interface := self.cleaned_data.get('interface'):
|
||||||
|
self.instance.assigned_object = interface
|
||||||
|
|
||||||
|
instance = super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Assign the MAC address as primary for its interface, if designated as such
|
||||||
|
if interface and self.cleaned_data['is_primary'] and self.instance.pk:
|
||||||
|
interface.primary_mac_address = self.instance
|
||||||
|
interface.save()
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
@ -3,7 +3,9 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
|
from dcim.models import MACAddress
|
||||||
from utilities.forms import get_field_value
|
from utilities.forms import get_field_value
|
||||||
|
from utilities.forms.fields import DynamicModelChoiceField
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'InterfaceCommonForm',
|
'InterfaceCommonForm',
|
||||||
@ -12,17 +14,17 @@ __all__ = (
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceCommonForm(forms.Form):
|
class InterfaceCommonForm(forms.Form):
|
||||||
mac_address = forms.CharField(
|
|
||||||
empty_value=None,
|
|
||||||
required=False,
|
|
||||||
label=_('MAC address')
|
|
||||||
)
|
|
||||||
mtu = forms.IntegerField(
|
mtu = forms.IntegerField(
|
||||||
required=False,
|
required=False,
|
||||||
min_value=INTERFACE_MTU_MIN,
|
min_value=INTERFACE_MTU_MIN,
|
||||||
max_value=INTERFACE_MTU_MAX,
|
max_value=INTERFACE_MTU_MAX,
|
||||||
label=_('MTU')
|
label=_('MTU')
|
||||||
)
|
)
|
||||||
|
primary_mac_address = DynamicModelChoiceField(
|
||||||
|
queryset=MACAddress.objects.all(),
|
||||||
|
label=_('Primary MAC address'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -40,6 +42,10 @@ class InterfaceCommonForm(forms.Form):
|
|||||||
if interface_mode != InterfaceModeChoices.MODE_Q_IN_Q:
|
if interface_mode != InterfaceModeChoices.MODE_Q_IN_Q:
|
||||||
del self.fields['qinq_svlan']
|
del self.fields['qinq_svlan']
|
||||||
|
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
filter_name = f'{self._meta.model._meta.model_name}_id'
|
||||||
|
self.fields['primary_mac_address'].widget.add_query_param(filter_name, self.instance.pk)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_ch
|
|||||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
from utilities.forms.widgets import NumberWithOptions
|
from utilities.forms.widgets import NumberWithOptions
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
||||||
from vpn.models import L2VPN
|
from vpn.models import L2VPN
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
|
|
||||||
@ -34,6 +34,7 @@ __all__ = (
|
|||||||
'InventoryItemFilterForm',
|
'InventoryItemFilterForm',
|
||||||
'InventoryItemRoleFilterForm',
|
'InventoryItemRoleFilterForm',
|
||||||
'LocationFilterForm',
|
'LocationFilterForm',
|
||||||
|
'MACAddressFilterForm',
|
||||||
'ManufacturerFilterForm',
|
'ManufacturerFilterForm',
|
||||||
'ModuleFilterForm',
|
'ModuleFilterForm',
|
||||||
'ModuleBayFilterForm',
|
'ModuleBayFilterForm',
|
||||||
@ -1574,6 +1575,34 @@ class InventoryItemRoleFilterForm(NetBoxModelFilterSetForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Addressing
|
||||||
|
#
|
||||||
|
|
||||||
|
class MACAddressFilterForm(NetBoxModelFilterSetForm):
|
||||||
|
model = MACAddress
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
|
FieldSet('mac_address', 'device_id', 'virtual_machine_id', name=_('MAC address')),
|
||||||
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id')
|
||||||
|
mac_address = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=_('MAC address')
|
||||||
|
)
|
||||||
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Assigned Device'),
|
||||||
|
)
|
||||||
|
virtual_machine_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=VirtualMachine.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Assigned VM'),
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Connections
|
# Connections
|
||||||
#
|
#
|
||||||
|
@ -18,7 +18,7 @@ from utilities.forms.fields import (
|
|||||||
)
|
)
|
||||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||||
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
|
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster, VMInterface
|
||||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||||
from .common import InterfaceCommonForm, ModuleCommonForm
|
from .common import InterfaceCommonForm, ModuleCommonForm
|
||||||
|
|
||||||
@ -42,6 +42,7 @@ __all__ = (
|
|||||||
'InventoryItemRoleForm',
|
'InventoryItemRoleForm',
|
||||||
'InventoryItemTemplateForm',
|
'InventoryItemTemplateForm',
|
||||||
'LocationForm',
|
'LocationForm',
|
||||||
|
'MACAddressForm',
|
||||||
'ManufacturerForm',
|
'ManufacturerForm',
|
||||||
'ModuleForm',
|
'ModuleForm',
|
||||||
'ModuleBayForm',
|
'ModuleBayForm',
|
||||||
@ -112,12 +113,14 @@ class SiteForm(TenancyForm, NetBoxModelForm):
|
|||||||
region = DynamicModelChoiceField(
|
region = DynamicModelChoiceField(
|
||||||
label=_('Region'),
|
label=_('Region'),
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
group = DynamicModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
label=_('Group'),
|
label=_('Group'),
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
asns = DynamicModelMultipleChoiceField(
|
asns = DynamicModelMultipleChoiceField(
|
||||||
queryset=ASN.objects.all(),
|
queryset=ASN.objects.all(),
|
||||||
@ -206,7 +209,8 @@ class RackRoleForm(NetBoxModelForm):
|
|||||||
class RackTypeForm(NetBoxModelForm):
|
class RackTypeForm(NetBoxModelForm):
|
||||||
manufacturer = DynamicModelChoiceField(
|
manufacturer = DynamicModelChoiceField(
|
||||||
label=_('Manufacturer'),
|
label=_('Manufacturer'),
|
||||||
queryset=Manufacturer.objects.all()
|
queryset=Manufacturer.objects.all(),
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
slug = SlugField(
|
slug = SlugField(
|
||||||
@ -348,7 +352,8 @@ class ManufacturerForm(NetBoxModelForm):
|
|||||||
class DeviceTypeForm(NetBoxModelForm):
|
class DeviceTypeForm(NetBoxModelForm):
|
||||||
manufacturer = DynamicModelChoiceField(
|
manufacturer = DynamicModelChoiceField(
|
||||||
label=_('Manufacturer'),
|
label=_('Manufacturer'),
|
||||||
queryset=Manufacturer.objects.all()
|
queryset=Manufacturer.objects.all(),
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
default_platform = DynamicModelChoiceField(
|
default_platform = DynamicModelChoiceField(
|
||||||
label=_('Default platform'),
|
label=_('Default platform'),
|
||||||
@ -436,7 +441,8 @@ class PlatformForm(NetBoxModelForm):
|
|||||||
manufacturer = DynamicModelChoiceField(
|
manufacturer = DynamicModelChoiceField(
|
||||||
label=_('Manufacturer'),
|
label=_('Manufacturer'),
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
config_template = DynamicModelChoiceField(
|
config_template = DynamicModelChoiceField(
|
||||||
label=_('Config template'),
|
label=_('Config template'),
|
||||||
@ -508,7 +514,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
label=_('Device role'),
|
label=_('Device role'),
|
||||||
queryset=DeviceRole.objects.all()
|
queryset=DeviceRole.objects.all(),
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
platform = DynamicModelChoiceField(
|
platform = DynamicModelChoiceField(
|
||||||
label=_('Platform'),
|
label=_('Platform'),
|
||||||
@ -750,7 +757,8 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm):
|
|||||||
power_panel = DynamicModelChoiceField(
|
power_panel = DynamicModelChoiceField(
|
||||||
label=_('Power panel'),
|
label=_('Power panel'),
|
||||||
queryset=PowerPanel.objects.all(),
|
queryset=PowerPanel.objects.all(),
|
||||||
selector=True
|
selector=True,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
rack = DynamicModelChoiceField(
|
rack = DynamicModelChoiceField(
|
||||||
label=_('Rack'),
|
label=_('Rack'),
|
||||||
@ -1403,7 +1411,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
|||||||
FieldSet(
|
FieldSet(
|
||||||
'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags', name=_('Interface')
|
'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags', name=_('Interface')
|
||||||
),
|
),
|
||||||
FieldSet('vrf', 'mac_address', 'wwn', name=_('Addressing')),
|
FieldSet('vrf', 'primary_mac_address', 'wwn', name=_('Addressing')),
|
||||||
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
|
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
|
||||||
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
|
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
|
||||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||||
@ -1420,10 +1428,11 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
|
'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge',
|
||||||
'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
|
'lag', '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',
|
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
|
||||||
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'tags',
|
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'primary_mac_address',
|
||||||
|
'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'speed': NumberWithOptions(
|
'speed': NumberWithOptions(
|
||||||
@ -1717,3 +1726,72 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
|
|||||||
'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant',
|
'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant',
|
||||||
'comments', 'tags'
|
'comments', 'tags'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Addressing
|
||||||
|
#
|
||||||
|
|
||||||
|
class MACAddressForm(NetBoxModelForm):
|
||||||
|
mac_address = forms.CharField(
|
||||||
|
required=True,
|
||||||
|
label=_('MAC address')
|
||||||
|
)
|
||||||
|
interface = DynamicModelChoiceField(
|
||||||
|
label=_('Interface'),
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
vminterface = DynamicModelChoiceField(
|
||||||
|
label=_('VM Interface'),
|
||||||
|
queryset=VMInterface.objects.all(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet(
|
||||||
|
'mac_address', 'description', 'tags',
|
||||||
|
),
|
||||||
|
FieldSet(
|
||||||
|
TabbedGroups(
|
||||||
|
FieldSet('interface', name=_('Device')),
|
||||||
|
FieldSet('vminterface', name=_('Virtual Machine')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MACAddress
|
||||||
|
fields = [
|
||||||
|
'mac_address', 'interface', 'vminterface', 'description', 'tags',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Initialize helper selectors
|
||||||
|
instance = kwargs.get('instance')
|
||||||
|
initial = kwargs.get('initial', {}).copy()
|
||||||
|
if instance:
|
||||||
|
if type(instance.assigned_object) is Interface:
|
||||||
|
initial['interface'] = instance.assigned_object
|
||||||
|
elif type(instance.assigned_object) is VMInterface:
|
||||||
|
initial['vminterface'] = instance.assigned_object
|
||||||
|
kwargs['initial'] = initial
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Handle object assignment
|
||||||
|
selected_objects = [
|
||||||
|
field for field in ('interface', 'vminterface') if self.cleaned_data[field]
|
||||||
|
]
|
||||||
|
if len(selected_objects) > 1:
|
||||||
|
raise forms.ValidationError({
|
||||||
|
selected_objects[1]: _("A MAC address can only be assigned to a single object.")
|
||||||
|
})
|
||||||
|
elif selected_objects:
|
||||||
|
self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
|
||||||
|
else:
|
||||||
|
self.instance.assigned_object = None
|
||||||
|
@ -23,6 +23,7 @@ __all__ = (
|
|||||||
'InventoryItemFilter',
|
'InventoryItemFilter',
|
||||||
'InventoryItemRoleFilter',
|
'InventoryItemRoleFilter',
|
||||||
'LocationFilter',
|
'LocationFilter',
|
||||||
|
'MACAddressFilter',
|
||||||
'ManufacturerFilter',
|
'ManufacturerFilter',
|
||||||
'ModuleFilter',
|
'ModuleFilter',
|
||||||
'ModuleBayFilter',
|
'ModuleBayFilter',
|
||||||
@ -133,6 +134,12 @@ class FrontPortTemplateFilter(BaseFilterMixin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.filter(models.MACAddress, lookups=True)
|
||||||
|
@autotype_decorator(filtersets.MACAddressFilterSet)
|
||||||
|
class MACAddressFilter(BaseFilterMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter(models.Interface, lookups=True)
|
@strawberry_django.filter(models.Interface, lookups=True)
|
||||||
@autotype_decorator(filtersets.InterfaceFilterSet)
|
@autotype_decorator(filtersets.InterfaceFilterSet)
|
||||||
class InterfaceFilter(BaseFilterMixin):
|
class InterfaceFilter(BaseFilterMixin):
|
||||||
|
@ -44,6 +44,9 @@ class DCIMQuery:
|
|||||||
front_port_template: FrontPortTemplateType = strawberry_django.field()
|
front_port_template: FrontPortTemplateType = strawberry_django.field()
|
||||||
front_port_template_list: List[FrontPortTemplateType] = strawberry_django.field()
|
front_port_template_list: List[FrontPortTemplateType] = strawberry_django.field()
|
||||||
|
|
||||||
|
mac_address: MACAddressType = strawberry_django.field()
|
||||||
|
mac_address_list: List[MACAddressType] = strawberry_django.field()
|
||||||
|
|
||||||
interface: InterfaceType = strawberry_django.field()
|
interface: InterfaceType = strawberry_django.field()
|
||||||
interface_list: List[InterfaceType] = strawberry_django.field()
|
interface_list: List[InterfaceType] = strawberry_django.field()
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ __all__ = (
|
|||||||
'InventoryItemRoleType',
|
'InventoryItemRoleType',
|
||||||
'InventoryItemTemplateType',
|
'InventoryItemTemplateType',
|
||||||
'LocationType',
|
'LocationType',
|
||||||
|
'MACAddressType',
|
||||||
'ManufacturerType',
|
'ManufacturerType',
|
||||||
'ModularComponentType',
|
'ModularComponentType',
|
||||||
'ModuleType',
|
'ModuleType',
|
||||||
@ -366,6 +367,22 @@ class FrontPortTemplateType(ModularComponentTemplateType):
|
|||||||
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
|
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(
|
||||||
|
models.MACAddress,
|
||||||
|
exclude=('assigned_object_type', 'assigned_object_id'),
|
||||||
|
filters=MACAddressFilter
|
||||||
|
)
|
||||||
|
class MACAddressType(NetBoxObjectType):
|
||||||
|
mac_address: str
|
||||||
|
|
||||||
|
@strawberry_django.field
|
||||||
|
def assigned_object(self) -> Annotated[Union[
|
||||||
|
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
|
||||||
|
Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')],
|
||||||
|
], strawberry.union("MACAddressAssignmentType")] | None:
|
||||||
|
return self.assigned_object
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
models.Interface,
|
models.Interface,
|
||||||
exclude=('_path',),
|
exclude=('_path',),
|
||||||
@ -373,7 +390,6 @@ class FrontPortTemplateType(ModularComponentTemplateType):
|
|||||||
)
|
)
|
||||||
class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||||
_name: str
|
_name: str
|
||||||
mac_address: str | None
|
|
||||||
wwn: str | None
|
wwn: str | None
|
||||||
parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
bridge: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
bridge: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
@ -381,6 +397,7 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
|
|||||||
wireless_link: Annotated["WirelessLinkType", strawberry.lazy('wireless.graphql.types')] | None
|
wireless_link: Annotated["WirelessLinkType", strawberry.lazy('wireless.graphql.types')] | None
|
||||||
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
|
primary_mac_address: Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
|
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
|
|
||||||
@ -390,6 +407,7 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
|
|||||||
wireless_lans: List[Annotated["WirelessLANType", strawberry.lazy('wireless.graphql.types')]]
|
wireless_lans: List[Annotated["WirelessLANType", strawberry.lazy('wireless.graphql.types')]]
|
||||||
member_interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
|
member_interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
child_interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
|
child_interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
mac_addresses: List[Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
|
36
netbox/dcim/migrations/0199_macaddress.py
Normal file
36
netbox/dcim/migrations/0199_macaddress.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
import taggit.managers
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import dcim.fields
|
||||||
|
import utilities.json
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0198_natural_ordering'),
|
||||||
|
('extras', '0122_charfield_null_choices'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MACAddress',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('comments', models.TextField(blank=True)),
|
||||||
|
('mac_address', dcim.fields.MACAddressField()),
|
||||||
|
('assigned_object_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
||||||
|
('assigned_object_type', models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
|
||||||
|
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'ordering': ('mac_address',)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
52
netbox/dcim/migrations/0200_populate_mac_addresses.py
Normal file
52
netbox/dcim/migrations/0200_populate_mac_addresses.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_mac_addresses(apps, schema_editor):
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
Interface = apps.get_model('dcim', 'Interface')
|
||||||
|
MACAddress = apps.get_model('dcim', 'MACAddress')
|
||||||
|
interface_ct = ContentType.objects.get_for_model(Interface)
|
||||||
|
|
||||||
|
mac_addresses = [
|
||||||
|
MACAddress(
|
||||||
|
mac_address=interface.mac_address,
|
||||||
|
assigned_object_type=interface_ct,
|
||||||
|
assigned_object_id=interface.pk
|
||||||
|
)
|
||||||
|
for interface in Interface.objects.filter(mac_address__isnull=False)
|
||||||
|
]
|
||||||
|
MACAddress.objects.bulk_create(mac_addresses, batch_size=100)
|
||||||
|
|
||||||
|
# TODO: Optimize interface updates
|
||||||
|
for mac_address in mac_addresses:
|
||||||
|
Interface.objects.filter(pk=mac_address.assigned_object_id).update(primary_mac_address=mac_address)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0199_macaddress'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='primary_mac_address',
|
||||||
|
field=models.OneToOneField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name='+',
|
||||||
|
to='dcim.macaddress'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=populate_mac_addresses,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='interface',
|
||||||
|
name='mac_address',
|
||||||
|
),
|
||||||
|
]
|
@ -10,7 +10,7 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import MACAddressField, WWNField
|
from dcim.fields import WWNField
|
||||||
from netbox.choices import ColorChoices
|
from netbox.choices import ColorChoices
|
||||||
from netbox.models import OrganizationalModel, NetBoxModel
|
from netbox.models import OrganizationalModel, NetBoxModel
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
@ -505,11 +505,6 @@ class BaseInterface(models.Model):
|
|||||||
verbose_name=_('enabled'),
|
verbose_name=_('enabled'),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
mac_address = MACAddressField(
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_('MAC address')
|
|
||||||
)
|
|
||||||
mtu = models.PositiveIntegerField(
|
mtu = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
@ -572,6 +567,14 @@ class BaseInterface(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_('VLAN Translation Policy')
|
verbose_name=_('VLAN Translation Policy')
|
||||||
)
|
)
|
||||||
|
primary_mac_address = models.OneToOneField(
|
||||||
|
to='dcim.MACAddress',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='+',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_('primary MAC address')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
@ -585,6 +588,14 @@ class BaseInterface(models.Model):
|
|||||||
'qinq_svlan': _("Only Q-in-Q interfaces may specify a service VLAN.")
|
'qinq_svlan': _("Only Q-in-Q interfaces may specify a service VLAN.")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Check that the primary MAC address (if any) is assigned to this interface
|
||||||
|
if self.primary_mac_address and self.primary_mac_address.assigned_object != self:
|
||||||
|
raise ValidationError({
|
||||||
|
'primary_mac_address': _("MAC address {mac_address} is not assigned to this interface.").format(
|
||||||
|
mac_address=self.primary_mac_address
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
# Remove untagged VLAN assignment for non-802.1Q interfaces
|
# Remove untagged VLAN assignment for non-802.1Q interfaces
|
||||||
@ -609,6 +620,11 @@ class BaseInterface(models.Model):
|
|||||||
def count_fhrp_groups(self):
|
def count_fhrp_groups(self):
|
||||||
return self.fhrp_group_assignments.count()
|
return self.fhrp_group_assignments.count()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def mac_address(self):
|
||||||
|
if self.primary_mac_address:
|
||||||
|
return self.primary_mac_address.mac_address
|
||||||
|
|
||||||
|
|
||||||
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint, TrackingModelMixin):
|
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint, TrackingModelMixin):
|
||||||
"""
|
"""
|
||||||
@ -738,6 +754,12 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
object_id_field='assigned_object_id',
|
object_id_field='assigned_object_id',
|
||||||
related_query_name='interface'
|
related_query_name='interface'
|
||||||
)
|
)
|
||||||
|
mac_addresses = GenericRelation(
|
||||||
|
to='dcim.MACAddress',
|
||||||
|
content_type_field='assigned_object_type',
|
||||||
|
object_id_field='assigned_object_id',
|
||||||
|
related_query_name='interface'
|
||||||
|
)
|
||||||
fhrp_group_assignments = GenericRelation(
|
fhrp_group_assignments = GenericRelation(
|
||||||
to='ipam.FHRPGroupAssignment',
|
to='ipam.FHRPGroupAssignment',
|
||||||
content_type_field='interface_type',
|
content_type_field='interface_type',
|
||||||
@ -976,6 +998,14 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
|
|||||||
def l2vpn_termination(self):
|
def l2vpn_termination(self):
|
||||||
return self.l2vpn_terminations.first()
|
return self.l2vpn_terminations.first()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def connected_endpoints(self):
|
||||||
|
# If this is a virtual interface, return the remote endpoint of the connected
|
||||||
|
# virtual circuit, if any.
|
||||||
|
if self.is_virtual and hasattr(self, 'virtual_circuit_termination'):
|
||||||
|
return self.virtual_circuit_termination.peer_terminations
|
||||||
|
return super().connected_endpoints
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Pass-through ports
|
# Pass-through ports
|
||||||
|
@ -3,6 +3,7 @@ import yaml
|
|||||||
|
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
@ -16,6 +17,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
|
from dcim.fields import MACAddressField
|
||||||
from extras.models import ConfigContextModel, CustomField
|
from extras.models import ConfigContextModel, CustomField
|
||||||
from extras.querysets import ConfigContextModelQuerySet
|
from extras.querysets import ConfigContextModelQuerySet
|
||||||
from netbox.choices import ColorChoices
|
from netbox.choices import ColorChoices
|
||||||
@ -33,6 +35,7 @@ __all__ = (
|
|||||||
'Device',
|
'Device',
|
||||||
'DeviceRole',
|
'DeviceRole',
|
||||||
'DeviceType',
|
'DeviceType',
|
||||||
|
'MACAddress',
|
||||||
'Manufacturer',
|
'Manufacturer',
|
||||||
'Module',
|
'Module',
|
||||||
'ModuleType',
|
'ModuleType',
|
||||||
@ -1470,3 +1473,37 @@ class VirtualDeviceContext(PrimaryModel):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
f'primary_ip{family}': _('Primary IP address must belong to an interface on the assigned device.')
|
f'primary_ip{family}': _('Primary IP address must belong to an interface on the assigned device.')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Addressing
|
||||||
|
#
|
||||||
|
|
||||||
|
class MACAddress(PrimaryModel):
|
||||||
|
mac_address = MACAddressField(
|
||||||
|
verbose_name=_('MAC address')
|
||||||
|
)
|
||||||
|
assigned_object_type = models.ForeignKey(
|
||||||
|
to='contenttypes.ContentType',
|
||||||
|
limit_choices_to=MACADDRESS_ASSIGNMENT_MODELS,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
assigned_object_id = models.PositiveBigIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
assigned_object = GenericForeignKey(
|
||||||
|
ct_field='assigned_object_type',
|
||||||
|
fk_field='assigned_object_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('mac_address',)
|
||||||
|
verbose_name = _('MAC address')
|
||||||
|
verbose_name_plural = _('MAC addresses')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.mac_address)
|
||||||
|
@ -98,19 +98,28 @@ class FrontPortIndex(SearchIndex):
|
|||||||
display_attrs = ('device', 'label', 'type', 'description')
|
display_attrs = ('device', 'label', 'type', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
@register_search
|
||||||
|
class MACAddressIndex(SearchIndex):
|
||||||
|
model = models.MACAddress
|
||||||
|
fields = (
|
||||||
|
('mac_address', 100),
|
||||||
|
('description', 500),
|
||||||
|
)
|
||||||
|
display_attrs = ('mac_address', 'interface')
|
||||||
|
|
||||||
|
|
||||||
@register_search
|
@register_search
|
||||||
class InterfaceIndex(SearchIndex):
|
class InterfaceIndex(SearchIndex):
|
||||||
model = models.Interface
|
model = models.Interface
|
||||||
fields = (
|
fields = (
|
||||||
('name', 100),
|
('name', 100),
|
||||||
('label', 200),
|
('label', 200),
|
||||||
('mac_address', 300),
|
|
||||||
('wwn', 300),
|
('wwn', 300),
|
||||||
('description', 500),
|
('description', 500),
|
||||||
('mtu', 2000),
|
('mtu', 2000),
|
||||||
('speed', 2000),
|
('speed', 2000),
|
||||||
)
|
)
|
||||||
display_attrs = ('device', 'label', 'type', 'mac_address', 'wwn', 'description')
|
display_attrs = ('device', 'label', 'type', 'wwn', 'description')
|
||||||
|
|
||||||
|
|
||||||
@register_search
|
@register_search
|
||||||
|
@ -29,6 +29,7 @@ __all__ = (
|
|||||||
'InterfaceTable',
|
'InterfaceTable',
|
||||||
'InventoryItemRoleTable',
|
'InventoryItemRoleTable',
|
||||||
'InventoryItemTable',
|
'InventoryItemTable',
|
||||||
|
'MACAddressTable',
|
||||||
'ModuleBayTable',
|
'ModuleBayTable',
|
||||||
'PlatformTable',
|
'PlatformTable',
|
||||||
'PowerOutletTable',
|
'PowerOutletTable',
|
||||||
@ -42,6 +43,16 @@ MODULEBAY_STATUS = """
|
|||||||
{% badge record.installed_module.get_status_display bg_color=record.installed_module.get_status_color %}
|
{% badge record.installed_module.get_status_display bg_color=record.installed_module.get_status_color %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
MACADDRESS_LINK = """
|
||||||
|
{% if record.pk %}
|
||||||
|
<a href="{{ record.get_absolute_url }}" id="macaddress_{{ record.pk }}">{{ record.mac_address }}</a>
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
MACADDRESS_COPY_BUTTON = """
|
||||||
|
{% copy_content record.pk prefix="macaddress_" %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device roles
|
# Device roles
|
||||||
@ -588,6 +599,10 @@ class BaseInterfaceTable(NetBoxTable):
|
|||||||
verbose_name=_('Q-in-Q SVLAN'),
|
verbose_name=_('Q-in-Q SVLAN'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
primary_mac_address = tables.Column(
|
||||||
|
verbose_name=_('MAC Address'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
|
||||||
def value_ip_addresses(self, value):
|
def value_ip_addresses(self, value):
|
||||||
return ",".join([str(obj.address) for obj in value.all()])
|
return ",".join([str(obj.address) for obj in value.all()])
|
||||||
@ -634,15 +649,23 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi
|
|||||||
url_name='dcim:interface_list'
|
url_name='dcim:interface_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Override PathEndpointTable.connection to accommodate virtual circuits
|
||||||
|
connection = columns.TemplateColumn(
|
||||||
|
accessor='_path__destinations',
|
||||||
|
template_code=INTERFACE_LINKTERMINATION,
|
||||||
|
verbose_name=_('Connection'),
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = models.Interface
|
model = models.Interface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
||||||
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role',
|
'speed', 'speed_formatted', 'duplex', 'mode', 'primary_mac_address', 'wwn', 'poe_mode', 'poe_type',
|
||||||
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected',
|
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description',
|
||||||
'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf',
|
'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection',
|
||||||
'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans',
|
||||||
'inventory_items', 'created', 'last_updated',
|
'qinq_svlan', 'inventory_items', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||||
|
|
||||||
@ -1098,3 +1121,34 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'identifier', 'status', 'tenant', 'primary_ip',
|
'pk', 'name', 'identifier', 'status', 'tenant', 'primary_ip',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MACAddressTable(NetBoxTable):
|
||||||
|
mac_address = tables.TemplateColumn(
|
||||||
|
template_code=MACADDRESS_LINK,
|
||||||
|
verbose_name=_('MAC Address')
|
||||||
|
)
|
||||||
|
assigned_object = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name=_('Interface')
|
||||||
|
)
|
||||||
|
assigned_object_parent = tables.Column(
|
||||||
|
accessor='assigned_object__parent_object',
|
||||||
|
linkify=True,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name=_('Parent')
|
||||||
|
)
|
||||||
|
tags = columns.TagColumn(
|
||||||
|
url_name='dcim:macaddress_list'
|
||||||
|
)
|
||||||
|
actions = columns.ActionsColumn(
|
||||||
|
extra_buttons=MACADDRESS_COPY_BUTTON
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(DeviceComponentTable.Meta):
|
||||||
|
model = models.MACAddress
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'created', 'last_updated',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'mac_address', 'assigned_object_parent', 'assigned_object')
|
||||||
|
@ -10,6 +10,20 @@ LINKTERMINATION = """
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
INTERFACE_LINKTERMINATION = """
|
||||||
|
{% load i18n %}
|
||||||
|
{% if record.is_virtual and record.virtual_circuit_termination %}
|
||||||
|
{% for termination in record.connected_endpoints %}
|
||||||
|
<a href="{{ termination.interface.parent_object.get_absolute_url }}">{{ termination.interface.parent_object }}</a>
|
||||||
|
<i class="mdi mdi-chevron-right"></i>
|
||||||
|
<a href="{{ termination.interface.get_absolute_url }}">{{ termination.interface }}</a>
|
||||||
|
{% trans "via" %}
|
||||||
|
<a href="{{ termination.parent_object.get_absolute_url }}">{{ termination.parent_object }}</a>
|
||||||
|
{% if not forloop.last %}<br />{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}""" + LINKTERMINATION + """{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
CABLE_LENGTH = """
|
CABLE_LENGTH = """
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %}
|
{% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %}
|
||||||
@ -314,6 +328,9 @@ INTERFACE_BUTTONS = """
|
|||||||
{% if perms.ipam.add_ipaddress %}
|
{% if perms.ipam.add_ipaddress %}
|
||||||
<li><a class="dropdown-item" href="{% url 'ipam:ipaddress_add' %}?interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">IP Address</a></li>
|
<li><a class="dropdown-item" href="{% url 'ipam:ipaddress_add' %}?interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">IP Address</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_macaddress %}
|
||||||
|
<li><a class="dropdown-item" href="{% url 'dcim:macaddress_add' %}?interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">MAC Address</a></li>
|
||||||
|
{% endif %}
|
||||||
{% if perms.dcim.add_inventoryitem %}
|
{% if perms.dcim.add_inventoryitem %}
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ record.device_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Inventory Item</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ record.device_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Inventory Item</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -9,8 +9,8 @@ from ipam.models import ASN, IPAddress, RIR, VLAN, VLANTranslationPolicy, VRF
|
|||||||
from netbox.choices import ColorChoices, WeightUnitChoices
|
from netbox.choices import ColorChoices, WeightUnitChoices
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
|
||||||
from virtualization.models import Cluster, ClusterType, ClusterGroup
|
from virtualization.models import Cluster, ClusterType, ClusterGroup, VMInterface, VirtualMachine
|
||||||
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||||
|
|
||||||
|
|
||||||
@ -2323,10 +2323,17 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
PowerOutlet(device=devices[1], name='Power Outlet 2'),
|
PowerOutlet(device=devices[1], name='Power Outlet 2'),
|
||||||
))
|
))
|
||||||
interfaces = (
|
interfaces = (
|
||||||
Interface(device=devices[0], name='Interface 1', mac_address='00-00-00-00-00-01'),
|
Interface(device=devices[0], name='Interface 1'),
|
||||||
Interface(device=devices[1], name='Interface 2', mac_address='00-00-00-00-00-02'),
|
Interface(device=devices[1], name='Interface 2'),
|
||||||
)
|
)
|
||||||
Interface.objects.bulk_create(interfaces)
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
mac_addresses = (
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-01'),
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-02'),
|
||||||
|
)
|
||||||
|
MACAddress.objects.bulk_create(mac_addresses)
|
||||||
|
interfaces[0].mac_addresses.set([mac_addresses[0]])
|
||||||
|
interfaces[1].mac_addresses.set([mac_addresses[1]])
|
||||||
rear_ports = (
|
rear_ports = (
|
||||||
RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C),
|
RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C),
|
||||||
RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
|
RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
|
||||||
@ -3670,6 +3677,13 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
)
|
)
|
||||||
VirtualDeviceContext.objects.bulk_create(vdcs)
|
VirtualDeviceContext.objects.bulk_create(vdcs)
|
||||||
|
|
||||||
|
mac_addresses = (
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-01'),
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-02'),
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-03'),
|
||||||
|
)
|
||||||
|
MACAddress.objects.bulk_create(mac_addresses)
|
||||||
|
|
||||||
vlans = (
|
vlans = (
|
||||||
VLAN(name='SVLAN 1', vid=1001, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE),
|
VLAN(name='SVLAN 1', vid=1001, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE),
|
||||||
VLAN(name='SVLAN 2', vid=1002, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE),
|
VLAN(name='SVLAN 2', vid=1002, qinq_role=VLANQinQRoleChoices.ROLE_SERVICE),
|
||||||
@ -3695,7 +3709,6 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
mgmt_only=True,
|
mgmt_only=True,
|
||||||
mtu=100,
|
mtu=100,
|
||||||
mode=InterfaceModeChoices.MODE_ACCESS,
|
mode=InterfaceModeChoices.MODE_ACCESS,
|
||||||
mac_address='00-00-00-00-00-01',
|
|
||||||
description='First',
|
description='First',
|
||||||
vrf=vrfs[0],
|
vrf=vrfs[0],
|
||||||
speed=1000000,
|
speed=1000000,
|
||||||
@ -3721,7 +3734,6 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
mgmt_only=True,
|
mgmt_only=True,
|
||||||
mtu=200,
|
mtu=200,
|
||||||
mode=InterfaceModeChoices.MODE_TAGGED,
|
mode=InterfaceModeChoices.MODE_TAGGED,
|
||||||
mac_address='00-00-00-00-00-02',
|
|
||||||
description='Second',
|
description='Second',
|
||||||
vrf=vrfs[1],
|
vrf=vrfs[1],
|
||||||
speed=1000000,
|
speed=1000000,
|
||||||
@ -3740,7 +3752,6 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
mgmt_only=False,
|
mgmt_only=False,
|
||||||
mtu=300,
|
mtu=300,
|
||||||
mode=InterfaceModeChoices.MODE_TAGGED_ALL,
|
mode=InterfaceModeChoices.MODE_TAGGED_ALL,
|
||||||
mac_address='00-00-00-00-00-03',
|
|
||||||
description='Third',
|
description='Third',
|
||||||
vrf=vrfs[2],
|
vrf=vrfs[2],
|
||||||
speed=100000,
|
speed=100000,
|
||||||
@ -3814,6 +3825,10 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
interfaces[6].vdcs.set([vdcs[0]])
|
interfaces[6].vdcs.set([vdcs[0]])
|
||||||
interfaces[7].vdcs.set([vdcs[1]])
|
interfaces[7].vdcs.set([vdcs[1]])
|
||||||
|
|
||||||
|
interfaces[0].mac_addresses.set([mac_addresses[0]])
|
||||||
|
interfaces[2].mac_addresses.set([mac_addresses[1]])
|
||||||
|
interfaces[3].mac_addresses.set([mac_addresses[2]])
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save()
|
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save()
|
||||||
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save()
|
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save()
|
||||||
@ -5842,3 +5857,80 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'primary_ip6_id': [addresses[2].pk]}
|
params = {'primary_ip6_id': [addresses[2].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class MACAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
|
queryset = MACAddress.objects.all()
|
||||||
|
filterset = MACAddressFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
devices = (
|
||||||
|
create_test_device('Device 1'),
|
||||||
|
create_test_device('Device 2'),
|
||||||
|
create_test_device('Device 3'),
|
||||||
|
)
|
||||||
|
interfaces = (
|
||||||
|
Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
|
virtual_machines = (
|
||||||
|
create_test_virtualmachine('Virtual Machine 1'),
|
||||||
|
create_test_virtualmachine('Virtual Machine 2'),
|
||||||
|
create_test_virtualmachine('Virtual Machine 3'),
|
||||||
|
)
|
||||||
|
vm_interfaces = (
|
||||||
|
VMInterface(virtual_machine=virtual_machines[0], name='Interface 1'),
|
||||||
|
VMInterface(virtual_machine=virtual_machines[1], name='Interface 2'),
|
||||||
|
VMInterface(virtual_machine=virtual_machines[2], name='Interface 3'),
|
||||||
|
)
|
||||||
|
VMInterface.objects.bulk_create(vm_interfaces)
|
||||||
|
|
||||||
|
mac_addresses = (
|
||||||
|
# Device MACs
|
||||||
|
MACAddress(mac_address='00-00-00-01-01-01', assigned_object=interfaces[0]),
|
||||||
|
MACAddress(mac_address='00-00-00-02-01-01', assigned_object=interfaces[1]),
|
||||||
|
MACAddress(mac_address='00-00-00-03-01-01', assigned_object=interfaces[2]),
|
||||||
|
MACAddress(mac_address='00-00-00-03-01-02', assigned_object=interfaces[2]),
|
||||||
|
# VM MACs
|
||||||
|
MACAddress(mac_address='00-00-00-04-01-01', assigned_object=vm_interfaces[0]),
|
||||||
|
MACAddress(mac_address='00-00-00-05-01-01', assigned_object=vm_interfaces[1]),
|
||||||
|
MACAddress(mac_address='00-00-00-06-01-01', assigned_object=vm_interfaces[2]),
|
||||||
|
MACAddress(mac_address='00-00-00-06-01-02', assigned_object=vm_interfaces[2]),
|
||||||
|
)
|
||||||
|
MACAddress.objects.bulk_create(mac_addresses)
|
||||||
|
|
||||||
|
def test_mac_address(self):
|
||||||
|
params = {'mac_address': ['00-00-00-01-01-01', '00-00-00-02-01-01']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_device(self):
|
||||||
|
devices = Device.objects.all()[:2]
|
||||||
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'device': [devices[0].name, devices[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_virtual_machine(self):
|
||||||
|
virtual_machines = VirtualMachine.objects.all()[:2]
|
||||||
|
params = {'virtual_machine_id': [virtual_machines[0].pk, virtual_machines[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'virtual_machine': [virtual_machines[0].name, virtual_machines[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_interface(self):
|
||||||
|
interfaces = Interface.objects.all()[:2]
|
||||||
|
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'interface': [interfaces[0].name, interfaces[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_vminterface(self):
|
||||||
|
vm_interfaces = VMInterface.objects.all()[:2]
|
||||||
|
params = {'vminterface_id': [vm_interfaces[0].pk, vm_interfaces[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'vminterface': [vm_interfaces[0].name, vm_interfaces[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -2508,7 +2508,6 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
'enabled': False,
|
'enabled': False,
|
||||||
'bridge': interfaces[4].pk,
|
'bridge': interfaces[4].pk,
|
||||||
'lag': interfaces[3].pk,
|
'lag': interfaces[3].pk,
|
||||||
'mac_address': EUI('01:02:03:04:05:06'),
|
|
||||||
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
||||||
'mtu': 65000,
|
'mtu': 65000,
|
||||||
'speed': 1000000,
|
'speed': 1000000,
|
||||||
@ -2533,7 +2532,6 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
'enabled': False,
|
'enabled': False,
|
||||||
'bridge': interfaces[4].pk,
|
'bridge': interfaces[4].pk,
|
||||||
'lag': interfaces[3].pk,
|
'lag': interfaces[3].pk,
|
||||||
'mac_address': EUI('01:02:03:04:05:06'),
|
|
||||||
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
||||||
'mtu': 2000,
|
'mtu': 2000,
|
||||||
'speed': 100000,
|
'speed': 100000,
|
||||||
@ -2554,7 +2552,6 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
'type': InterfaceTypeChoices.TYPE_1GE_FIXED,
|
'type': InterfaceTypeChoices.TYPE_1GE_FIXED,
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
'lag': interfaces[3].pk,
|
'lag': interfaces[3].pk,
|
||||||
'mac_address': EUI('01:02:03:04:05:06'),
|
|
||||||
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
||||||
'mtu': 2000,
|
'mtu': 2000,
|
||||||
'speed': 1000000,
|
'speed': 1000000,
|
||||||
|
@ -165,4 +165,7 @@ urlpatterns = [
|
|||||||
path('power-feeds/', include(get_model_urls('dcim', 'powerfeed', detail=False))),
|
path('power-feeds/', include(get_model_urls('dcim', 'powerfeed', detail=False))),
|
||||||
path('power-feeds/<int:pk>/', include(get_model_urls('dcim', 'powerfeed'))),
|
path('power-feeds/<int:pk>/', include(get_model_urls('dcim', 'powerfeed'))),
|
||||||
|
|
||||||
|
path('mac-addresses/', include(get_model_urls('dcim', 'macaddress', detail=False))),
|
||||||
|
path('mac-addresses/<int:pk>/', include(get_model_urls('dcim', 'macaddress'))),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -2716,7 +2716,7 @@ class InterfaceView(generic.ObjectView):
|
|||||||
|
|
||||||
# Get bridge interfaces
|
# Get bridge interfaces
|
||||||
bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance)
|
bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance)
|
||||||
bridge_interfaces_tables = tables.InterfaceTable(
|
bridge_interfaces_table = tables.InterfaceTable(
|
||||||
bridge_interfaces,
|
bridge_interfaces,
|
||||||
exclude=('device', 'parent'),
|
exclude=('device', 'parent'),
|
||||||
orderable=False
|
orderable=False
|
||||||
@ -2724,7 +2724,7 @@ class InterfaceView(generic.ObjectView):
|
|||||||
|
|
||||||
# Get child interfaces
|
# Get child interfaces
|
||||||
child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance)
|
child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance)
|
||||||
child_interfaces_tables = tables.InterfaceTable(
|
child_interfaces_table = tables.InterfaceTable(
|
||||||
child_interfaces,
|
child_interfaces,
|
||||||
exclude=('device', 'parent'),
|
exclude=('device', 'parent'),
|
||||||
orderable=False
|
orderable=False
|
||||||
@ -2754,8 +2754,8 @@ class InterfaceView(generic.ObjectView):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'vdc_table': vdc_table,
|
'vdc_table': vdc_table,
|
||||||
'bridge_interfaces_table': bridge_interfaces_tables,
|
'bridge_interfaces_table': bridge_interfaces_table,
|
||||||
'child_interfaces_table': child_interfaces_tables,
|
'child_interfaces_table': child_interfaces_table,
|
||||||
'vlan_table': vlan_table,
|
'vlan_table': vlan_table,
|
||||||
'vlan_translation_table': vlan_translation_table,
|
'vlan_translation_table': vlan_translation_table,
|
||||||
}
|
}
|
||||||
@ -3999,3 +3999,53 @@ class VirtualDeviceContextBulkDeleteView(generic.BulkDeleteView):
|
|||||||
queryset = VirtualDeviceContext.objects.all()
|
queryset = VirtualDeviceContext.objects.all()
|
||||||
filterset = filtersets.VirtualDeviceContextFilterSet
|
filterset = filtersets.VirtualDeviceContextFilterSet
|
||||||
table = tables.VirtualDeviceContextTable
|
table = tables.VirtualDeviceContextTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# MAC addresses
|
||||||
|
#
|
||||||
|
|
||||||
|
@register_model_view(MACAddress, 'list', path='', detail=False)
|
||||||
|
class MACAddressListView(generic.ObjectListView):
|
||||||
|
queryset = MACAddress.objects.all()
|
||||||
|
filterset = filtersets.MACAddressFilterSet
|
||||||
|
filterset_form = forms.MACAddressFilterForm
|
||||||
|
table = tables.MACAddressTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(MACAddress)
|
||||||
|
class MACAddressView(generic.ObjectView):
|
||||||
|
queryset = MACAddress.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(MACAddress, 'add', detail=False)
|
||||||
|
@register_model_view(MACAddress, 'edit')
|
||||||
|
class MACAddressEditView(generic.ObjectEditView):
|
||||||
|
queryset = MACAddress.objects.all()
|
||||||
|
form = forms.MACAddressForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(MACAddress, 'delete')
|
||||||
|
class MACAddressDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = MACAddress.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(MACAddress, 'import', detail=False)
|
||||||
|
class MACAddressBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = MACAddress.objects.all()
|
||||||
|
model_form = forms.MACAddressImportForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(MACAddress, 'bulk_edit', path='edit', detail=False)
|
||||||
|
class MACAddressBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = MACAddress.objects.all()
|
||||||
|
filterset = filtersets.MACAddressFilterSet
|
||||||
|
table = tables.MACAddressTable
|
||||||
|
form = forms.MACAddressBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(MACAddress, 'bulk_delete', path='delete', detail=False)
|
||||||
|
class MACAddressBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = MACAddress.objects.all()
|
||||||
|
filterset = filtersets.MACAddressFilterSet
|
||||||
|
table = tables.MACAddressTable
|
||||||
|
@ -1135,6 +1135,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
'l2vpn',
|
'l2vpn',
|
||||||
'l2vpntermination',
|
'l2vpntermination',
|
||||||
'location',
|
'location',
|
||||||
|
'macaddress',
|
||||||
'manufacturer',
|
'manufacturer',
|
||||||
'module',
|
'module',
|
||||||
'modulebay',
|
'modulebay',
|
||||||
@ -1167,6 +1168,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
'tunnelgroup',
|
'tunnelgroup',
|
||||||
'tunneltermination',
|
'tunneltermination',
|
||||||
'virtualchassis',
|
'virtualchassis',
|
||||||
|
'virtualcircuit',
|
||||||
|
'virtualcircuittermination',
|
||||||
'virtualdevicecontext',
|
'virtualdevicecontext',
|
||||||
'virtualdisk',
|
'virtualdisk',
|
||||||
'virtualmachine',
|
'virtualmachine',
|
||||||
|
@ -109,7 +109,8 @@ class RIRForm(NetBoxModelForm):
|
|||||||
class AggregateForm(TenancyForm, NetBoxModelForm):
|
class AggregateForm(TenancyForm, NetBoxModelForm):
|
||||||
rir = DynamicModelChoiceField(
|
rir = DynamicModelChoiceField(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
label=_('RIR')
|
label=_('RIR'),
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
@ -132,6 +133,7 @@ class ASNRangeForm(TenancyForm, NetBoxModelForm):
|
|||||||
rir = DynamicModelChoiceField(
|
rir = DynamicModelChoiceField(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
label=_('RIR'),
|
label=_('RIR'),
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@ -150,6 +152,7 @@ class ASNForm(TenancyForm, NetBoxModelForm):
|
|||||||
rir = DynamicModelChoiceField(
|
rir = DynamicModelChoiceField(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
label=_('RIR'),
|
label=_('RIR'),
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
sites = DynamicModelMultipleChoiceField(
|
sites = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
@ -216,7 +219,8 @@ class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm):
|
|||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
label=_('Role'),
|
label=_('Role'),
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
@ -246,7 +250,8 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
|
|||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
label=_('Role'),
|
label=_('Role'),
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
@ -639,7 +644,8 @@ class VLANForm(TenancyForm, NetBoxModelForm):
|
|||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
label=_('Role'),
|
label=_('Role'),
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
qinq_svlan = DynamicModelChoiceField(
|
qinq_svlan = DynamicModelChoiceField(
|
||||||
label=_('Q-in-Q SVLAN'),
|
label=_('Q-in-Q SVLAN'),
|
||||||
|
@ -179,6 +179,8 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
# The filter field has been explicitly defined on the filterset class so we must manually
|
# The filter field has been explicitly defined on the filterset class so we must manually
|
||||||
# create the new filter with the same type because there is no guarantee the defined type
|
# create the new filter with the same type because there is no guarantee the defined type
|
||||||
# is the same as the default type for the field
|
# is the same as the default type for the field
|
||||||
|
if field is None:
|
||||||
|
raise ValueError('Invalid field name/lookup on {}: {}'.format(existing_filter_name, field_name))
|
||||||
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
||||||
filter_cls = type(existing_filter)
|
filter_cls = type(existing_filter)
|
||||||
if lookup_expr == 'empty':
|
if lookup_expr == 'empty':
|
||||||
|
@ -88,6 +88,12 @@ DEVICES_MENU = Menu(
|
|||||||
get_model_item('dcim', 'manufacturer', _('Manufacturers')),
|
get_model_item('dcim', 'manufacturer', _('Manufacturers')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
MenuGroup(
|
||||||
|
label=_('Addressing'),
|
||||||
|
items=(
|
||||||
|
get_model_item('dcim', 'macaddress', _('MAC Addresses')),
|
||||||
|
),
|
||||||
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
label=_('Device Components'),
|
label=_('Device Components'),
|
||||||
items=(
|
items=(
|
||||||
@ -278,6 +284,13 @@ CIRCUITS_MENU = Menu(
|
|||||||
get_model_item('circuits', 'circuittermination', _('Circuit Terminations')),
|
get_model_item('circuits', 'circuittermination', _('Circuit Terminations')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
MenuGroup(
|
||||||
|
label=_('Virtual Circuits'),
|
||||||
|
items=(
|
||||||
|
get_model_item('circuits', 'virtualcircuit', _('Virtual Circuits')),
|
||||||
|
get_model_item('circuits', 'virtualcircuittermination', _('Virtual Circuit Terminations')),
|
||||||
|
),
|
||||||
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
label=_('Providers'),
|
label=_('Providers'),
|
||||||
items=(
|
items=(
|
||||||
|
@ -233,18 +233,23 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
form = self.form(instance=obj, initial=initial_data)
|
form = self.form(instance=obj, initial=initial_data)
|
||||||
restrict_form_fields(form, request.user)
|
restrict_form_fields(form, request.user)
|
||||||
|
|
||||||
# If this is an HTMX request, return only the rendered form HTML
|
context = {
|
||||||
if htmx_partial(request):
|
|
||||||
return render(request, self.htmx_template_name, {
|
|
||||||
'model': model,
|
|
||||||
'object': obj,
|
|
||||||
'form': form,
|
|
||||||
})
|
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
|
||||||
'model': model,
|
'model': model,
|
||||||
'object': obj,
|
'object': obj,
|
||||||
'form': form,
|
'form': form,
|
||||||
|
}
|
||||||
|
|
||||||
|
# If the form is being displayed within a "quick add" widget,
|
||||||
|
# use the appropriate template
|
||||||
|
if request.GET.get('_quickadd'):
|
||||||
|
return render(request, 'htmx/quick_add.html', context)
|
||||||
|
|
||||||
|
# If this is an HTMX request, return only the rendered form HTML
|
||||||
|
if htmx_partial(request):
|
||||||
|
return render(request, self.htmx_template_name, context)
|
||||||
|
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
**context,
|
||||||
'return_url': self.get_return_url(request, obj),
|
'return_url': self.get_return_url(request, obj),
|
||||||
'prerequisite_model': get_prerequisite_model(self.queryset),
|
'prerequisite_model': get_prerequisite_model(self.queryset),
|
||||||
**self.get_extra_context(request, obj),
|
**self.get_extra_context(request, obj),
|
||||||
@ -259,6 +264,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
"""
|
"""
|
||||||
logger = logging.getLogger('netbox.views.ObjectEditView')
|
logger = logging.getLogger('netbox.views.ObjectEditView')
|
||||||
obj = self.get_object(**kwargs)
|
obj = self.get_object(**kwargs)
|
||||||
|
model = self.queryset.model
|
||||||
|
|
||||||
# Take a snapshot for change logging (if editing an existing object)
|
# Take a snapshot for change logging (if editing an existing object)
|
||||||
if obj.pk and hasattr(obj, 'snapshot'):
|
if obj.pk and hasattr(obj, 'snapshot'):
|
||||||
@ -292,6 +298,12 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
msg = f'{msg} {obj}'
|
msg = f'{msg} {obj}'
|
||||||
messages.success(request, msg)
|
messages.success(request, msg)
|
||||||
|
|
||||||
|
# Object was created via "quick add" modal
|
||||||
|
if '_quickadd' in request.POST:
|
||||||
|
return render(request, 'htmx/quick_add_created.html', {
|
||||||
|
'object': obj,
|
||||||
|
})
|
||||||
|
|
||||||
# If adding another object, redirect back to the edit form
|
# If adding another object, redirect back to the edit form
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
redirect_url = request.path
|
redirect_url = request.path
|
||||||
@ -324,12 +336,19 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
else:
|
else:
|
||||||
logger.debug("Form validation failed")
|
logger.debug("Form validation failed")
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
context = {
|
||||||
|
'model': model,
|
||||||
'object': obj,
|
'object': obj,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': self.get_return_url(request, obj),
|
'return_url': self.get_return_url(request, obj),
|
||||||
**self.get_extra_context(request, obj),
|
**self.get_extra_context(request, obj),
|
||||||
})
|
}
|
||||||
|
|
||||||
|
# Form was submitted via a "quick add" widget
|
||||||
|
if '_quickadd' in request.POST:
|
||||||
|
return render(request, 'htmx/quick_add.html', context)
|
||||||
|
|
||||||
|
return render(request, self.template_name, context)
|
||||||
|
|
||||||
|
|
||||||
class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
|
class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
|
||||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -1,3 +1,5 @@
|
|||||||
|
import { getElements } from '../util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a slug from any input string.
|
* Create a slug from any input string.
|
||||||
*
|
*
|
||||||
@ -15,34 +17,30 @@ function slugify(slug: string, chars: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a slug field exists, add event listeners to handle automatically generating its value.
|
* For any slug fields, add event listeners to handle automatically generating slug values.
|
||||||
*/
|
*/
|
||||||
export function initReslug(): void {
|
export function initReslug(): void {
|
||||||
const slugField = document.getElementById('id_slug') as HTMLInputElement;
|
for (const slugButton of getElements<HTMLButtonElement>('button#reslug')) {
|
||||||
const slugButton = document.getElementById('reslug') as HTMLButtonElement;
|
const form = slugButton.form;
|
||||||
if (slugField === null || slugButton === null) {
|
if (form == null) continue;
|
||||||
return;
|
const slugField = form.querySelector('#id_slug') as HTMLInputElement;
|
||||||
}
|
if (slugField == null) continue;
|
||||||
const sourceId = slugField.getAttribute('slug-source');
|
const sourceId = slugField.getAttribute('slug-source');
|
||||||
const sourceField = document.getElementById(`id_${sourceId}`) as HTMLInputElement;
|
const sourceField = form.querySelector(`#id_${sourceId}`) as HTMLInputElement;
|
||||||
|
|
||||||
if (sourceField === null) {
|
const slugLengthAttr = slugField.getAttribute('maxlength');
|
||||||
console.error('Unable to find field for slug field.');
|
let slugLength = 50;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const slugLengthAttr = slugField.getAttribute('maxlength');
|
if (slugLengthAttr) {
|
||||||
let slugLength = 50;
|
slugLength = Number(slugLengthAttr);
|
||||||
|
|
||||||
if (slugLengthAttr) {
|
|
||||||
slugLength = Number(slugLengthAttr);
|
|
||||||
}
|
|
||||||
sourceField.addEventListener('blur', () => {
|
|
||||||
if (!slugField.value) {
|
|
||||||
slugField.value = slugify(sourceField.value, slugLength);
|
|
||||||
}
|
}
|
||||||
});
|
sourceField.addEventListener('blur', () => {
|
||||||
slugButton.addEventListener('click', () => {
|
if (!slugField.value) {
|
||||||
slugField.value = slugify(sourceField.value, slugLength);
|
slugField.value = slugify(sourceField.value, slugLength);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
slugButton.addEventListener('click', () => {
|
||||||
|
slugField.value = slugify(sourceField.value, slugLength);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,16 @@ import { initSelects } from './select';
|
|||||||
import { initObjectSelector } from './objectSelector';
|
import { initObjectSelector } from './objectSelector';
|
||||||
import { initBootstrap } from './bs';
|
import { initBootstrap } from './bs';
|
||||||
import { initMessages } from './messages';
|
import { initMessages } from './messages';
|
||||||
|
import { initQuickAdd } from './quickAdd';
|
||||||
|
|
||||||
function initDepedencies(): void {
|
function initDepedencies(): void {
|
||||||
for (const init of [initButtons, initClipboard, initSelects, initObjectSelector, initBootstrap, initMessages]) {
|
initButtons();
|
||||||
init();
|
initClipboard();
|
||||||
}
|
initSelects();
|
||||||
|
initObjectSelector();
|
||||||
|
initQuickAdd();
|
||||||
|
initBootstrap();
|
||||||
|
initMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
39
netbox/project-static/src/quickAdd.ts
Normal file
39
netbox/project-static/src/quickAdd.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Modal } from 'bootstrap';
|
||||||
|
|
||||||
|
function handleQuickAddObject(): void {
|
||||||
|
const quick_add = document.getElementById('quick-add-object');
|
||||||
|
if (quick_add == null) return;
|
||||||
|
|
||||||
|
const object_id = quick_add.getAttribute('data-object-id');
|
||||||
|
if (object_id == null) return;
|
||||||
|
const object_repr = quick_add.getAttribute('data-object-repr');
|
||||||
|
if (object_repr == null) return;
|
||||||
|
|
||||||
|
const target_id = quick_add.getAttribute('data-target-id');
|
||||||
|
if (target_id == null) return;
|
||||||
|
const target = document.getElementById(target_id);
|
||||||
|
if (target == null) return;
|
||||||
|
|
||||||
|
//@ts-expect-error tomselect added on init
|
||||||
|
target.tomselect.addOption({
|
||||||
|
id: object_id,
|
||||||
|
display: object_repr,
|
||||||
|
});
|
||||||
|
//@ts-expect-error tomselect added on init
|
||||||
|
target.tomselect.addItem(object_id);
|
||||||
|
|
||||||
|
const modal_element = document.getElementById('htmx-modal');
|
||||||
|
if (modal_element) {
|
||||||
|
const modal = Modal.getInstance(modal_element);
|
||||||
|
if (modal) {
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initQuickAdd(): void {
|
||||||
|
const quick_add_modal = document.getElementById('htmx-modal-content');
|
||||||
|
if (quick_add_modal) {
|
||||||
|
quick_add_modal.addEventListener('htmx:afterSwap', () => handleQuickAddObject());
|
||||||
|
}
|
||||||
|
}
|
@ -50,6 +50,19 @@
|
|||||||
<h2 class="card-header">{% trans "Circuits" %}</h2>
|
<h2 class="card-header">{% trans "Circuits" %}</h2>
|
||||||
{% htmx_table 'circuits:circuit_list' provider_network_id=object.pk %}
|
{% htmx_table 'circuits:circuit_list' provider_network_id=object.pk %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">
|
||||||
|
{% trans "Virtual Circuits" %}
|
||||||
|
{% if perms.circuits.add_virtualcircuit %}
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="{% url 'circuits:virtualcircuit_add' %}?provider_network={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Virtual Circuit" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
{% htmx_table 'circuits:virtualcircuit_list' provider_network_id=object.pk %}
|
||||||
|
</div>
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
84
netbox/templates/circuits/virtualcircuit.html
Normal file
84
netbox/templates/circuits/virtualcircuit.html
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'circuits:virtualcircuit_list' %}?provider_id={{ object.provider.pk }}">{{ object.provider }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'circuits:virtualcircuit_list' %}?provider_network_id={{ object.provider_network.pk }}">{{ object.provider_network }}</a>
|
||||||
|
</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">{% trans "Virtual circuit" %}</h2>
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider" %}</th>
|
||||||
|
<td>{{ object.provider|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider Network" %}</th>
|
||||||
|
<td>{{ object.provider_network|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider account" %}</th>
|
||||||
|
<td>{{ object.provider_account|linkify|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Circuit ID" %}</th>
|
||||||
|
<td>{{ object.cid }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Status" %}</th>
|
||||||
|
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Tenant" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if object.tenant.group %}
|
||||||
|
{{ object.tenant.group|linkify }} /
|
||||||
|
{% endif %}
|
||||||
|
{{ object.tenant|linkify|placeholder }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% include 'inc/panels/comments.html' %}
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">
|
||||||
|
{% trans "Terminations" %}
|
||||||
|
{% if perms.circuits.add_virtualcircuittermination %}
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="{% url 'circuits:virtualcircuittermination_add' %}?virtual_circuit={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||||
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Termination" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
{% htmx_table 'circuits:virtualcircuittermination_list' virtual_circuit_id=object.pk %}
|
||||||
|
</div>
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
81
netbox/templates/circuits/virtualcircuittermination.html
Normal file
81
netbox/templates/circuits/virtualcircuittermination.html
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'circuits:virtualcircuit_list' %}?provider_id={{ object.virtual_circuit.provider.pk }}">{{ object.virtual_circuit.provider }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'circuits:virtualcircuit_list' %}?provider_network_id={{ object.virtual_circuit.provider_network.pk }}">{{ object.virtual_circuit.provider_network }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'circuits:virtualcircuittermination_list' %}?virtual_circuit_id={{ object.virtual_circuit.pk }}">{{ object.virtual_circuit }}</a>
|
||||||
|
</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">{% trans "Virtual Circuit Termination" %}</h2>
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider" %}</th>
|
||||||
|
<td>{{ object.virtual_circuit.provider|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider Network" %}</th>
|
||||||
|
<td>{{ object.virtual_circuit.provider_network|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider account" %}</th>
|
||||||
|
<td>{{ object.virtual_circuit.provider_account|linkify|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Virtual circuit" %}</th>
|
||||||
|
<td>{{ object.virtual_circuit|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Role" %}</th>
|
||||||
|
<td>{% badge object.get_role_display bg_color=object.get_role_color %}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">{% trans "Interface" %}</h2>
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Device" %}</th>
|
||||||
|
<td>{{ object.interface.device|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Interface" %}</th>
|
||||||
|
<td>{{ object.interface|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Type" %}</th>
|
||||||
|
<td>{{ object.interface.get_type_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
|
<td>{{ object.interface.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -123,11 +123,24 @@
|
|||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "MAC Address" %}</th>
|
<th scope="row">{% trans "MAC Address" %}</th>
|
||||||
<td><span class="font-monospace">{{ object.mac_address|placeholder }}</span></td>
|
<td>
|
||||||
|
{% if object.mac_address %}
|
||||||
|
<span class="font-monospace">{{ object.mac_address }}</span>
|
||||||
|
<span class="badge text-bg-primary">{% trans "Primary" %}</span>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "WWN" %}</th>
|
<th scope="row">{% trans "WWN" %}</th>
|
||||||
<td><span class="font-monospace">{{ object.wwn|placeholder }}</span></td>
|
<td>
|
||||||
|
{% if object.wwn %}
|
||||||
|
<span class="font-monospace">{{ object.wwn }}</span>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "VRF" %}</th>
|
<th scope="row">{% trans "VRF" %}</th>
|
||||||
@ -139,7 +152,41 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if not object.is_virtual %}
|
{% if object.is_virtual and object.virtual_circuit_termination %}
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">{% trans "Virtual Circuit" %}</h2>
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider" %}</th>
|
||||||
|
<td>{{ object.virtual_circuit_termination.virtual_circuit.provider|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider Network" %}</th>
|
||||||
|
<td>{{ object.virtual_circuit_termination.virtual_circuit.provider_network|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Circuit ID" %}</th>
|
||||||
|
<td>{{ object.virtual_circuit_termination.virtual_circuit|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Role" %}</th>
|
||||||
|
<td>{{ object.virtual_circuit_termination.get_role_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Connections" %}</th>
|
||||||
|
<td>
|
||||||
|
{% for termination in object.virtual_circuit_termination.peer_terminations %}
|
||||||
|
<a href="{{ termination.interface.parent_object.get_absolute_url }}">{{ termination.interface.parent_object }}</a>
|
||||||
|
<i class="mdi mdi-chevron-right"></i>
|
||||||
|
<a href="{{ termination.interface.get_absolute_url }}">{{ termination.interface }}</a>
|
||||||
|
({{ termination.get_role_display }})
|
||||||
|
{% if not forloop.last %}<br />{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% elif not object.is_virtual %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="card-header">{% trans "Connection" %}</h2>
|
<h2 class="card-header">{% trans "Connection" %}</h2>
|
||||||
{% if object.mark_connected %}
|
{% if object.mark_connected %}
|
||||||
@ -350,7 +397,23 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
{% htmx_table 'ipam:ipaddress_list' interface_id=object.pk %}
|
{% htmx_table 'ipam:ipaddress_list' interface_id=object.pk %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">
|
||||||
|
{% trans "MAC Addresses" %}
|
||||||
|
{% if perms.dcim.add_macaddress %}
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="{% url 'dcim:macaddress_add' %}?device={{ object.device.pk }}&interface={{ 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 MAC Address" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
{% htmx_table 'dcim:macaddress_list' interface_id=object.pk %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
55
netbox/templates/dcim/macaddress.html
Normal file
55
netbox/templates/dcim/macaddress.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-6">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">{% trans "MAC Address" %}</h2>
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "MAC Address" %}</th>
|
||||||
|
<td>
|
||||||
|
<span id="macaddress_{{ object.pk }}">{{ object.mac_address|placeholder }}</span>
|
||||||
|
{% copy_content object.pk prefix="macaddress_" %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Assignment" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if object.assigned_object %}
|
||||||
|
{{ object.assigned_object.parent_object|linkify }} /
|
||||||
|
{{ object.assigned_object|linkify }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Primary for interface" %}</th>
|
||||||
|
<td>{% checkmark object.is_primary %}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
{% 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 %}
|
28
netbox/templates/htmx/quick_add.html
Normal file
28
netbox/templates/htmx/quick_add.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% load form_helpers %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title">
|
||||||
|
{% trans "Quick Add" %} {{ model|meta:"verbose_name"|bettertitle }}
|
||||||
|
</h2>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body row">
|
||||||
|
<form
|
||||||
|
hx-post="{% url model|viewname:"add" %}?_quickadd=True&target={{ request.GET.target }}"
|
||||||
|
hx-target="#htmx-modal-content"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% include 'htmx/form.html' %}
|
||||||
|
<div class="text-end">
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-float" data-bs-dismiss="modal" aria-label="Cancel">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</button>
|
||||||
|
<button type="submit" name="_quickadd" class="btn btn-primary">
|
||||||
|
{% trans "Create" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
22
netbox/templates/htmx/quick_add_created.html
Normal file
22
netbox/templates/htmx/quick_add_created.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% load form_helpers %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title">
|
||||||
|
{{ object|meta:"verbose_name"|bettertitle }} {% trans "Created" %}
|
||||||
|
</h2>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body row">
|
||||||
|
{# This content is intended to be scraped and populated in the targeted selection field. #}
|
||||||
|
<p id="quick-add-object"
|
||||||
|
data-object-repr="{{ object }}"
|
||||||
|
data-object-id="{{ object.pk }}"
|
||||||
|
data-target-id="{{ request.GET.target }}"
|
||||||
|
>
|
||||||
|
{% blocktrans with object=object|linkify object_type=object|meta:"verbose_name" %}
|
||||||
|
Created {{ object_type }} {{ object }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
@ -14,73 +14,85 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="card-header">{% trans "Interface" %}</h2>
|
<h2 class="card-header">{% trans "Interface" %}</h2>
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Virtual Machine" %}</th>
|
<th scope="row">{% trans "Virtual Machine" %}</th>
|
||||||
<td>{{ object.virtual_machine|linkify }}</td>
|
<td>{{ object.virtual_machine|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Name" %}</th>
|
<th scope="row">{% trans "Name" %}</th>
|
||||||
<td>{{ object.name }}</td>
|
<td>{{ object.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Enabled" %}</th>
|
<th scope="row">{% trans "Enabled" %}</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.enabled %}
|
{% if object.enabled %}
|
||||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-danger"><i class="mdi mdi-close"></i></span>
|
<span class="text-danger"><i class="mdi mdi-close"></i></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Parent" %}</th>
|
<th scope="row">{% trans "Parent" %}</th>
|
||||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Bridge" %}</th>
|
<th scope="row">{% trans "Bridge" %}</th>
|
||||||
<td>{{ object.bridge|linkify|placeholder }}</td>
|
<td>{{ object.bridge|linkify|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "VRF" %}</th>
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
<td>{{ object.vrf|linkify|placeholder }}</td>
|
<td>{{ object.description|placeholder }} </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Description" %}</th>
|
<th scope="row">{% trans "MTU" %}</th>
|
||||||
<td>{{ object.description|placeholder }} </td>
|
<td>{{ object.mtu|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "MTU" %}</th>
|
<th scope="row">{% trans "802.1Q Mode" %}</th>
|
||||||
<td>{{ object.mtu|placeholder }}</td>
|
<td>{{ object.get_mode_display|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "MAC Address" %}</th>
|
<th scope="row">{% trans "Tunnel" %}</th>
|
||||||
<td><span class="font-monospace">{{ object.mac_address|placeholder }}</span></td>
|
<td>{{ object.tunnel_termination.tunnel|linkify|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
</table>
|
||||||
<th scope="row">{% trans "802.1Q Mode" %}</th>
|
|
||||||
<td>{{ object.get_mode_display|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<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' %}
|
|
||||||
{% plugin_left_page object %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% plugin_left_page object %}
|
||||||
{% include 'ipam/inc/panels/fhrp_groups.html' %}
|
</div>
|
||||||
{% plugin_right_page object %}
|
<div class="col col-md-6">
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">{% trans "Addressing" %}</h2>
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "MAC Address" %}</th>
|
||||||
|
<td>
|
||||||
|
{% if object.mac_address %}
|
||||||
|
<span class="font-monospace">{{ object.mac_address }}</span>
|
||||||
|
<span class="badge text-bg-primary">{% trans "Primary" %}</span>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<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>
|
</div>
|
||||||
|
{% include 'ipam/inc/panels/fhrp_groups.html' %}
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
@ -99,6 +111,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">
|
||||||
|
{% trans "MAC Addresses" %}
|
||||||
|
{% if perms.ipam.add_macaddress %}
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="{% url 'dcim:macaddress_add' %}?virtual_machine={{ object.device.pk }}&vminterface={{ 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 MAC Address" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
{% htmx_table 'dcim:macaddress_list' vminterface_id=object.pk %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||||
|
@ -25,6 +25,7 @@ class TenancyForm(forms.Form):
|
|||||||
label=_('Tenant'),
|
label=_('Tenant'),
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
quick_add=True,
|
||||||
query_params={
|
query_params={
|
||||||
'group_id': '$tenant_group'
|
'group_id': '$tenant_group'
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import django_filters
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.forms import BoundField
|
from django.forms import BoundField
|
||||||
from django.urls import reverse
|
from django.urls import reverse, reverse_lazy
|
||||||
|
|
||||||
from utilities.forms import widgets
|
from utilities.forms import widgets
|
||||||
from utilities.views import get_viewname
|
from utilities.views import get_viewname
|
||||||
@ -66,6 +66,8 @@ class DynamicModelChoiceMixin:
|
|||||||
choice (DEPRECATED: pass `context={'disabled': '$fieldname'}` instead)
|
choice (DEPRECATED: pass `context={'disabled': '$fieldname'}` instead)
|
||||||
context: A mapping of <option> template variables to their API data keys (optional; see below)
|
context: A mapping of <option> template variables to their API data keys (optional; see below)
|
||||||
selector: Include an advanced object selection widget to assist the user in identifying the desired object
|
selector: Include an advanced object selection widget to assist the user in identifying the desired object
|
||||||
|
quick_add: Include a widget to quickly create a new related object for assignment. NOTE: Nested usage of
|
||||||
|
quick-add fields is not currently supported.
|
||||||
|
|
||||||
Context keys:
|
Context keys:
|
||||||
value: The name of the attribute which contains the option's value (default: 'id')
|
value: The name of the attribute which contains the option's value (default: 'id')
|
||||||
@ -90,6 +92,7 @@ class DynamicModelChoiceMixin:
|
|||||||
disabled_indicator=None,
|
disabled_indicator=None,
|
||||||
context=None,
|
context=None,
|
||||||
selector=False,
|
selector=False,
|
||||||
|
quick_add=False,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
self.model = queryset.model
|
self.model = queryset.model
|
||||||
@ -99,6 +102,7 @@ class DynamicModelChoiceMixin:
|
|||||||
self.disabled_indicator = disabled_indicator
|
self.disabled_indicator = disabled_indicator
|
||||||
self.context = context or {}
|
self.context = context or {}
|
||||||
self.selector = selector
|
self.selector = selector
|
||||||
|
self.quick_add = quick_add
|
||||||
|
|
||||||
super().__init__(queryset, **kwargs)
|
super().__init__(queryset, **kwargs)
|
||||||
|
|
||||||
@ -121,6 +125,12 @@ class DynamicModelChoiceMixin:
|
|||||||
if self.selector:
|
if self.selector:
|
||||||
attrs['selector'] = self.model._meta.label_lower
|
attrs['selector'] = self.model._meta.label_lower
|
||||||
|
|
||||||
|
# Include quick add?
|
||||||
|
if self.quick_add:
|
||||||
|
app_label = self.model._meta.app_label
|
||||||
|
model_name = self.model._meta.model_name
|
||||||
|
attrs['quick_add'] = reverse_lazy(f'{app_label}:{model_name}_add')
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def get_bound_field(self, form, field_name):
|
def get_bound_field(self, form, field_name):
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% if widget.attrs.selector and not widget.attrs.disabled %}
|
<div class="d-flex">
|
||||||
<div class="d-flex">
|
{% include 'django/forms/widgets/select.html' %}
|
||||||
{% include 'django/forms/widgets/select.html' %}
|
{% if widget.attrs.selector and not widget.attrs.disabled %}
|
||||||
|
{# Opens the object selector modal #}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="{% trans "Open selector" %}"
|
title="{% trans "Open selector" %}"
|
||||||
@ -13,7 +14,19 @@
|
|||||||
>
|
>
|
||||||
<i class="mdi mdi-database-search-outline"></i>
|
<i class="mdi mdi-database-search-outline"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{% endif %}
|
||||||
{% else %}
|
{% if widget.attrs.quick_add and not widget.attrs.disabled %}
|
||||||
{% include 'django/forms/widgets/select.html' %}
|
{# Opens the quick add modal #}
|
||||||
{% endif %}
|
<button
|
||||||
|
type="button"
|
||||||
|
title="{% trans "Quick add" %}"
|
||||||
|
class="btn btn-outline-secondary ms-1"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#htmx-modal"
|
||||||
|
hx-get="{{ widget.attrs.quick_add }}?_quickadd=True&target={{ widget.attrs.id }}"
|
||||||
|
hx-target="#htmx-modal-content"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-plus-circle"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
@ -9,7 +9,7 @@ from dcim.choices import *
|
|||||||
from dcim.fields import MACAddressField
|
from dcim.fields import MACAddressField
|
||||||
from dcim.filtersets import DeviceFilterSet, SiteFilterSet, InterfaceFilterSet
|
from dcim.filtersets import DeviceFilterSet, SiteFilterSet, InterfaceFilterSet
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
Device, DeviceRole, DeviceType, Interface, Manufacturer, Platform, Rack, Region, Site
|
Device, DeviceRole, DeviceType, Interface, MACAddress, Manufacturer, Platform, Rack, Region, Site
|
||||||
)
|
)
|
||||||
from extras.filters import TagFilter
|
from extras.filters import TagFilter
|
||||||
from extras.models import TaggedItem
|
from extras.models import TaggedItem
|
||||||
@ -433,16 +433,33 @@ class DynamicFilterLookupExpressionTest(TestCase):
|
|||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
|
mac_addresses = (
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-01'),
|
||||||
|
MACAddress(mac_address='aa-00-00-00-00-01'),
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-02'),
|
||||||
|
MACAddress(mac_address='bb-00-00-00-00-02'),
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-03'),
|
||||||
|
MACAddress(mac_address='cc-00-00-00-00-03'),
|
||||||
|
)
|
||||||
|
MACAddress.objects.bulk_create(mac_addresses)
|
||||||
|
|
||||||
interfaces = (
|
interfaces = (
|
||||||
Interface(device=devices[0], name='Interface 1', mac_address='00-00-00-00-00-01'),
|
Interface(device=devices[0], name='Interface 1'),
|
||||||
Interface(device=devices[0], name='Interface 2', mac_address='aa-00-00-00-00-01'),
|
Interface(device=devices[0], name='Interface 2'),
|
||||||
Interface(device=devices[1], name='Interface 3', mac_address='00-00-00-00-00-02'),
|
Interface(device=devices[1], name='Interface 3'),
|
||||||
Interface(device=devices[1], name='Interface 4', mac_address='bb-00-00-00-00-02'),
|
Interface(device=devices[1], name='Interface 4'),
|
||||||
Interface(device=devices[2], name='Interface 5', mac_address='00-00-00-00-00-03'),
|
Interface(device=devices[2], name='Interface 5'),
|
||||||
Interface(device=devices[2], name='Interface 6', mac_address='cc-00-00-00-00-03', rf_role=WirelessRoleChoices.ROLE_AP),
|
Interface(device=devices[2], name='Interface 6', rf_role=WirelessRoleChoices.ROLE_AP),
|
||||||
)
|
)
|
||||||
Interface.objects.bulk_create(interfaces)
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
|
interfaces[0].mac_addresses.set([mac_addresses[0]])
|
||||||
|
interfaces[1].mac_addresses.set([mac_addresses[1]])
|
||||||
|
interfaces[2].mac_addresses.set([mac_addresses[2]])
|
||||||
|
interfaces[3].mac_addresses.set([mac_addresses[3]])
|
||||||
|
interfaces[4].mac_addresses.set([mac_addresses[4]])
|
||||||
|
interfaces[5].mac_addresses.set([mac_addresses[5]])
|
||||||
|
|
||||||
def test_site_name_negation(self):
|
def test_site_name_negation(self):
|
||||||
params = {'name__n': ['Site 1']}
|
params = {'name__n': ['Site 1']}
|
||||||
self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2)
|
self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2)
|
||||||
|
@ -2,6 +2,7 @@ from drf_spectacular.utils import extend_schema_field
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.api.serializers_.devices import DeviceSerializer
|
from dcim.api.serializers_.devices import DeviceSerializer
|
||||||
|
from dcim.api.serializers_.device_components import MACAddressSerializer
|
||||||
from dcim.api.serializers_.platforms import PlatformSerializer
|
from dcim.api.serializers_.platforms import PlatformSerializer
|
||||||
from dcim.api.serializers_.roles import DeviceRoleSerializer
|
from dcim.api.serializers_.roles import DeviceRoleSerializer
|
||||||
from dcim.api.serializers_.sites import SiteSerializer
|
from dcim.api.serializers_.sites import SiteSerializer
|
||||||
@ -95,19 +96,18 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
|||||||
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
|
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
|
||||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||||
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
||||||
mac_address = serializers.CharField(
|
# Maintains backward compatibility with NetBox <v4.2
|
||||||
required=False,
|
mac_address = serializers.CharField(allow_null=True, read_only=True)
|
||||||
default=None,
|
primary_mac_address = MACAddressSerializer(nested=True, required=False, allow_null=True)
|
||||||
allow_null=True
|
mac_addresses = MACAddressSerializer(many=True, nested=True, read_only=True, allow_null=True)
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu',
|
'id', 'url', 'display_url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu',
|
||||||
'mac_address', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
'mac_address', 'primary_mac_address', 'mac_addresses', 'description', 'mode', 'untagged_vlan',
|
||||||
'vlan_translation_policy', 'vrf', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated',
|
'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'l2vpn_termination', 'tags',
|
||||||
'count_ipaddresses', 'count_fhrp_groups',
|
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
|
||||||
|
|
||||||
|
@ -2,9 +2,10 @@ import django_filters
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.filtersets import CommonInterfaceFilterSet
|
|
||||||
from dcim.base_filtersets import ScopedFilterSet
|
from dcim.base_filtersets import ScopedFilterSet
|
||||||
|
from dcim.filtersets import CommonInterfaceFilterSet
|
||||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
|
from dcim.models import MACAddress
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.filtersets import PrimaryIPFilterSet
|
from ipam.filtersets import PrimaryIPFilterSet
|
||||||
@ -191,7 +192,7 @@ class VirtualMachineFilterSet(
|
|||||||
label=_('Platform (slug)'),
|
label=_('Platform (slug)'),
|
||||||
)
|
)
|
||||||
mac_address = MultiValueMACAddressFilter(
|
mac_address = MultiValueMACAddressFilter(
|
||||||
field_name='interfaces__mac_address',
|
field_name='interfaces__mac_addresses__mac_address',
|
||||||
label=_('MAC address'),
|
label=_('MAC address'),
|
||||||
)
|
)
|
||||||
has_primary_ip = django_filters.BooleanFilter(
|
has_primary_ip = django_filters.BooleanFilter(
|
||||||
@ -263,8 +264,20 @@ class VMInterfaceFilterSet(NetBoxModelFilterSet, CommonInterfaceFilterSet):
|
|||||||
label=_('Bridged interface (ID)'),
|
label=_('Bridged interface (ID)'),
|
||||||
)
|
)
|
||||||
mac_address = MultiValueMACAddressFilter(
|
mac_address = MultiValueMACAddressFilter(
|
||||||
|
field_name='mac_addresses__mac_address',
|
||||||
label=_('MAC address'),
|
label=_('MAC address'),
|
||||||
)
|
)
|
||||||
|
primary_mac_address_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='primary_mac_address',
|
||||||
|
queryset=MACAddress.objects.all(),
|
||||||
|
label=_('Primary MAC address (ID)'),
|
||||||
|
)
|
||||||
|
primary_mac_address = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='primary_mac_address__mac_address',
|
||||||
|
queryset=MACAddress.objects.all(),
|
||||||
|
to_field_name='mac_address',
|
||||||
|
label=_('Primary MAC address'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
|
@ -279,7 +279,7 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
# Check interface sites. First interface should set site, further interfaces will either continue the
|
# Check interface sites. First interface should set site, further interfaces will either continue the
|
||||||
# loop or reset back to no site and break the loop.
|
# loop or reset back to no site and break the loop.
|
||||||
for interface in interfaces:
|
for interface in interfaces:
|
||||||
vm_site = interface.virtual_machine.site or interface.virtual_machine.cluster.site
|
vm_site = interface.virtual_machine.site or interface.virtual_machine.cluster._site
|
||||||
if site is None:
|
if site is None:
|
||||||
site = vm_site
|
site = vm_site
|
||||||
elif vm_site is not site:
|
elif vm_site is not site:
|
||||||
|
@ -182,7 +182,7 @@ class VMInterfaceImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = (
|
fields = (
|
||||||
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
|
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mtu', 'description', 'mode',
|
||||||
'vrf', 'tags'
|
'vrf', 'tags'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,12 +62,14 @@ class ClusterGroupForm(NetBoxModelForm):
|
|||||||
class ClusterForm(TenancyForm, ScopedForm, NetBoxModelForm):
|
class ClusterForm(TenancyForm, ScopedForm, NetBoxModelForm):
|
||||||
type = DynamicModelChoiceField(
|
type = DynamicModelChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
queryset=ClusterType.objects.all()
|
queryset=ClusterType.objects.all(),
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
group = DynamicModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
label=_('Group'),
|
label=_('Group'),
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
@ -358,7 +360,7 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('virtual_machine', 'name', 'description', 'tags', name=_('Interface')),
|
FieldSet('virtual_machine', 'name', 'description', 'tags', name=_('Interface')),
|
||||||
FieldSet('vrf', 'mac_address', name=_('Addressing')),
|
FieldSet('vrf', 'primary_mac_address', name=_('Addressing')),
|
||||||
FieldSet('mtu', 'enabled', name=_('Operation')),
|
FieldSet('mtu', 'enabled', name=_('Operation')),
|
||||||
FieldSet('parent', 'bridge', name=_('Related Interfaces')),
|
FieldSet('parent', 'bridge', name=_('Related Interfaces')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
@ -370,8 +372,9 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = [
|
fields = [
|
||||||
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
|
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mtu', 'description', 'mode', 'vlan_group',
|
||||||
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'tags',
|
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'primary_mac_address',
|
||||||
|
'tags',
|
||||||
]
|
]
|
||||||
labels = {
|
labels = {
|
||||||
'mode': _('802.1Q Mode'),
|
'mode': _('802.1Q Mode'),
|
||||||
|
@ -106,12 +106,14 @@ class VMInterfaceType(IPAddressesMixin, ComponentType):
|
|||||||
bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||||
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
|
primary_mac_address: Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
vlan_translation_policy: Annotated["VLANTranslationPolicyType", 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')]]
|
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
||||||
bridge_interfaces: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]
|
bridge_interfaces: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]
|
||||||
child_interfaces: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]
|
child_interfaces: List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]
|
||||||
|
mac_addresses: List[Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_mac_addresses(apps, schema_editor):
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
VMInterface = apps.get_model('virtualization', 'VMInterface')
|
||||||
|
MACAddress = apps.get_model('dcim', 'MACAddress')
|
||||||
|
vminterface_ct = ContentType.objects.get_for_model(VMInterface)
|
||||||
|
|
||||||
|
mac_addresses = [
|
||||||
|
MACAddress(
|
||||||
|
mac_address=vminterface.mac_address,
|
||||||
|
assigned_object_type=vminterface_ct,
|
||||||
|
assigned_object_id=vminterface.pk
|
||||||
|
)
|
||||||
|
for vminterface in VMInterface.objects.filter(mac_address__isnull=False)
|
||||||
|
]
|
||||||
|
MACAddress.objects.bulk_create(mac_addresses, batch_size=100)
|
||||||
|
|
||||||
|
# TODO: Optimize interface updates
|
||||||
|
for mac_address in mac_addresses:
|
||||||
|
VMInterface.objects.filter(pk=mac_address.assigned_object_id).update(primary_mac_address=mac_address)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0199_macaddress'),
|
||||||
|
('virtualization', '0047_natural_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vminterface',
|
||||||
|
name='primary_mac_address',
|
||||||
|
field=models.OneToOneField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name='+',
|
||||||
|
to='dcim.macaddress'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=populate_mac_addresses,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='vminterface',
|
||||||
|
name='mac_address',
|
||||||
|
),
|
||||||
|
]
|
@ -348,6 +348,12 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
|
|||||||
object_id_field='assigned_object_id',
|
object_id_field='assigned_object_id',
|
||||||
related_query_name='vminterface',
|
related_query_name='vminterface',
|
||||||
)
|
)
|
||||||
|
mac_addresses = GenericRelation(
|
||||||
|
to='dcim.MACAddress',
|
||||||
|
content_type_field='assigned_object_type',
|
||||||
|
object_id_field='assigned_object_id',
|
||||||
|
related_query_name='vminterface'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(ComponentModel.Meta):
|
class Meta(ComponentModel.Meta):
|
||||||
verbose_name = _('interface')
|
verbose_name = _('interface')
|
||||||
|
@ -52,11 +52,10 @@ class VMInterfaceIndex(SearchIndex):
|
|||||||
model = models.VMInterface
|
model = models.VMInterface
|
||||||
fields = (
|
fields = (
|
||||||
('name', 100),
|
('name', 100),
|
||||||
('mac_address', 300),
|
|
||||||
('description', 500),
|
('description', 500),
|
||||||
('mtu', 2000),
|
('mtu', 2000),
|
||||||
)
|
)
|
||||||
display_attrs = ('virtual_machine', 'mac_address', 'description')
|
display_attrs = ('virtual_machine', 'description')
|
||||||
|
|
||||||
|
|
||||||
@register_search
|
@register_search
|
||||||
|
@ -25,6 +25,9 @@ VMINTERFACE_BUTTONS = """
|
|||||||
{% if perms.ipam.add_ipaddress %}
|
{% if perms.ipam.add_ipaddress %}
|
||||||
<li><a class="dropdown-item" href="{% url 'ipam:ipaddress_add' %}?vminterface={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">IP Address</a></li>
|
<li><a class="dropdown-item" href="{% url 'ipam:ipaddress_add' %}?vminterface={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">IP Address</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_macaddress %}
|
||||||
|
<li><a class="dropdown-item" href="{% url 'dcim:macaddress_add' %}?vminterface={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">MAC Address</a></li>
|
||||||
|
{% endif %}
|
||||||
{% if perms.vpn.add_l2vpntermination %}
|
{% if perms.vpn.add_l2vpntermination %}
|
||||||
<li><a class="dropdown-item" href="{% url 'vpn:l2vpntermination_add' %}?virtual_machine={{ object.pk }}&vminterface={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">L2VPN Termination</a></li>
|
<li><a class="dropdown-item" href="{% url 'vpn:l2vpntermination_add' %}?virtual_machine={{ object.pk }}&vminterface={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">L2VPN Termination</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -150,8 +153,8 @@ class VMInterfaceTable(BaseInterfaceTable):
|
|||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||||
'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
'vrf', 'primary_mac_address', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan',
|
||||||
'created', 'last_updated',
|
'tagged_vlans', 'qinq_svlan', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, MACAddress, Platform, Region, Site, SiteGroup
|
||||||
from ipam.choices import VLANQinQRoleChoices
|
from ipam.choices import VLANQinQRoleChoices
|
||||||
from ipam.models import IPAddress, VLAN, VLANTranslationPolicy, VRF
|
from ipam.models import IPAddress, VLAN, VLANTranslationPolicy, VRF
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
@ -366,13 +366,24 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
VirtualMachine.objects.bulk_create(vms)
|
VirtualMachine.objects.bulk_create(vms)
|
||||||
|
|
||||||
|
mac_addresses = (
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-01'),
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-02'),
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-03'),
|
||||||
|
)
|
||||||
|
MACAddress.objects.bulk_create(mac_addresses)
|
||||||
|
|
||||||
interfaces = (
|
interfaces = (
|
||||||
VMInterface(virtual_machine=vms[0], name='Interface 1', mac_address='00-00-00-00-00-01'),
|
VMInterface(virtual_machine=vms[0], name='Interface 1'),
|
||||||
VMInterface(virtual_machine=vms[1], name='Interface 2', mac_address='00-00-00-00-00-02'),
|
VMInterface(virtual_machine=vms[1], name='Interface 2'),
|
||||||
VMInterface(virtual_machine=vms[2], name='Interface 3', mac_address='00-00-00-00-00-03'),
|
VMInterface(virtual_machine=vms[2], name='Interface 3'),
|
||||||
)
|
)
|
||||||
VMInterface.objects.bulk_create(interfaces)
|
VMInterface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
|
interfaces[0].mac_addresses.set([mac_addresses[0]])
|
||||||
|
interfaces[1].mac_addresses.set([mac_addresses[1]])
|
||||||
|
interfaces[2].mac_addresses.set([mac_addresses[2]])
|
||||||
|
|
||||||
# Assign primary IPs for filtering
|
# Assign primary IPs for filtering
|
||||||
ipaddresses = (
|
ipaddresses = (
|
||||||
IPAddress(address='192.0.2.1/24', assigned_object=interfaces[0]),
|
IPAddress(address='192.0.2.1/24', assigned_object=interfaces[0]),
|
||||||
@ -579,13 +590,19 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
VLANTranslationPolicy.objects.bulk_create(vlan_translation_policies)
|
||||||
|
|
||||||
|
mac_addresses = (
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-01'),
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-02'),
|
||||||
|
MACAddress(mac_address='00-00-00-00-00-03'),
|
||||||
|
)
|
||||||
|
MACAddress.objects.bulk_create(mac_addresses)
|
||||||
|
|
||||||
interfaces = (
|
interfaces = (
|
||||||
VMInterface(
|
VMInterface(
|
||||||
virtual_machine=vms[0],
|
virtual_machine=vms[0],
|
||||||
name='Interface 1',
|
name='Interface 1',
|
||||||
enabled=True,
|
enabled=True,
|
||||||
mtu=100,
|
mtu=100,
|
||||||
mac_address='00-00-00-00-00-01',
|
|
||||||
vrf=vrfs[0],
|
vrf=vrfs[0],
|
||||||
description='foobar1',
|
description='foobar1',
|
||||||
vlan_translation_policy=vlan_translation_policies[0],
|
vlan_translation_policy=vlan_translation_policies[0],
|
||||||
@ -595,7 +612,6 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
name='Interface 2',
|
name='Interface 2',
|
||||||
enabled=True,
|
enabled=True,
|
||||||
mtu=200,
|
mtu=200,
|
||||||
mac_address='00-00-00-00-00-02',
|
|
||||||
vrf=vrfs[1],
|
vrf=vrfs[1],
|
||||||
description='foobar2',
|
description='foobar2',
|
||||||
vlan_translation_policy=vlan_translation_policies[0],
|
vlan_translation_policy=vlan_translation_policies[0],
|
||||||
@ -605,7 +621,6 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
name='Interface 3',
|
name='Interface 3',
|
||||||
enabled=False,
|
enabled=False,
|
||||||
mtu=300,
|
mtu=300,
|
||||||
mac_address='00-00-00-00-00-03',
|
|
||||||
vrf=vrfs[2],
|
vrf=vrfs[2],
|
||||||
description='foobar3',
|
description='foobar3',
|
||||||
mode=InterfaceModeChoices.MODE_Q_IN_Q,
|
mode=InterfaceModeChoices.MODE_Q_IN_Q,
|
||||||
@ -614,6 +629,10 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
VMInterface.objects.bulk_create(interfaces)
|
VMInterface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
|
interfaces[0].mac_addresses.set([mac_addresses[0]])
|
||||||
|
interfaces[1].mac_addresses.set([mac_addresses[1]])
|
||||||
|
interfaces[2].mac_addresses.set([mac_addresses[2]])
|
||||||
|
|
||||||
def test_q(self):
|
def test_q(self):
|
||||||
params = {'q': 'foobar1'}
|
params = {'q': 'foobar1'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from netaddr import EUI
|
|
||||||
|
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.models import DeviceRole, Platform, Site
|
from dcim.models import DeviceRole, Platform, Site
|
||||||
@ -331,7 +330,6 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
'name': 'Interface X',
|
'name': 'Interface X',
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'bridge': interfaces[1].pk,
|
'bridge': interfaces[1].pk,
|
||||||
'mac_address': EUI('01-02-03-04-05-06'),
|
|
||||||
'mtu': 65000,
|
'mtu': 65000,
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||||
@ -346,7 +344,6 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
'name': 'Interface [4-6]',
|
'name': 'Interface [4-6]',
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'bridge': interfaces[3].pk,
|
'bridge': interfaces[3].pk,
|
||||||
'mac_address': EUI('01-02-03-04-05-06'),
|
|
||||||
'mtu': 2000,
|
'mtu': 2000,
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||||
|
@ -47,7 +47,8 @@ class TunnelForm(TenancyForm, NetBoxModelForm):
|
|||||||
group = DynamicModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
queryset=TunnelGroup.objects.all(),
|
queryset=TunnelGroup.objects.all(),
|
||||||
label=_('Tunnel Group'),
|
label=_('Tunnel Group'),
|
||||||
required=False
|
required=False,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
ipsec_profile = DynamicModelChoiceField(
|
ipsec_profile = DynamicModelChoiceField(
|
||||||
queryset=IPSecProfile.objects.all(),
|
queryset=IPSecProfile.objects.all(),
|
||||||
@ -313,7 +314,8 @@ class IKEProposalForm(NetBoxModelForm):
|
|||||||
class IKEPolicyForm(NetBoxModelForm):
|
class IKEPolicyForm(NetBoxModelForm):
|
||||||
proposals = DynamicModelMultipleChoiceField(
|
proposals = DynamicModelMultipleChoiceField(
|
||||||
queryset=IKEProposal.objects.all(),
|
queryset=IKEProposal.objects.all(),
|
||||||
label=_('Proposals')
|
label=_('Proposals'),
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@ -349,7 +351,8 @@ class IPSecProposalForm(NetBoxModelForm):
|
|||||||
class IPSecPolicyForm(NetBoxModelForm):
|
class IPSecPolicyForm(NetBoxModelForm):
|
||||||
proposals = DynamicModelMultipleChoiceField(
|
proposals = DynamicModelMultipleChoiceField(
|
||||||
queryset=IPSecProposal.objects.all(),
|
queryset=IPSecProposal.objects.all(),
|
||||||
label=_('Proposals')
|
label=_('Proposals'),
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
|
@ -40,7 +40,8 @@ class WirelessLANForm(ScopedForm, TenancyForm, NetBoxModelForm):
|
|||||||
group = DynamicModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
label=_('Group'),
|
label=_('Group'),
|
||||||
queryset=WirelessLANGroup.objects.all(),
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
quick_add=True
|
||||||
)
|
)
|
||||||
vlan = DynamicModelChoiceField(
|
vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
|
Loading…
Reference in New Issue
Block a user