mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Merge pull request #5930 from netbox-community/1519-interface-parent
Closes #1519: Enable parent assignment for interfaces
This commit is contained in:
commit
1c66733b8a
@ -6,6 +6,10 @@
|
|||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
|
#### Parent Interface Assignments ([#1519](https://github.com/netbox-community/netbox/issues/1519))
|
||||||
|
|
||||||
|
Virtual interfaces can now be assigned to a "parent" physical interface, by setting the `parent` field on the Interface model. This is helpful for associating subinterfaces with their physical counterpart. For example, you might assign virtual interfaces Gi0/0.100 and Gi0/0.200 to the physical interface Gi0/0.
|
||||||
|
|
||||||
#### Mark as Connected Without a Cable ([#3648](https://github.com/netbox-community/netbox/issues/3648))
|
#### Mark as Connected Without a Cable ([#3648](https://github.com/netbox-community/netbox/issues/3648))
|
||||||
|
|
||||||
Cable termination objects (circuit terminations, power feeds, and most device components) can now be marked as "connected" without actually attaching a cable. This helps simplify the process of modeling an infrastructure boundary where you don't necessarily know or care what is connected to the far end of a cable, but still need to designate the near end termination.
|
Cable termination objects (circuit terminations, power feeds, and most device components) can now be marked as "connected" without actually attaching a cable. This helps simplify the process of modeling an infrastructure boundary where you don't necessarily know or care what is connected to the far end of a cable, but still need to designate the near end termination.
|
||||||
@ -58,6 +62,8 @@ The ObjectChange model (which is used to record the creation, modification, and
|
|||||||
* The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
|
* The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
|
||||||
* dcim.Device
|
* dcim.Device
|
||||||
* Added the `location` field
|
* Added the `location` field
|
||||||
|
* dcim.Interface
|
||||||
|
* Added the `parent` field
|
||||||
* dcim.PowerPanel
|
* dcim.PowerPanel
|
||||||
* Renamed `rack_group` field to `location`
|
* Renamed `rack_group` field to `location`
|
||||||
* dcim.Rack
|
* dcim.Rack
|
||||||
|
@ -295,7 +295,7 @@ class CircuitTermination(ChangeLoggingMixin, BigIDModel, PathEndpoint, CableTerm
|
|||||||
return super().to_objectchange(action, related_object=circuit)
|
return super().to_objectchange(action, related_object=circuit)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self):
|
def parent_object(self):
|
||||||
return self.circuit
|
return self.circuit
|
||||||
|
|
||||||
def get_peer_termination(self):
|
def get_peer_termination(self):
|
||||||
|
@ -598,6 +598,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
|
|||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||||
|
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||||
@ -613,10 +614,11 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
|
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
||||||
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_peer',
|
'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable',
|
||||||
'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
|
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
||||||
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied',
|
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
|
||||||
|
'_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
@ -522,7 +522,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
|||||||
|
|
||||||
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
||||||
queryset = Interface.objects.prefetch_related(
|
queryset = Interface.objects.prefetch_related(
|
||||||
'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
'device', 'parent', 'lag', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
filterset_class = filters.InterfaceFilterSet
|
filterset_class = filters.InterfaceFilterSet
|
||||||
|
@ -844,6 +844,11 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
|
|||||||
method='filter_kind',
|
method='filter_kind',
|
||||||
label='Kind of interface',
|
label='Kind of interface',
|
||||||
)
|
)
|
||||||
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='parent',
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
label='Parent interface (ID)',
|
||||||
|
)
|
||||||
lag_id = django_filters.ModelMultipleChoiceFilter(
|
lag_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='lag',
|
field_name='lag',
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
|
@ -2802,6 +2802,24 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
||||||
|
parent = DynamicModelChoiceField(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Parent interface',
|
||||||
|
display_field='display_name',
|
||||||
|
query_params={
|
||||||
|
'kind': 'physical',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lag = DynamicModelChoiceField(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='LAG interface',
|
||||||
|
display_field='display_name',
|
||||||
|
query_params={
|
||||||
|
'type': 'lag',
|
||||||
|
}
|
||||||
|
)
|
||||||
untagged_vlan = DynamicModelChoiceField(
|
untagged_vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -2830,13 +2848,12 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'label', 'type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected',
|
'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only',
|
||||||
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
|
'mark_connected', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
'type': StaticSelect2(),
|
'type': StaticSelect2(),
|
||||||
'lag': StaticSelect2(),
|
|
||||||
'mode': StaticSelect2(),
|
'mode': StaticSelect2(),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
@ -2849,19 +2866,11 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if self.is_bound:
|
device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device
|
||||||
device = Device.objects.get(pk=self.data['device'])
|
|
||||||
else:
|
|
||||||
device = self.instance.device
|
|
||||||
|
|
||||||
# Limit LAG choices to interfaces belonging to this device or a peer VC member
|
# Restrict parent/LAG interface assignment by device
|
||||||
device_query = Q(device=device)
|
self.fields['parent'].widget.add_query_param('device_id', device.pk)
|
||||||
if device.virtual_chassis:
|
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||||
device_query |= Q(device__virtual_chassis=device.virtual_chassis)
|
|
||||||
self.fields['lag'].queryset = Interface.objects.filter(
|
|
||||||
device_query,
|
|
||||||
type=InterfaceTypeChoices.TYPE_LAG
|
|
||||||
).exclude(pk=self.instance.pk)
|
|
||||||
|
|
||||||
# Add current site to VLANs query params
|
# Add current site to VLANs query params
|
||||||
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
|
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
|
||||||
@ -2878,11 +2887,23 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
|||||||
required=False,
|
required=False,
|
||||||
initial=True
|
initial=True
|
||||||
)
|
)
|
||||||
lag = forms.ModelChoiceField(
|
parent = DynamicModelChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label='Parent LAG',
|
display_field='display_name',
|
||||||
widget=StaticSelect2(),
|
query_params={
|
||||||
|
'device_id': '$device',
|
||||||
|
'kind': 'physical',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lag = DynamicModelChoiceField(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
required=False,
|
||||||
|
display_field='display_name',
|
||||||
|
query_params={
|
||||||
|
'device_id': '$device',
|
||||||
|
'type': 'lag',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
mtu = forms.IntegerField(
|
mtu = forms.IntegerField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -2923,23 +2944,17 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
field_order = (
|
field_order = (
|
||||||
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'description',
|
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
||||||
'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
|
'description', 'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit LAG choices to interfaces belonging to this device or a peer VC member
|
# Add current site to VLANs query params
|
||||||
device = Device.objects.get(
|
device = Device.objects.get(
|
||||||
pk=self.initial.get('device') or self.data.get('device')
|
pk=self.initial.get('device') or self.data.get('device')
|
||||||
)
|
)
|
||||||
device_query = Q(device=device)
|
|
||||||
if device.virtual_chassis:
|
|
||||||
device_query |= Q(device__virtual_chassis=device.virtual_chassis)
|
|
||||||
self.fields['lag'].queryset = Interface.objects.filter(device_query, type=InterfaceTypeChoices.TYPE_LAG)
|
|
||||||
|
|
||||||
# Add current site to VLANs query params
|
|
||||||
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
|
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
|
||||||
self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk)
|
self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk)
|
||||||
|
|
||||||
@ -2956,7 +2971,7 @@ class InterfaceBulkCreateForm(
|
|||||||
|
|
||||||
class InterfaceBulkEditForm(
|
class InterfaceBulkEditForm(
|
||||||
form_from_model(Interface, [
|
form_from_model(Interface, [
|
||||||
'label', 'type', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode'
|
'label', 'type', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode',
|
||||||
]),
|
]),
|
||||||
BootstrapMixin,
|
BootstrapMixin,
|
||||||
AddRemoveTagsForm,
|
AddRemoveTagsForm,
|
||||||
@ -2976,6 +2991,22 @@ class InterfaceBulkEditForm(
|
|||||||
required=False,
|
required=False,
|
||||||
widget=BulkEditNullBooleanSelect
|
widget=BulkEditNullBooleanSelect
|
||||||
)
|
)
|
||||||
|
parent = DynamicModelChoiceField(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
required=False,
|
||||||
|
display_field='display_name',
|
||||||
|
query_params={
|
||||||
|
'kind': 'physical',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lag = DynamicModelChoiceField(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
required=False,
|
||||||
|
display_field='display_name',
|
||||||
|
query_params={
|
||||||
|
'type': 'lag',
|
||||||
|
}
|
||||||
|
)
|
||||||
mgmt_only = forms.NullBooleanField(
|
mgmt_only = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=BulkEditNullBooleanSelect,
|
widget=BulkEditNullBooleanSelect,
|
||||||
@ -3006,25 +3037,24 @@ class InterfaceBulkEditForm(
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = [
|
nullable_fields = [
|
||||||
'label', 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
|
'label', 'parent', 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit LAG choices to interfaces which belong to the parent device (or VC master)
|
|
||||||
if 'device' in self.initial:
|
if 'device' in self.initial:
|
||||||
device = Device.objects.filter(pk=self.initial['device']).first()
|
device = Device.objects.filter(pk=self.initial['device']).first()
|
||||||
self.fields['lag'].queryset = Interface.objects.filter(
|
|
||||||
device__in=[device, device.get_vc_master()],
|
# Restrict parent/LAG interface assignment by device
|
||||||
type=InterfaceTypeChoices.TYPE_LAG
|
self.fields['parent'].widget.add_query_param('device_id', device.pk)
|
||||||
)
|
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||||
|
|
||||||
# Add current site to VLANs query params
|
# Add current site to VLANs query params
|
||||||
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
|
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
|
||||||
self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk)
|
self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# See 4523
|
# See #4523
|
||||||
if 'pk' in self.initial:
|
if 'pk' in self.initial:
|
||||||
site = None
|
site = None
|
||||||
interfaces = Interface.objects.filter(pk__in=self.initial['pk']).prefetch_related('device__site')
|
interfaces = Interface.objects.filter(pk__in=self.initial['pk']).prefetch_related('device__site')
|
||||||
@ -3042,6 +3072,8 @@ class InterfaceBulkEditForm(
|
|||||||
self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
|
self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
|
||||||
self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
|
self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
|
||||||
|
|
||||||
|
self.fields['parent'].choices = ()
|
||||||
|
self.fields['parent'].widget.attrs['disabled'] = True
|
||||||
self.fields['lag'].choices = ()
|
self.fields['lag'].choices = ()
|
||||||
self.fields['lag'].widget.attrs['disabled'] = True
|
self.fields['lag'].widget.attrs['disabled'] = True
|
||||||
|
|
||||||
@ -3064,6 +3096,12 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
|||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name'
|
to_field_name='name'
|
||||||
)
|
)
|
||||||
|
parent = CSVModelChoiceField(
|
||||||
|
queryset=Interface.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Parent interface'
|
||||||
|
)
|
||||||
lag = CSVModelChoiceField(
|
lag = CSVModelChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
17
netbox/dcim/migrations/0129_interface_parent.py
Normal file
17
netbox/dcim/migrations/0129_interface_parent.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0128_device_location_populate'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='parent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_interfaces', to='dcim.interface'),
|
||||||
|
),
|
||||||
|
]
|
@ -4,13 +4,11 @@ from django.db import models
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from extras.models import ObjectChange
|
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from netbox.models import BigIDModel, ChangeLoggingMixin
|
from netbox.models import BigIDModel, ChangeLoggingMixin
|
||||||
from utilities.fields import NaturalOrderingField
|
from utilities.fields import NaturalOrderingField
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.ordering import naturalize_interface
|
from utilities.ordering import naturalize_interface
|
||||||
from utilities.utils import serialize_object
|
|
||||||
from .device_components import (
|
from .device_components import (
|
||||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
|
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
|
||||||
)
|
)
|
||||||
|
@ -11,7 +11,7 @@ from taggit.managers import TaggableManager
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import MACAddressField
|
from dcim.fields import MACAddressField
|
||||||
from extras.models import ObjectChange, TaggedItem
|
from extras.models import TaggedItem
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from netbox.models import PrimaryModel
|
from netbox.models import PrimaryModel
|
||||||
from utilities.fields import NaturalOrderingField
|
from utilities.fields import NaturalOrderingField
|
||||||
@ -19,7 +19,6 @@ from utilities.mptt import TreeManager
|
|||||||
from utilities.ordering import naturalize_interface
|
from utilities.ordering import naturalize_interface
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.query_functions import CollateAsChar
|
from utilities.query_functions import CollateAsChar
|
||||||
from utilities.utils import serialize_object
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -85,8 +84,8 @@ class ComponentModel(PrimaryModel):
|
|||||||
return super().to_objectchange(action, related_object=device)
|
return super().to_objectchange(action, related_object=device)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self):
|
def parent_object(self):
|
||||||
return getattr(self, 'device', None)
|
return self.device
|
||||||
|
|
||||||
|
|
||||||
class CableTermination(models.Model):
|
class CableTermination(models.Model):
|
||||||
@ -153,6 +152,10 @@ class CableTermination(models.Model):
|
|||||||
def _occupied(self):
|
def _occupied(self):
|
||||||
return bool(self.mark_connected or self.cable_id)
|
return bool(self.mark_connected or self.cable_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent_object(self):
|
||||||
|
raise NotImplementedError("CableTermination models must implement parent_object()")
|
||||||
|
|
||||||
|
|
||||||
class PathEndpoint(models.Model):
|
class PathEndpoint(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -208,7 +211,7 @@ class PathEndpoint(models.Model):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||||
class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||||
"""
|
"""
|
||||||
@ -252,7 +255,7 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||||
class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||||
"""
|
"""
|
||||||
@ -296,7 +299,7 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||||
class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||||
"""
|
"""
|
||||||
@ -408,7 +411,7 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||||
class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
|
class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||||
"""
|
"""
|
||||||
@ -509,7 +512,7 @@ class BaseInterface(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||||
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||||
"""
|
"""
|
||||||
@ -520,6 +523,14 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
|||||||
max_length=100,
|
max_length=100,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
parent = models.ForeignKey(
|
||||||
|
to='self',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='child_interfaces',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Parent interface'
|
||||||
|
)
|
||||||
lag = models.ForeignKey(
|
lag = models.ForeignKey(
|
||||||
to='self',
|
to='self',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
@ -560,8 +571,8 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
|||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'device', 'name', 'label', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu', 'mgmt_only',
|
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu',
|
||||||
'description', 'mode',
|
'mgmt_only', 'description', 'mode',
|
||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -576,6 +587,7 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
|||||||
self.device.identifier if self.device else None,
|
self.device.identifier if self.device else None,
|
||||||
self.name,
|
self.name,
|
||||||
self.label,
|
self.label,
|
||||||
|
self.parent.name if self.parent else None,
|
||||||
self.lag.name if self.lag else None,
|
self.lag.name if self.lag else None,
|
||||||
self.get_type_display(),
|
self.get_type_display(),
|
||||||
self.enabled,
|
self.enabled,
|
||||||
@ -599,6 +611,27 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
|||||||
"Disconnect the interface or choose a suitable type."
|
"Disconnect the interface or choose a suitable type."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# An interface's parent must belong to the same device or virtual chassis
|
||||||
|
if self.parent and self.parent.device != self.device:
|
||||||
|
if self.device.virtual_chassis is None:
|
||||||
|
raise ValidationError({
|
||||||
|
'parent': f"The selected parent interface ({self.parent}) belongs to a different device "
|
||||||
|
f"({self.parent.device})."
|
||||||
|
})
|
||||||
|
elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
|
||||||
|
raise ValidationError({
|
||||||
|
'parent': f"The selected parent interface ({self.parent}) belongs to {self.parent.device}, which "
|
||||||
|
f"is not part of virtual chassis {self.device.virtual_chassis}."
|
||||||
|
})
|
||||||
|
|
||||||
|
# A physical interface cannot have a parent interface
|
||||||
|
if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None:
|
||||||
|
raise ValidationError({'parent': "Only virtual interfaces may be assigned to a parent interface."})
|
||||||
|
|
||||||
|
# A virtual interface cannot be a parent interface
|
||||||
|
if self.parent is not None and self.parent.type == InterfaceTypeChoices.TYPE_VIRTUAL:
|
||||||
|
raise ValidationError({'parent': "Virtual interfaces may not be parents of other interfaces."})
|
||||||
|
|
||||||
# An interface's LAG must belong to the same device or virtual chassis
|
# An interface's LAG must belong to the same device or virtual chassis
|
||||||
if self.lag and self.lag.device != self.device:
|
if self.lag and self.lag.device != self.device:
|
||||||
if self.device.virtual_chassis is None:
|
if self.device.virtual_chassis is None:
|
||||||
@ -620,16 +653,12 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
|||||||
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
|
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
|
||||||
|
|
||||||
# Validate untagged VLAN
|
# Validate untagged VLAN
|
||||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
|
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
|
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
|
||||||
"device, or it must be global".format(self.untagged_vlan)
|
"device, or it must be global".format(self.untagged_vlan)
|
||||||
})
|
})
|
||||||
|
|
||||||
@property
|
|
||||||
def parent(self):
|
|
||||||
return self.device
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connectable(self):
|
def is_connectable(self):
|
||||||
return self.type not in NONCONNECTABLE_IFACE_TYPES
|
return self.type not in NONCONNECTABLE_IFACE_TYPES
|
||||||
@ -656,7 +685,7 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||||
class FrontPort(CableTermination, ComponentModel):
|
class FrontPort(ComponentModel, CableTermination):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the front of a Device.
|
A pass-through port on the front of a Device.
|
||||||
"""
|
"""
|
||||||
@ -722,7 +751,7 @@ class FrontPort(CableTermination, ComponentModel):
|
|||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||||
class RearPort(CableTermination, ComponentModel):
|
class RearPort(ComponentModel, CableTermination):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the rear of a Device.
|
A pass-through port on the rear of a Device.
|
||||||
"""
|
"""
|
||||||
|
@ -201,7 +201,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self):
|
def parent_object(self):
|
||||||
return self.power_panel
|
return self.power_panel
|
||||||
|
|
||||||
def get_type_class(self):
|
def get_type_class(self):
|
||||||
|
@ -8,13 +8,11 @@ from timezone_field import TimeZoneField
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import ASNField
|
from dcim.fields import ASNField
|
||||||
from extras.models import ObjectChange, TaggedItem
|
from extras.models import TaggedItem
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from netbox.models import NestedGroupModel, PrimaryModel
|
from netbox.models import NestedGroupModel, PrimaryModel
|
||||||
from utilities.fields import NaturalOrderingField
|
from utilities.fields import NaturalOrderingField
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.mptt import TreeManager
|
|
||||||
from utilities.utils import serialize_object
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Region',
|
'Region',
|
||||||
|
@ -436,6 +436,10 @@ class DeviceInterfaceTable(InterfaceTable):
|
|||||||
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
|
parent = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name='Parent'
|
||||||
|
)
|
||||||
lag = tables.Column(
|
lag = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name='LAG'
|
verbose_name='LAG'
|
||||||
@ -449,13 +453,13 @@ class DeviceInterfaceTable(InterfaceTable):
|
|||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'description',
|
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||||
'mark_connected', 'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan',
|
'description', 'mark_connected', 'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses',
|
||||||
'tagged_vlans', 'actions',
|
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mtu', 'mode', 'description', 'ip_addresses', 'cable',
|
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
||||||
'connection', 'actions',
|
'cable', 'connection', 'actions',
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': lambda record: record.cable.get_status_class() if record.cable else '',
|
'class': lambda record: record.cable.get_status_class() if record.cable else '',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
CABLETERMINATION = """
|
CABLETERMINATION = """
|
||||||
{% if value %}
|
{% if value %}
|
||||||
<a href="{{ value.parent.get_absolute_url }}">{{ value.parent }}</a>
|
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
|
||||||
<i class="mdi mdi-chevron-right"></i>
|
<i class="mdi mdi-chevron-right"></i>
|
||||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -64,7 +64,7 @@ POWERFEED_CABLE = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
POWERFEED_CABLETERMINATION = """
|
POWERFEED_CABLETERMINATION = """
|
||||||
<a href="{{ value.parent.get_absolute_url }}">{{ value.parent }}</a>
|
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
|
||||||
<i class="mdi mdi-chevron-right"></i>
|
<i class="mdi mdi-chevron-right"></i>
|
||||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||||
"""
|
"""
|
||||||
|
@ -1931,6 +1931,34 @@ class InterfaceTestCase(TestCase):
|
|||||||
params = {'description': ['First', 'Second']}
|
params = {'description': ['First', 'Second']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_parent(self):
|
||||||
|
# Create child interfaces
|
||||||
|
parent_interface = Interface.objects.first()
|
||||||
|
child_interfaces = (
|
||||||
|
Interface(device=parent_interface.device, name='Child 1', parent=parent_interface, type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||||
|
Interface(device=parent_interface.device, name='Child 2', parent=parent_interface, type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||||
|
Interface(device=parent_interface.device, name='Child 3', parent=parent_interface, type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(child_interfaces)
|
||||||
|
|
||||||
|
params = {'parent_id': [parent_interface.pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
|
def test_lag(self):
|
||||||
|
# Create LAG members
|
||||||
|
device = Device.objects.first()
|
||||||
|
lag_interface = Interface(device=device, name='LAG', type=InterfaceTypeChoices.TYPE_LAG)
|
||||||
|
lag_interface.save()
|
||||||
|
lag_members = (
|
||||||
|
Interface(device=device, name='Member 1', lag=lag_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=device, name='Member 2', lag=lag_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
Interface(device=device, name='Member 3', lag=lag_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(lag_members)
|
||||||
|
|
||||||
|
params = {'lag_id': [lag_interface.pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
def test_region(self):
|
def test_region(self):
|
||||||
regions = Region.objects.all()[:2]
|
regions = Region.objects.all()[:2]
|
||||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||||
|
@ -2178,13 +2178,13 @@ class CableCreateView(generic.ObjectEditView):
|
|||||||
initial_data = {k: request.GET[k] for k in request.GET}
|
initial_data = {k: request.GET[k] for k in request.GET}
|
||||||
|
|
||||||
# Set initial site and rack based on side A termination (if not already set)
|
# Set initial site and rack based on side A termination (if not already set)
|
||||||
termination_a_site = getattr(obj.termination_a.parent, 'site', None)
|
termination_a_site = getattr(obj.termination_a.parent_object, 'site', None)
|
||||||
if termination_a_site and 'termination_b_region' not in initial_data:
|
if termination_a_site and 'termination_b_region' not in initial_data:
|
||||||
initial_data['termination_b_region'] = termination_a_site.region
|
initial_data['termination_b_region'] = termination_a_site.region
|
||||||
if 'termination_b_site' not in initial_data:
|
if 'termination_b_site' not in initial_data:
|
||||||
initial_data['termination_b_site'] = termination_a_site
|
initial_data['termination_b_site'] = termination_a_site
|
||||||
if 'termination_b_rack' not in initial_data:
|
if 'termination_b_rack' not in initial_data:
|
||||||
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent, 'rack', None)
|
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent_object, 'rack', None)
|
||||||
|
|
||||||
form = self.model_form(instance=obj, initial=initial_data)
|
form = self.model_form(instance=obj, initial=initial_data)
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ from django.urls import reverse
|
|||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.models import ObjectChange, TaggedItem
|
from extras.models import TaggedItem
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from netbox.models import OrganizationalModel, PrimaryModel
|
from netbox.models import OrganizationalModel, PrimaryModel
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
@ -19,7 +19,6 @@ from ipam.managers import IPAddressManager
|
|||||||
from ipam.querysets import PrefixQuerySet
|
from ipam.querysets import PrefixQuerySet
|
||||||
from ipam.validators import DNSValidator
|
from ipam.validators import DNSValidator
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.utils import serialize_object
|
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
|
@ -102,12 +102,12 @@
|
|||||||
<tr{% if cablepath.pk == path.pk %} class="info"{% endif %}>
|
<tr{% if cablepath.pk == path.pk %} class="info"{% endif %}>
|
||||||
<td>
|
<td>
|
||||||
<a href="?cablepath_id={{ cablepath.pk }}">
|
<a href="?cablepath_id={{ cablepath.pk }}">
|
||||||
{{ cablepath.origin.parent }} / {{ cablepath.origin }}
|
{{ cablepath.origin.parent_object }} / {{ cablepath.origin }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if cablepath.destination %}
|
{% if cablepath.destination %}
|
||||||
{{ cablepath.destination }} ({{ cablepath.destination.parent }})
|
{{ cablepath.destination }} ({{ cablepath.destination.parent_object }})
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Incomplete</span>
|
<span class="text-muted">Incomplete</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if termination.parent.provider %}
|
{% if termination.parent_object.provider %}
|
||||||
<i class="mdi mdi-lightning-bolt" title="Circuit"></i>
|
<i class="mdi mdi-lightning-bolt" title="Circuit"></i>
|
||||||
<a href="{{ termination.parent.get_absolute_url }}">
|
<a href="{{ termination.parent_object.get_absolute_url }}">
|
||||||
{{ termination.parent.provider }}
|
{{ termination.parent_object.provider }}
|
||||||
{{ termination.parent }}
|
{{ termination.parent_object }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ termination.parent.get_absolute_url }}">{{ termination.parent }}</a>
|
<a href="{{ termination.parent_object.get_absolute_url }}">{{ termination.parent_object }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% if path.destination_id %}
|
{% if path.destination_id %}
|
||||||
{% with endpoint=path.destination %}
|
{% with endpoint=path.destination %}
|
||||||
<td><a href="{{ endpoint.parent.get_absolute_url }}">{{ endpoint.parent }}</a></td>
|
<td><a href="{{ endpoint.parent_object.get_absolute_url }}">{{ endpoint.parent_object }}</a></td>
|
||||||
<td><a href="{{ endpoint.get_absolute_url }}">{{ endpoint }}</a></td>
|
<td><a href="{{ endpoint.get_absolute_url }}">{{ endpoint }}</a></td>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -38,6 +38,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Parent</td>
|
||||||
|
<td>
|
||||||
|
{% if object.parent %}
|
||||||
|
<a href="{{ object.parent.get_absolute_url }}">{{ object.parent }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>LAG</td>
|
<td>LAG</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
{% render_field form.label %}
|
{% render_field form.label %}
|
||||||
{% render_field form.type %}
|
{% render_field form.type %}
|
||||||
{% render_field form.enabled %}
|
{% render_field form.enabled %}
|
||||||
|
{% render_field form.parent %}
|
||||||
{% render_field form.lag %}
|
{% render_field form.lag %}
|
||||||
{% render_field form.mac_address %}
|
{% render_field form.mac_address %}
|
||||||
{% render_field form.mtu %}
|
{% render_field form.mtu %}
|
||||||
|
@ -3,7 +3,7 @@ from django.urls import reverse
|
|||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from extras.models import ObjectChange, TaggedItem
|
from extras.models import TaggedItem
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from netbox.models import NestedGroupModel, PrimaryModel
|
from netbox.models import NestedGroupModel, PrimaryModel
|
||||||
from utilities.mptt import TreeManager
|
from utilities.mptt import TreeManager
|
||||||
|
@ -6,7 +6,7 @@ from django.urls import reverse
|
|||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from dcim.models import BaseInterface, Device
|
from dcim.models import BaseInterface, Device
|
||||||
from extras.models import ConfigContextModel, ObjectChange, TaggedItem
|
from extras.models import ConfigContextModel, TaggedItem
|
||||||
from extras.querysets import ConfigContextModelQuerySet
|
from extras.querysets import ConfigContextModelQuerySet
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from netbox.models import BigIDModel, ChangeLoggingMixin, OrganizationalModel, PrimaryModel
|
from netbox.models import BigIDModel, ChangeLoggingMixin, OrganizationalModel, PrimaryModel
|
||||||
|
Loading…
Reference in New Issue
Block a user