mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-09 13:22:18 -06:00
Merge branch 'develop-2.9' into 2006-scripts-reports-background
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from django.db.models import Count
|
||||
from django.db.models import Count, Prefetch
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
@@ -28,8 +28,8 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
A convenience method for rendering graphs for a particular provider.
|
||||
"""
|
||||
provider = get_object_or_404(Provider, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='provider')
|
||||
provider = get_object_or_404(self.queryset, pk=pk)
|
||||
queryset = Graph.objects.restrict(request.user).filter(type__model='provider')
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -52,7 +52,10 @@ class CircuitTypeViewSet(ModelViewSet):
|
||||
|
||||
class CircuitViewSet(CustomFieldModelViewSet):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'type', 'tenant', 'provider', 'terminations__site', 'terminations__connected_endpoint__device'
|
||||
Prefetch('terminations', queryset=CircuitTermination.objects.unrestricted().prefetch_related(
|
||||
'site', 'connected_endpoint__device'
|
||||
)),
|
||||
'type', 'tenant', 'provider',
|
||||
).prefetch_related('tags')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filterset_class = filters.CircuitFilterSet
|
||||
|
||||
@@ -239,7 +239,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
return self.STATUS_CLASS_MAP.get(self.status)
|
||||
|
||||
def _get_termination(self, side):
|
||||
for ct in self.terminations.all():
|
||||
for ct in self.terminations.unrestricted():
|
||||
if ct.term_side == side:
|
||||
return ct
|
||||
return None
|
||||
|
||||
@@ -10,7 +10,7 @@ def update_circuit(instance, **kwargs):
|
||||
"""
|
||||
When a CircuitTermination has been modified, update the last_updated time of its parent Circuit.
|
||||
"""
|
||||
circuits = Circuit.objects.filter(pk=instance.circuit_id)
|
||||
circuits = Circuit.objects.unrestricted().filter(pk=instance.circuit_id)
|
||||
time = timezone.now()
|
||||
for circuit in circuits:
|
||||
circuit.last_updated = time
|
||||
|
||||
@@ -2,19 +2,9 @@ import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, TagColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, ButtonsColumn, TagColumn, ToggleColumn
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
CIRCUITTYPE_ACTIONS = """
|
||||
<a href="{% url 'circuits:circuittype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.circuit.change_circuittype %}
|
||||
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}?return_url={{ request.path }}"
|
||||
class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
"""
|
||||
@@ -53,11 +43,7 @@ class CircuitTypeTable(BaseTable):
|
||||
circuit_count = tables.Column(
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=CIRCUITTYPE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(CircuitType, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
|
||||
@@ -49,7 +49,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
"""
|
||||
Test retrieval of Graphs assigned to Providers.
|
||||
"""
|
||||
provider = self.model.objects.first()
|
||||
provider = self.model.objects.unrestricted().first()
|
||||
ct = ContentType.objects.get(app_label='circuits', model='provider')
|
||||
graphs = (
|
||||
Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'),
|
||||
|
||||
@@ -25,6 +25,7 @@ urlpatterns = [
|
||||
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
|
||||
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
||||
path('circuit-types/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||
path('circuit-types/<slug:slug>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
|
||||
path('circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
|
||||
|
||||
# Circuits
|
||||
|
||||
@@ -60,19 +60,16 @@ class ProviderEditView(ObjectEditView):
|
||||
queryset = Provider.objects.all()
|
||||
model_form = forms.ProviderForm
|
||||
template_name = 'circuits/provider_edit.html'
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderDeleteView(ObjectDeleteView):
|
||||
queryset = Provider.objects.all()
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderBulkImportView(BulkImportView):
|
||||
queryset = Provider.objects.all()
|
||||
model_form = forms.ProviderCSVForm
|
||||
table = tables.ProviderTable
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderBulkEditView(BulkEditView):
|
||||
@@ -80,14 +77,12 @@ class ProviderBulkEditView(BulkEditView):
|
||||
filterset = filters.ProviderFilterSet
|
||||
table = tables.ProviderTable
|
||||
form = forms.ProviderBulkEditForm
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderBulkDeleteView(BulkDeleteView):
|
||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||
filterset = filters.ProviderFilterSet
|
||||
table = tables.ProviderTable
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -102,20 +97,21 @@ class CircuitTypeListView(ObjectListView):
|
||||
class CircuitTypeEditView(ObjectEditView):
|
||||
queryset = CircuitType.objects.all()
|
||||
model_form = forms.CircuitTypeForm
|
||||
default_return_url = 'circuits:circuittype_list'
|
||||
|
||||
|
||||
class CircuitTypeDeleteView(ObjectDeleteView):
|
||||
queryset = CircuitType.objects.all()
|
||||
|
||||
|
||||
class CircuitTypeBulkImportView(BulkImportView):
|
||||
queryset = CircuitType.objects.all()
|
||||
model_form = forms.CircuitTypeCSVForm
|
||||
table = tables.CircuitTypeTable
|
||||
default_return_url = 'circuits:circuittype_list'
|
||||
|
||||
|
||||
class CircuitTypeBulkDeleteView(BulkDeleteView):
|
||||
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||
table = tables.CircuitTypeTable
|
||||
default_return_url = 'circuits:circuittype_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -165,19 +161,16 @@ class CircuitEditView(ObjectEditView):
|
||||
queryset = Circuit.objects.all()
|
||||
model_form = forms.CircuitForm
|
||||
template_name = 'circuits/circuit_edit.html'
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitDeleteView(ObjectDeleteView):
|
||||
queryset = Circuit.objects.all()
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitBulkImportView(BulkImportView):
|
||||
queryset = Circuit.objects.all()
|
||||
model_form = forms.CircuitCSVForm
|
||||
table = tables.CircuitTable
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitBulkEditView(BulkEditView):
|
||||
@@ -185,14 +178,12 @@ class CircuitBulkEditView(BulkEditView):
|
||||
filterset = filters.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
form = forms.CircuitBulkEditForm
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitBulkDeleteView(BulkDeleteView):
|
||||
queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||
filterset = filters.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitSwapTerminations(ObjectEditView):
|
||||
|
||||
@@ -332,7 +332,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualChassis
|
||||
fields = ['id', 'url', 'master', 'member_count']
|
||||
fields = ['id', 'name', 'url', 'master', 'member_count']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
@@ -183,10 +184,10 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
default=RackElevationDetailRenderChoices.RENDER_JSON
|
||||
)
|
||||
unit_width = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_UNIT_WIDTH_DEFAULT
|
||||
default=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH
|
||||
)
|
||||
unit_height = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
|
||||
default=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT
|
||||
)
|
||||
legend_width = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
|
||||
@@ -245,7 +246,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'description']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@@ -258,7 +259,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'description']
|
||||
|
||||
|
||||
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@@ -271,7 +272,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
||||
|
||||
|
||||
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
@@ -292,7 +293,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description']
|
||||
|
||||
|
||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
@@ -301,7 +302,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'mgmt_only']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@@ -310,7 +311,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'positions']
|
||||
fields = ['id', 'device_type', 'name', 'type', 'positions', 'description']
|
||||
|
||||
|
||||
class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@@ -320,7 +321,7 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position']
|
||||
fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
|
||||
|
||||
|
||||
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
@@ -328,7 +329,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'description']
|
||||
|
||||
|
||||
#
|
||||
@@ -694,12 +695,12 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
#
|
||||
|
||||
class VirtualChassisSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
master = NestedDeviceSerializer()
|
||||
master = NestedDeviceSerializer(required=False)
|
||||
member_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'master', 'domain', 'tags', 'member_count']
|
||||
fields = ['id', 'name', 'domain', 'master', 'tags', 'member_count']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -29,6 +29,7 @@ from utilities.api import (
|
||||
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
|
||||
)
|
||||
from utilities.utils import get_subquery
|
||||
from utilities.metadata import ContentTypeMetadata
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
@@ -43,7 +44,7 @@ class CableTraceMixin(object):
|
||||
"""
|
||||
Trace a complete cable path and return each segment as a three-tuple of (termination, cable, termination).
|
||||
"""
|
||||
obj = get_object_or_404(self.queryset.model, pk=pk)
|
||||
obj = get_object_or_404(self.queryset, pk=pk)
|
||||
|
||||
# Initialize the path array
|
||||
path = []
|
||||
@@ -103,8 +104,8 @@ class SiteViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
A convenience method for rendering graphs for a particular site.
|
||||
"""
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='site')
|
||||
site = get_object_or_404(self.queryset, pk=pk)
|
||||
queryset = Graph.objects.restrict(request.user).filter(type__model='site')
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -156,7 +157,7 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
|
||||
"""
|
||||
rack = get_object_or_404(Rack, pk=pk)
|
||||
rack = get_object_or_404(self.queryset, pk=pk)
|
||||
serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, 400)
|
||||
@@ -226,7 +227,7 @@ class ManufacturerViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').prefetch_related('tags').annotate(
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
|
||||
device_count=Count('instances')
|
||||
)
|
||||
serializer_class = serializers.DeviceTypeSerializer
|
||||
@@ -347,8 +348,8 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
A convenience method for rendering graphs for a particular Device.
|
||||
"""
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='device')
|
||||
device = get_object_or_404(self.queryset, pk=pk)
|
||||
queryset = Graph.objects.restrict(request.user).filter(type__model='device')
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
|
||||
|
||||
return Response(serializer.data)
|
||||
@@ -369,7 +370,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
Execute a NAPALM method on a Device
|
||||
"""
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
device = get_object_or_404(self.queryset, pk=pk)
|
||||
if not device.primary_ip:
|
||||
raise ServiceUnavailable("This device does not have a primary IP address configured.")
|
||||
if device.platform is None:
|
||||
@@ -496,8 +497,8 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
|
||||
"""
|
||||
A convenience method for rendering graphs for a particular interface.
|
||||
"""
|
||||
interface = get_object_or_404(Interface, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='interface')
|
||||
interface = get_object_or_404(self.queryset, pk=pk)
|
||||
queryset = Graph.objects.restrict(request.user).filter(type__model='interface')
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -567,6 +568,7 @@ class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
#
|
||||
|
||||
class CableViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = Cable.objects.prefetch_related(
|
||||
'termination_a', 'termination_b'
|
||||
)
|
||||
@@ -655,7 +657,11 @@ class ConnectedDeviceViewSet(ViewSet):
|
||||
raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
|
||||
|
||||
# Determine local interface from peer interface's connection
|
||||
peer_interface = get_object_or_404(Interface, device__name=peer_device_name, name=peer_interface_name)
|
||||
peer_interface = get_object_or_404(
|
||||
Interface.objects.unrestricted(),
|
||||
device__name=peer_device_name,
|
||||
name=peer_interface_name
|
||||
)
|
||||
local_interface = peer_interface._connected_interface
|
||||
|
||||
if local_interface is None:
|
||||
|
||||
@@ -260,6 +260,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
|
||||
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
|
||||
# NEMA non-locking
|
||||
TYPE_NEMA_115P = 'nema-1-15p'
|
||||
TYPE_NEMA_515P = 'nema-5-15p'
|
||||
TYPE_NEMA_520P = 'nema-5-20p'
|
||||
TYPE_NEMA_530P = 'nema-5-30p'
|
||||
@@ -268,16 +269,27 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_620P = 'nema-6-20p'
|
||||
TYPE_NEMA_630P = 'nema-6-30p'
|
||||
TYPE_NEMA_650P = 'nema-6-50p'
|
||||
TYPE_NEMA_1030P = 'nema-10-30p'
|
||||
TYPE_NEMA_1050P = 'nema-10-50p'
|
||||
TYPE_NEMA_1420P = 'nema-14-20p'
|
||||
TYPE_NEMA_1430P = 'nema-14-30p'
|
||||
TYPE_NEMA_1450P = 'nema-14-50p'
|
||||
TYPE_NEMA_1460P = 'nema-14-60p'
|
||||
# NEMA locking
|
||||
TYPE_NEMA_L115P = 'nema-l1-15p'
|
||||
TYPE_NEMA_L515P = 'nema-l5-15p'
|
||||
TYPE_NEMA_L520P = 'nema-l5-20p'
|
||||
TYPE_NEMA_L530P = 'nema-l5-30p'
|
||||
TYPE_NEMA_L615P = 'nema-l5-50p'
|
||||
TYPE_NEMA_L550P = 'nema-l5-50p'
|
||||
TYPE_NEMA_L615P = 'nema-l6-15p'
|
||||
TYPE_NEMA_L620P = 'nema-l6-20p'
|
||||
TYPE_NEMA_L630P = 'nema-l6-30p'
|
||||
TYPE_NEMA_L650P = 'nema-l6-50p'
|
||||
TYPE_NEMA_L1030P = 'nema-l10-30p'
|
||||
TYPE_NEMA_L1420P = 'nema-l14-20p'
|
||||
TYPE_NEMA_L1430P = 'nema-l14-30p'
|
||||
TYPE_NEMA_L1450P = 'nema-l14-50p'
|
||||
TYPE_NEMA_L1460P = 'nema-l14-60p'
|
||||
TYPE_NEMA_L2120P = 'nema-l21-20p'
|
||||
TYPE_NEMA_L2130P = 'nema-l21-30p'
|
||||
# California style
|
||||
@@ -324,6 +336,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_IEC_3PNE9H, '3P+N+E 9H'),
|
||||
)),
|
||||
('NEMA (Non-locking)', (
|
||||
(TYPE_NEMA_115P, 'NEMA 1-15P'),
|
||||
(TYPE_NEMA_515P, 'NEMA 5-15P'),
|
||||
(TYPE_NEMA_520P, 'NEMA 5-20P'),
|
||||
(TYPE_NEMA_530P, 'NEMA 5-30P'),
|
||||
@@ -332,17 +345,28 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_620P, 'NEMA 6-20P'),
|
||||
(TYPE_NEMA_630P, 'NEMA 6-30P'),
|
||||
(TYPE_NEMA_650P, 'NEMA 6-50P'),
|
||||
(TYPE_NEMA_1030P, 'NEMA 10-30P'),
|
||||
(TYPE_NEMA_1050P, 'NEMA 10-50P'),
|
||||
(TYPE_NEMA_1420P, 'NEMA 14-20P'),
|
||||
(TYPE_NEMA_1430P, 'NEMA 14-30P'),
|
||||
(TYPE_NEMA_1450P, 'NEMA 14-50P'),
|
||||
(TYPE_NEMA_1460P, 'NEMA 14-60P'),
|
||||
)),
|
||||
('NEMA (Locking)', (
|
||||
(TYPE_NEMA_L115P, 'NEMA L1-15P'),
|
||||
(TYPE_NEMA_L515P, 'NEMA L5-15P'),
|
||||
(TYPE_NEMA_L520P, 'NEMA L5-20P'),
|
||||
(TYPE_NEMA_L530P, 'NEMA L5-30P'),
|
||||
(TYPE_NEMA_L550P, 'NEMA L5-50P'),
|
||||
(TYPE_NEMA_L615P, 'NEMA L6-15P'),
|
||||
(TYPE_NEMA_L620P, 'NEMA L6-20P'),
|
||||
(TYPE_NEMA_L630P, 'NEMA L6-30P'),
|
||||
(TYPE_NEMA_L650P, 'NEMA L6-50P'),
|
||||
(TYPE_NEMA_L1030P, 'NEMA L10-30P'),
|
||||
(TYPE_NEMA_L1420P, 'NEMA L14-20P'),
|
||||
(TYPE_NEMA_L1430P, 'NEMA L14-30P'),
|
||||
(TYPE_NEMA_L1450P, 'NEMA L14-50P'),
|
||||
(TYPE_NEMA_L1460P, 'NEMA L14-60P'),
|
||||
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
|
||||
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
|
||||
)),
|
||||
@@ -397,6 +421,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
|
||||
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
|
||||
# NEMA non-locking
|
||||
TYPE_NEMA_115R = 'nema-1-15r'
|
||||
TYPE_NEMA_515R = 'nema-5-15r'
|
||||
TYPE_NEMA_520R = 'nema-5-20r'
|
||||
TYPE_NEMA_530R = 'nema-5-30r'
|
||||
@@ -405,16 +430,27 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_620R = 'nema-6-20r'
|
||||
TYPE_NEMA_630R = 'nema-6-30r'
|
||||
TYPE_NEMA_650R = 'nema-6-50r'
|
||||
TYPE_NEMA_1030R = 'nema-10-30r'
|
||||
TYPE_NEMA_1050R = 'nema-10-50r'
|
||||
TYPE_NEMA_1420R = 'nema-14-20r'
|
||||
TYPE_NEMA_1430R = 'nema-14-30r'
|
||||
TYPE_NEMA_1450R = 'nema-14-50r'
|
||||
TYPE_NEMA_1460R = 'nema-14-60r'
|
||||
# NEMA locking
|
||||
TYPE_NEMA_L115R = 'nema-l1-15r'
|
||||
TYPE_NEMA_L515R = 'nema-l5-15r'
|
||||
TYPE_NEMA_L520R = 'nema-l5-20r'
|
||||
TYPE_NEMA_L530R = 'nema-l5-30r'
|
||||
TYPE_NEMA_L615R = 'nema-l5-50r'
|
||||
TYPE_NEMA_L550R = 'nema-l5-50r'
|
||||
TYPE_NEMA_L615R = 'nema-l6-15r'
|
||||
TYPE_NEMA_L620R = 'nema-l6-20r'
|
||||
TYPE_NEMA_L630R = 'nema-l6-30r'
|
||||
TYPE_NEMA_L650R = 'nema-l6-50r'
|
||||
TYPE_NEMA_L1030R = 'nema-l10-30r'
|
||||
TYPE_NEMA_L1420R = 'nema-l14-20r'
|
||||
TYPE_NEMA_L1430R = 'nema-l14-30r'
|
||||
TYPE_NEMA_L1450R = 'nema-l14-50r'
|
||||
TYPE_NEMA_L1460R = 'nema-l14-60r'
|
||||
TYPE_NEMA_L2120R = 'nema-l21-20r'
|
||||
TYPE_NEMA_L2130R = 'nema-l21-30r'
|
||||
# California style
|
||||
@@ -462,6 +498,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_IEC_3PNE9H, '3P+N+E 9H'),
|
||||
)),
|
||||
('NEMA (Non-locking)', (
|
||||
(TYPE_NEMA_115R, 'NEMA 1-15R'),
|
||||
(TYPE_NEMA_515R, 'NEMA 5-15R'),
|
||||
(TYPE_NEMA_520R, 'NEMA 5-20R'),
|
||||
(TYPE_NEMA_530R, 'NEMA 5-30R'),
|
||||
@@ -470,17 +507,28 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_620R, 'NEMA 6-20R'),
|
||||
(TYPE_NEMA_630R, 'NEMA 6-30R'),
|
||||
(TYPE_NEMA_650R, 'NEMA 6-50R'),
|
||||
(TYPE_NEMA_1030R, 'NEMA 10-30R'),
|
||||
(TYPE_NEMA_1050R, 'NEMA 10-50R'),
|
||||
(TYPE_NEMA_1420R, 'NEMA 14-20R'),
|
||||
(TYPE_NEMA_1430R, 'NEMA 14-30R'),
|
||||
(TYPE_NEMA_1450R, 'NEMA 14-50R'),
|
||||
(TYPE_NEMA_1460R, 'NEMA 14-60R'),
|
||||
)),
|
||||
('NEMA (Locking)', (
|
||||
(TYPE_NEMA_L115R, 'NEMA L1-15R'),
|
||||
(TYPE_NEMA_L515R, 'NEMA L5-15R'),
|
||||
(TYPE_NEMA_L520R, 'NEMA L5-20R'),
|
||||
(TYPE_NEMA_L530R, 'NEMA L5-30R'),
|
||||
(TYPE_NEMA_L550R, 'NEMA L5-50R'),
|
||||
(TYPE_NEMA_L615R, 'NEMA L6-15R'),
|
||||
(TYPE_NEMA_L620R, 'NEMA L6-20R'),
|
||||
(TYPE_NEMA_L630R, 'NEMA L6-30R'),
|
||||
(TYPE_NEMA_L650R, 'NEMA L6-50R'),
|
||||
(TYPE_NEMA_L1030R, 'NEMA L10-30R'),
|
||||
(TYPE_NEMA_L1420R, 'NEMA L14-20R'),
|
||||
(TYPE_NEMA_L1430R, 'NEMA L14-30R'),
|
||||
(TYPE_NEMA_L1450R, 'NEMA L14-50R'),
|
||||
(TYPE_NEMA_L1460R, 'NEMA L14-60R'),
|
||||
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
|
||||
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
|
||||
)),
|
||||
|
||||
@@ -11,8 +11,6 @@ RACK_U_HEIGHT_DEFAULT = 42
|
||||
|
||||
RACK_ELEVATION_BORDER_WIDTH = 2
|
||||
RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30
|
||||
RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 220
|
||||
RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 22
|
||||
|
||||
|
||||
#
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -79,42 +79,42 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebay',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_consoleports,
|
||||
|
||||
@@ -75,37 +75,37 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebaytemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_consoleporttemplates,
|
||||
|
||||
@@ -43,17 +43,17 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize, null=True),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_sites,
|
||||
|
||||
@@ -35,12 +35,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interfacetemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_interfacetemplates,
|
||||
|
||||
18
netbox/dcim/migrations/0109_interface_remove_vm.py
Normal file
18
netbox/dcim/migrations/0109_interface_remove_vm.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.6 on 2020-06-22 16:03
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0108_add_tags'),
|
||||
('virtualization', '0016_replicate_interfaces'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='interface',
|
||||
name='virtual_machine',
|
||||
),
|
||||
]
|
||||
46
netbox/dcim/migrations/0110_virtualchassis_name.py
Normal file
46
netbox/dcim/migrations/0110_virtualchassis_name.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def copy_master_name(apps, schema_editor):
|
||||
"""
|
||||
Copy the master device's name to the VirtualChassis.
|
||||
"""
|
||||
VirtualChassis = apps.get_model('dcim', 'VirtualChassis')
|
||||
|
||||
for vc in VirtualChassis.objects.prefetch_related('master'):
|
||||
name = vc.master.name if vc.master.name else f'Unnamed VC #{vc.pk}'
|
||||
VirtualChassis.objects.filter(pk=vc.pk).update(name=name)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0109_interface_remove_vm'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='virtualchassis',
|
||||
options={'ordering': ['name'], 'verbose_name_plural': 'virtual chassis'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='virtualchassis',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='virtualchassis',
|
||||
name='master',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=copy_master_name,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='virtualchassis',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 3.0.6 on 2020-06-30 18:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0110_virtualchassis_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebaytemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontporttemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interfacetemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearporttemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
]
|
||||
@@ -35,11 +35,12 @@ from .device_component_templates import (
|
||||
PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||
)
|
||||
from .device_components import (
|
||||
CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, PowerOutlet,
|
||||
PowerPort, RearPort,
|
||||
BaseInterface, CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem,
|
||||
PowerOutlet, PowerPort, RearPort,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'BaseInterface',
|
||||
'Cable',
|
||||
'CableTermination',
|
||||
'ConsolePort',
|
||||
@@ -579,7 +580,11 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
if self.pk:
|
||||
# Validate that Rack is tall enough to house the installed Devices
|
||||
top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('-position').first()
|
||||
top_device = Device.objects.unrestricted().filter(
|
||||
rack=self
|
||||
).exclude(
|
||||
position__isnull=True
|
||||
).order_by('-position').first()
|
||||
if top_device:
|
||||
min_height = top_device.position + top_device.device_type.u_height - 1
|
||||
if self.u_height < min_height:
|
||||
@@ -600,13 +605,13 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
# Record the original site assignment for this rack.
|
||||
_site_id = None
|
||||
if self.pk:
|
||||
_site_id = Rack.objects.get(pk=self.pk).site_id
|
||||
_site_id = Rack.objects.unrestricted().get(pk=self.pk).site_id
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Update racked devices if the assigned Site has been changed.
|
||||
if _site_id is not None and self.site_id != _site_id:
|
||||
devices = Device.objects.filter(rack=self)
|
||||
devices = Device.objects.unrestricted().filter(rack=self)
|
||||
for device in devices:
|
||||
device.site = self.site
|
||||
device.save()
|
||||
@@ -668,7 +673,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
# Add devices to rack units list
|
||||
if self.pk:
|
||||
queryset = Device.objects.prefetch_related(
|
||||
queryset = Device.objects.unrestricted().prefetch_related(
|
||||
'device_type',
|
||||
'device_type__manufacturer',
|
||||
'device_role'
|
||||
@@ -744,8 +749,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
def get_elevation_svg(
|
||||
self,
|
||||
face=DeviceFaceChoices.FACE_FRONT,
|
||||
unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
|
||||
unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT,
|
||||
unit_width=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH,
|
||||
unit_height=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT,
|
||||
legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT,
|
||||
include_images=True,
|
||||
base_url=None
|
||||
@@ -1124,7 +1129,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
# room to expand within their racks. This validation will impose a very high performance penalty when there are
|
||||
# many instances to check, but increasing the u_height of a DeviceType should be a very rare occurrence.
|
||||
if self.pk and self.u_height > self._original_u_height:
|
||||
for d in Device.objects.filter(device_type=self, position__isnull=False):
|
||||
for d in Device.objects.unrestricted().filter(device_type=self, position__isnull=False):
|
||||
face_required = None if self.is_full_depth else d.face
|
||||
u_available = d.rack.get_available_units(
|
||||
u_height=self.u_height,
|
||||
@@ -1139,7 +1144,10 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
# If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position.
|
||||
elif self.pk and self._original_u_height > 0 and self.u_height == 0:
|
||||
racked_instance_count = Device.objects.filter(device_type=self, position__isnull=False).count()
|
||||
racked_instance_count = Device.objects.unrestricted().filter(
|
||||
device_type=self,
|
||||
position__isnull=False
|
||||
).count()
|
||||
if racked_instance_count:
|
||||
url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}"
|
||||
raise ValidationError({
|
||||
@@ -1492,7 +1500,11 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
|
||||
# of the uniqueness constraint without manual intervention.
|
||||
if self.name and self.tenant is None:
|
||||
if Device.objects.exclude(pk=self.pk).filter(name=self.name, site=self.site, tenant__isnull=True):
|
||||
if Device.objects.unrestricted().exclude(pk=self.pk).filter(
|
||||
name=self.name,
|
||||
site=self.site,
|
||||
tenant__isnull=True
|
||||
):
|
||||
raise ValidationError({
|
||||
'name': 'A device with this name already exists.'
|
||||
})
|
||||
@@ -1571,9 +1583,9 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
raise ValidationError({
|
||||
'primary_ip4': f"{self.primary_ip4} is not an IPv4 address."
|
||||
})
|
||||
if self.primary_ip4.interface in vc_interfaces:
|
||||
if self.primary_ip4.assigned_object in vc_interfaces:
|
||||
pass
|
||||
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in vc_interfaces:
|
||||
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.assigned_object in vc_interfaces:
|
||||
pass
|
||||
else:
|
||||
raise ValidationError({
|
||||
@@ -1584,9 +1596,9 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
raise ValidationError({
|
||||
'primary_ip6': f"{self.primary_ip6} is not an IPv6 address."
|
||||
})
|
||||
if self.primary_ip6.interface in vc_interfaces:
|
||||
if self.primary_ip6.assigned_object in vc_interfaces:
|
||||
pass
|
||||
elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.interface in vc_interfaces:
|
||||
elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.assigned_object in vc_interfaces:
|
||||
pass
|
||||
else:
|
||||
raise ValidationError({
|
||||
@@ -1622,32 +1634,32 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
# If this is a new Device, instantiate all of the related components per the DeviceType definition
|
||||
if is_new:
|
||||
ConsolePort.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.consoleport_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.consoleport_templates.unrestricted()]
|
||||
)
|
||||
ConsoleServerPort.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.consoleserverport_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.consoleserverport_templates.unrestricted()]
|
||||
)
|
||||
PowerPort.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.powerport_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.powerport_templates.unrestricted()]
|
||||
)
|
||||
PowerOutlet.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.poweroutlet_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.poweroutlet_templates.unrestricted()]
|
||||
)
|
||||
Interface.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.interface_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.interface_templates.unrestricted()]
|
||||
)
|
||||
RearPort.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.rearport_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.rearport_templates.unrestricted()]
|
||||
)
|
||||
FrontPort.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.frontport_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.frontport_templates.unrestricted()]
|
||||
)
|
||||
DeviceBay.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.device_bay_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.device_bay_templates.unrestricted()]
|
||||
)
|
||||
|
||||
# Update Site and Rack assignment for any child Devices
|
||||
devices = Device.objects.filter(parent_bay__device=self)
|
||||
devices = Device.objects.unrestricted().filter(parent_bay__device=self)
|
||||
for device in devices:
|
||||
device.site = self.site
|
||||
device.rack = self.rack
|
||||
@@ -1738,7 +1750,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
"""
|
||||
Return the set of child Devices installed in DeviceBays within this Device.
|
||||
"""
|
||||
return Device.objects.filter(parent_bay__device=self.pk)
|
||||
return Device.objects.unrestricted().filter(parent_bay__device=self.pk)
|
||||
|
||||
def get_status_class(self):
|
||||
return self.STATUS_CLASS_MAP.get(self.status)
|
||||
@@ -1756,7 +1768,12 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
master = models.OneToOneField(
|
||||
to='Device',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='vc_master_for'
|
||||
related_name='vc_master_for',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
domain = models.CharField(
|
||||
max_length=30,
|
||||
@@ -1766,14 +1783,14 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['master', 'domain']
|
||||
csv_headers = ['name', 'domain', 'master']
|
||||
|
||||
class Meta:
|
||||
ordering = ['master']
|
||||
ordering = ['name']
|
||||
verbose_name_plural = 'virtual chassis'
|
||||
|
||||
def __str__(self):
|
||||
return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis'
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:virtualchassis', kwargs={'pk': self.pk})
|
||||
@@ -1782,15 +1799,15 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
|
||||
# Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
|
||||
# VirtualChassis.)
|
||||
if self.pk and self.master not in self.members.all():
|
||||
if self.pk and self.master and self.master not in self.members.all():
|
||||
raise ValidationError({
|
||||
'master': "The selected master is not assigned to this virtual chassis."
|
||||
'master': f"The selected master ({self.master}) is not assigned to this virtual chassis."
|
||||
})
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
# Check for LAG interfaces split across member chassis
|
||||
interfaces = Interface.objects.filter(
|
||||
interfaces = Interface.objects.unrestricted().filter(
|
||||
device__in=self.members.all(),
|
||||
lag__isnull=False
|
||||
).exclude(
|
||||
@@ -1798,8 +1815,7 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
)
|
||||
if interfaces:
|
||||
raise ProtectedError(
|
||||
"Unable to delete virtual chassis {}. There are member interfaces which form a cross-chassis "
|
||||
"LAG".format(self),
|
||||
f"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG",
|
||||
interfaces
|
||||
)
|
||||
|
||||
@@ -1807,8 +1823,9 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.master,
|
||||
self.name,
|
||||
self.domain,
|
||||
self.master.name if self.master else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -2158,12 +2175,13 @@ class Cable(ChangeLoggedModel):
|
||||
return reverse('dcim:cable', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
from circuits.models import CircuitTermination
|
||||
|
||||
# Validate that termination A exists
|
||||
if not hasattr(self, 'termination_a_type'):
|
||||
raise ValidationError('Termination A type has not been specified')
|
||||
try:
|
||||
self.termination_a_type.model_class().objects.get(pk=self.termination_a_id)
|
||||
self.termination_a_type.model_class().objects.unrestricted().get(pk=self.termination_a_id)
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError({
|
||||
'termination_a': 'Invalid ID for type {}'.format(self.termination_a_type)
|
||||
@@ -2173,7 +2191,7 @@ class Cable(ChangeLoggedModel):
|
||||
if not hasattr(self, 'termination_b_type'):
|
||||
raise ValidationError('Termination B type has not been specified')
|
||||
try:
|
||||
self.termination_b_type.model_class().objects.get(pk=self.termination_b_id)
|
||||
self.termination_b_type.model_class().objects.unrestricted().get(pk=self.termination_b_id)
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError({
|
||||
'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type)
|
||||
@@ -2220,19 +2238,21 @@ class Cable(ChangeLoggedModel):
|
||||
f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}"
|
||||
)
|
||||
|
||||
# A RearPort with multiple positions must be connected to a RearPort with an equal number of positions
|
||||
# Check that a RearPort with multiple positions isn't connected to an endpoint
|
||||
# or a RearPort with a different number of positions.
|
||||
for term_a, term_b in [
|
||||
(self.termination_a, self.termination_b),
|
||||
(self.termination_b, self.termination_a)
|
||||
]:
|
||||
if isinstance(term_a, RearPort) and term_a.positions > 1:
|
||||
if not isinstance(term_b, RearPort):
|
||||
if not isinstance(term_b, (FrontPort, RearPort, CircuitTermination)):
|
||||
raise ValidationError(
|
||||
"Rear ports with multiple positions may only be connected to other rear ports"
|
||||
"Rear ports with multiple positions may only be connected to other pass-through ports"
|
||||
)
|
||||
elif term_a.positions != term_b.positions:
|
||||
if isinstance(term_b, RearPort) and term_b.positions > 1 and term_a.positions != term_b.positions:
|
||||
raise ValidationError(
|
||||
f"{term_a} has {term_a.positions} position(s) but {term_b} has {term_b.positions}. "
|
||||
f"{term_a} of {term_a.device} has {term_a.positions} position(s) but "
|
||||
f"{term_b} of {term_b.device} has {term_b.positions}. "
|
||||
f"Both terminations must have the same number of positions."
|
||||
)
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ __all__ = (
|
||||
|
||||
|
||||
class ComponentTemplateModel(models.Model):
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -19,7 +19,6 @@ from utilities.ordering import naturalize_interface
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.utils import serialize_object
|
||||
from virtualization.choices import VMInterfaceTypeChoices
|
||||
|
||||
|
||||
__all__ = (
|
||||
@@ -53,18 +52,12 @@ class ComponentModel(models.Model):
|
||||
return self.name
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the parent Device/VM
|
||||
try:
|
||||
parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None)
|
||||
except ObjectDoesNotExist:
|
||||
# The parent device/VM has already been deleted
|
||||
parent = None
|
||||
|
||||
# Annotate the parent Device
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=parent,
|
||||
related_object=self.device,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
|
||||
@@ -94,16 +87,16 @@ class CableTermination(models.Model):
|
||||
object_id_field='termination_b_id'
|
||||
)
|
||||
|
||||
is_path_endpoint = True
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def trace(self):
|
||||
"""
|
||||
Return two items: the traceable portion of a cable path, and the termination points where it splits (if any).
|
||||
This occurs when the trace is initiated from a midpoint along a path which traverses a RearPort. In cases where
|
||||
the originating endpoint is unknown, it is not possible to know which corresponding FrontPort to follow.
|
||||
Return three items: the traceable portion of a cable path, the termination points where it splits (if any), and
|
||||
the remaining positions on the position stack (if any). Splits occur when the trace is initiated from a midpoint
|
||||
along a path which traverses a RearPort. In cases where the originating endpoint is unknown, it is not possible
|
||||
to know which corresponding FrontPort to follow. Remaining positions occur when tracing a path that traverses
|
||||
a FrontPort without traversing a RearPort again.
|
||||
|
||||
The path is a list representing a complete cable path, with each individual segment represented as a
|
||||
three-tuple:
|
||||
@@ -123,26 +116,35 @@ class CableTermination(models.Model):
|
||||
|
||||
# Map a front port to its corresponding rear port
|
||||
if isinstance(termination, FrontPort):
|
||||
position_stack.append(termination.rear_port_position)
|
||||
# Retrieve the corresponding RearPort from database to ensure we have an up-to-date instance
|
||||
peer_port = RearPort.objects.get(pk=termination.rear_port.pk)
|
||||
|
||||
# Don't use the stack for RearPorts with a single position. Only remember the position at
|
||||
# many-to-one points so we can select the correct FrontPort when we reach the corresponding
|
||||
# one-to-many point.
|
||||
if peer_port.positions > 1:
|
||||
position_stack.append(termination)
|
||||
|
||||
return peer_port
|
||||
|
||||
# Map a rear port/position to its corresponding front port
|
||||
elif isinstance(termination, RearPort):
|
||||
if termination.positions > 1:
|
||||
# Can't map to a FrontPort without a position if there are multiple options
|
||||
if not position_stack:
|
||||
raise CableTraceSplit(termination)
|
||||
|
||||
# Can't map to a FrontPort without a position if there are multiple options
|
||||
if termination.positions > 1 and not position_stack:
|
||||
raise CableTraceSplit(termination)
|
||||
front_port = position_stack.pop()
|
||||
position = front_port.rear_port_position
|
||||
|
||||
# We can assume position 1 if the RearPort has only one position
|
||||
position = position_stack.pop() if position_stack else 1
|
||||
|
||||
# Validate the position
|
||||
if position not in range(1, termination.positions + 1):
|
||||
raise Exception("Invalid position for {} ({} positions): {})".format(
|
||||
termination, termination.positions, position
|
||||
))
|
||||
# Validate the position
|
||||
if position not in range(1, termination.positions + 1):
|
||||
raise Exception("Invalid position for {} ({} positions): {})".format(
|
||||
termination, termination.positions, position
|
||||
))
|
||||
else:
|
||||
# Don't use the stack for RearPorts with a single position. The only possible position is 1.
|
||||
position = 1
|
||||
|
||||
try:
|
||||
peer_port = FrontPort.objects.get(
|
||||
@@ -173,12 +175,12 @@ class CableTermination(models.Model):
|
||||
if not endpoint.cable:
|
||||
path.append((endpoint, None, None))
|
||||
logger.debug("No cable connected")
|
||||
return path, None
|
||||
return path, None, position_stack
|
||||
|
||||
# Check for loops
|
||||
if endpoint.cable in [segment[1] for segment in path]:
|
||||
logger.debug("Loop detected!")
|
||||
return path, None
|
||||
return path, None, position_stack
|
||||
|
||||
# Record the current segment in the path
|
||||
far_end = endpoint.get_cable_peer()
|
||||
@@ -191,10 +193,10 @@ class CableTermination(models.Model):
|
||||
try:
|
||||
endpoint = get_peer_port(far_end)
|
||||
except CableTraceSplit as e:
|
||||
return path, e.termination.frontports.all()
|
||||
return path, e.termination.frontports.all(), position_stack
|
||||
|
||||
if endpoint is None:
|
||||
return path, None
|
||||
return path, None, position_stack
|
||||
|
||||
def get_cable_peer(self):
|
||||
if self.cable is None:
|
||||
@@ -211,7 +213,7 @@ class CableTermination(models.Model):
|
||||
endpoints = []
|
||||
|
||||
# Get the far end of the last path segment
|
||||
path, split_ends = self.trace()
|
||||
path, split_ends, position_stack = self.trace()
|
||||
endpoint = path[-1][2]
|
||||
if split_ends is not None:
|
||||
for termination in split_ends:
|
||||
@@ -275,7 +277,7 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
return reverse('dcim:consoleport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@@ -332,7 +334,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
return reverse('dcim:consoleserverport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@@ -415,7 +417,7 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
return reverse('dcim:powerport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@@ -567,7 +569,7 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
return reverse('dcim:poweroutlet', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@@ -592,26 +594,7 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
@extras_features('graphs', 'export_templates', 'webhooks')
|
||||
class Interface(CableTermination, ComponentModel):
|
||||
"""
|
||||
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
|
||||
Interface.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='Device',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='interfaces',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
virtual_machine = models.ForeignKey(
|
||||
to='virtualization.VirtualMachine',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='interfaces',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
class BaseInterface(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
@@ -621,6 +604,42 @@ class Interface(CableTermination, ComponentModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
default=True
|
||||
)
|
||||
mac_address = MACAddressField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='MAC Address'
|
||||
)
|
||||
mtu = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(65536)],
|
||||
verbose_name='MTU'
|
||||
)
|
||||
mode = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfaceModeChoices,
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
@extras_features('graphs', 'export_templates', 'webhooks')
|
||||
class Interface(CableTermination, ComponentModel, BaseInterface):
|
||||
"""
|
||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='Device',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='interfaces',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
@@ -656,30 +675,11 @@ class Interface(CableTermination, ComponentModel):
|
||||
max_length=50,
|
||||
choices=InterfaceTypeChoices
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
default=True
|
||||
)
|
||||
mac_address = MACAddressField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='MAC Address'
|
||||
)
|
||||
mtu = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(65536)],
|
||||
verbose_name='MTU'
|
||||
)
|
||||
mgmt_only = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name='OOB Management',
|
||||
help_text='This interface is used only for out-of-band management'
|
||||
)
|
||||
mode = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfaceModeChoices,
|
||||
blank=True
|
||||
)
|
||||
untagged_vlan = models.ForeignKey(
|
||||
to='ipam.VLAN',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -694,15 +694,19 @@ class Interface(CableTermination, ComponentModel):
|
||||
blank=True,
|
||||
verbose_name='Tagged VLANs'
|
||||
)
|
||||
ip_addresses = GenericRelation(
|
||||
to='ipam.IPAddress',
|
||||
content_type_field='assigned_object_type',
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='interface'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'device', 'virtual_machine', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
|
||||
'description', 'mode',
|
||||
'device', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
# TODO: ordering and unique_together should include virtual_machine
|
||||
ordering = ('device', CollateAsChar('_name'))
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
@@ -712,7 +716,6 @@ class Interface(CableTermination, ComponentModel):
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier if self.device else None,
|
||||
self.virtual_machine.name if self.virtual_machine else None,
|
||||
self.name,
|
||||
self.lag.name if self.lag else None,
|
||||
self.get_type_display(),
|
||||
@@ -726,18 +729,6 @@ class Interface(CableTermination, ComponentModel):
|
||||
|
||||
def clean(self):
|
||||
|
||||
# An Interface must belong to a Device *or* to a VirtualMachine
|
||||
if self.device and self.virtual_machine:
|
||||
raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
|
||||
if not self.device and not self.virtual_machine:
|
||||
raise ValidationError("An interface must belong to either a device or a virtual machine.")
|
||||
|
||||
# VM interfaces must be virtual
|
||||
if self.virtual_machine and self.type not in VMInterfaceTypeChoices.values():
|
||||
raise ValidationError({
|
||||
'type': "Invalid interface type for a virtual machine: {}".format(self.type)
|
||||
})
|
||||
|
||||
# Virtual interfaces cannot be connected
|
||||
if self.type in NONCONNECTABLE_IFACE_TYPES and (
|
||||
self.cable or getattr(self, 'circuit_termination', False)
|
||||
@@ -773,7 +764,7 @@ class Interface(CableTermination, ComponentModel):
|
||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
|
||||
raise ValidationError({
|
||||
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
|
||||
"device/VM, or it must be global".format(self.untagged_vlan)
|
||||
"device, or it must be global".format(self.untagged_vlan)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -788,21 +779,6 @@ class Interface(CableTermination, ComponentModel):
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the parent Device/VM
|
||||
try:
|
||||
parent_obj = self.device or self.virtual_machine
|
||||
except ObjectDoesNotExist:
|
||||
parent_obj = None
|
||||
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=parent_obj,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
|
||||
@property
|
||||
def connected_endpoint(self):
|
||||
"""
|
||||
@@ -841,7 +817,7 @@ class Interface(CableTermination, ComponentModel):
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.device or self.virtual_machine
|
||||
return self.device
|
||||
|
||||
@property
|
||||
def is_connectable(self):
|
||||
@@ -902,7 +878,6 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
|
||||
is_path_endpoint = False
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@@ -914,6 +889,9 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:frontport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
@@ -970,7 +948,6 @@ class RearPort(CableTermination, ComponentModel):
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'positions', 'description']
|
||||
is_path_endpoint = False
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@@ -979,6 +956,9 @@ class RearPort(CableTermination, ComponentModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rearport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
@@ -1038,7 +1018,7 @@ class DeviceBay(ComponentModel):
|
||||
return '{} - {}'.format(self.device.name, self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
return reverse('dcim:devicebay', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@@ -1147,7 +1127,7 @@ class InventoryItem(ComponentModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device_inventory', kwargs={'pk': self.device.pk})
|
||||
return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
|
||||
@@ -4,20 +4,19 @@ from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .choices import CableStatusChoices
|
||||
from .models import Cable, Device, VirtualChassis
|
||||
from .models import Cable, CableTermination, Device, FrontPort, RearPort, VirtualChassis
|
||||
|
||||
|
||||
@receiver(post_save, sender=VirtualChassis)
|
||||
def assign_virtualchassis_master(instance, created, **kwargs):
|
||||
"""
|
||||
When a VirtualChassis is created, automatically assign its master device to the VC.
|
||||
When a VirtualChassis is created, automatically assign its master device (if any) to the VC.
|
||||
"""
|
||||
if created:
|
||||
devices = Device.objects.filter(pk=instance.master.pk)
|
||||
for device in devices:
|
||||
device.virtual_chassis = instance
|
||||
device.vc_position = None
|
||||
device.save()
|
||||
if created and instance.master:
|
||||
master = Device.objects.get(pk=instance.master.pk)
|
||||
master.virtual_chassis = instance
|
||||
master.vc_position = 1
|
||||
master.save()
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=VirtualChassis)
|
||||
@@ -52,7 +51,7 @@ def update_connected_endpoints(instance, **kwargs):
|
||||
# Update any endpoints for this Cable.
|
||||
endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
|
||||
for endpoint in endpoints:
|
||||
path, split_ends = endpoint.trace()
|
||||
path, split_ends, position_stack = endpoint.trace()
|
||||
# Determine overall path status (connected or planned)
|
||||
path_status = True
|
||||
for segment in path:
|
||||
@@ -61,9 +60,11 @@ def update_connected_endpoints(instance, **kwargs):
|
||||
break
|
||||
|
||||
endpoint_a = path[0][0]
|
||||
endpoint_b = path[-1][2]
|
||||
endpoint_b = path[-1][2] if not split_ends and not position_stack else None
|
||||
|
||||
if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False):
|
||||
# Patch panel ports are not connected endpoints, all other cable terminations are
|
||||
if isinstance(endpoint_a, CableTermination) and not isinstance(endpoint_a, (FrontPort, RearPort)) and \
|
||||
isinstance(endpoint_b, CableTermination) and not isinstance(endpoint_b, (FrontPort, RearPort)):
|
||||
logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b))
|
||||
endpoint_a.connected_endpoint = endpoint_b
|
||||
endpoint_a.connection_status = path_status
|
||||
|
||||
@@ -2,7 +2,9 @@ import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ColoredLabelColumn, TagColumn, ToggleColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, ColoredLabelColumn, TagColumn, ToggleColumn,
|
||||
)
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
@@ -40,69 +42,16 @@ DEVICE_LINK = """
|
||||
</a>
|
||||
"""
|
||||
|
||||
REGION_ACTIONS = """
|
||||
<a href="{% url 'dcim:region_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_region %}
|
||||
<a href="{% url 'dcim:region_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACKGROUP_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackgroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
RACKGROUP_ELEVATIONS = """
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackgroup %}
|
||||
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning" title="Edit">
|
||||
<i class="glyphicon glyphicon-pencil"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACKROLE_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackrole_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackrole %}
|
||||
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACK_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
RACKRESERVATION_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackreservation_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
MANUFACTURER_ACTIONS = """
|
||||
<a href="{% url 'dcim:manufacturer_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_manufacturer %}
|
||||
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_ACTIONS = """
|
||||
<a href="{% url 'dcim:devicerole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
@@ -119,15 +68,6 @@ PLATFORM_VM_COUNT = """
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
PLATFORM_ACTIONS = """
|
||||
<a href="{% url 'dcim:platform_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_platform %}
|
||||
<a href="{% url 'dcim:platform_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
"""
|
||||
@@ -198,11 +138,7 @@ class RegionTable(BaseTable):
|
||||
site_count = tables.Column(
|
||||
verbose_name='Sites'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=REGION_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(Region)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Region
|
||||
@@ -260,10 +196,9 @@ class RackGroupTable(BaseTable):
|
||||
rack_count = tables.Column(
|
||||
verbose_name='Racks'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKGROUP_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
actions = ButtonsColumn(
|
||||
model=RackGroup,
|
||||
prepend_template=RACKGROUP_ELEVATIONS
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -280,11 +215,7 @@ class RackRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
rack_count = tables.Column(verbose_name='Racks')
|
||||
color = tables.TemplateColumn(COLOR_LABEL)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(RackRole)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackRole
|
||||
@@ -386,11 +317,7 @@ class RackReservationTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:rackreservation_list'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKRESERVATION_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(RackReservation)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackReservation
|
||||
@@ -420,11 +347,7 @@ class ManufacturerTable(BaseTable):
|
||||
verbose_name='Platforms'
|
||||
)
|
||||
slug = tables.Column()
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=MANUFACTURER_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(Manufacturer, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Manufacturer
|
||||
@@ -486,22 +409,10 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class ConsolePortImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('device', 'name', 'description')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('consoleserverporttemplate'),
|
||||
@@ -511,22 +422,10 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class ConsoleServerPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = ('device', 'name', 'description')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class PowerPortTemplateTable(ComponentTemplateTable):
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('powerporttemplate'),
|
||||
@@ -536,22 +435,10 @@ class PowerPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class PowerPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('device', 'name', 'description', 'maximum_draw', 'allocated_draw')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class PowerOutletTemplateTable(ComponentTemplateTable):
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('poweroutlettemplate'),
|
||||
@@ -561,22 +448,10 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutletTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class PowerOutletImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = ('device', 'name', 'description', 'power_port', 'feed_leg')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class InterfaceTemplateTable(ComponentTemplateTable):
|
||||
mgmt_only = BooleanColumn(
|
||||
verbose_name='Management Only'
|
||||
@@ -589,30 +464,10 @@ class InterfaceTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InterfaceTemplate
|
||||
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class InterfaceImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
virtual_machine = tables.LinkColumn(
|
||||
viewname='virtualization:virtualmachine',
|
||||
args=[Accessor('virtual_machine.pk')],
|
||||
verbose_name='Virtual Machine'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu',
|
||||
'mgmt_only', 'mode',
|
||||
)
|
||||
empty_text = False
|
||||
|
||||
|
||||
class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name='Position'
|
||||
@@ -625,22 +480,10 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = FrontPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class FrontPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = FrontPort
|
||||
fields = ('device', 'name', 'description', 'type', 'rear_port', 'rear_port_position')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class RearPortTemplateTable(ComponentTemplateTable):
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('rearporttemplate'),
|
||||
@@ -650,22 +493,10 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RearPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'positions', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'positions', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class RearPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RearPort
|
||||
fields = ('device', 'name', 'description', 'type', 'position')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('devicebaytemplate'),
|
||||
@@ -675,7 +506,7 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceBayTemplate
|
||||
fields = ('pk', 'name', 'label', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
@@ -701,11 +532,8 @@ class DeviceRoleTable(BaseTable):
|
||||
template_code=COLOR_LABEL,
|
||||
verbose_name='Label'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
vm_role = BooleanColumn()
|
||||
actions = ButtonsColumn(DeviceRole, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
@@ -731,11 +559,7 @@ class PlatformTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='VMs'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=PLATFORM_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(Platform, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Platform
|
||||
@@ -861,153 +685,108 @@ class DeviceImportTable(BaseTable):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class DeviceComponentDetailTable(BaseTable):
|
||||
class DeviceComponentTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
cable = tables.LinkColumn()
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
order_by=('_name',)
|
||||
)
|
||||
cable = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
order_by = ('device', 'name')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
||||
sequence = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
||||
|
||||
|
||||
class ConsolePortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
class ConsolePortTable(DeviceComponentTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('name', 'label', 'type')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
class ConsolePortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
class ConsoleServerPortTable(DeviceComponentTable):
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class ConsoleServerPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = ('name', 'label', 'description')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
class PowerPortTable(DeviceComponentTable):
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class PowerPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('name', 'label', 'type')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||
|
||||
|
||||
class PowerPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
class PowerOutletTable(DeviceComponentTable):
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class PowerOutletTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = ('name', 'label', 'type', 'description')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||
|
||||
|
||||
class PowerOutletDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class InterfaceTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('name', 'label', 'type', 'lag', 'enabled', 'mgmt_only', 'description')
|
||||
|
||||
|
||||
class InterfaceDetailTable(DeviceComponentDetailTable):
|
||||
parent = tables.LinkColumn(order_by=('device', 'virtual_machine'))
|
||||
name = tables.LinkColumn()
|
||||
class InterfaceTable(DeviceComponentTable):
|
||||
enabled = BooleanColumn()
|
||||
|
||||
class Meta(InterfaceTable.Meta):
|
||||
order_by = ('parent', 'name')
|
||||
fields = ('pk', 'parent', 'name', 'label', 'enabled', 'type', 'description', 'cable')
|
||||
sequence = ('pk', 'parent', 'name', 'label', 'enabled', 'type', 'description', 'cable')
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'description', 'cable',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
|
||||
class FrontPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
class FrontPortTable(DeviceComponentTable):
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name='Position'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = FrontPort
|
||||
fields = ('name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
|
||||
empty_text = "None"
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
|
||||
|
||||
|
||||
class FrontPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
class RearPortTable(DeviceComponentTable):
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class RearPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = RearPort
|
||||
fields = ('name', 'label', 'type', 'positions', 'description')
|
||||
empty_text = "None"
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
class RearPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
class DeviceBayTable(DeviceComponentTable):
|
||||
installed_device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class DeviceBayTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('name', 'label', 'description')
|
||||
|
||||
|
||||
class DeviceBayDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
installed_device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceBayTable.Meta):
|
||||
fields = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
||||
sequence = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
||||
exclude = ('cable',)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
||||
|
||||
|
||||
class DeviceBayImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
installed_device = tables.LinkColumn('dcim:device', args=[Accessor('installed_device.pk')], verbose_name='Installed Device')
|
||||
class InventoryItemTable(DeviceComponentTable):
|
||||
manufacturer = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
discovered = BooleanColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('device', 'name', 'installed_device', 'description')
|
||||
empty_text = False
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered'
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
|
||||
|
||||
#
|
||||
@@ -1152,29 +931,6 @@ class InterfaceConnectionTable(BaseTable):
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# InventoryItems
|
||||
#
|
||||
|
||||
class InventoryItemTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device_inventory',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
accessor=Accessor('manufacturer')
|
||||
)
|
||||
discovered = BooleanColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered'
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
|
||||
|
||||
#
|
||||
# Virtual chassis
|
||||
#
|
||||
@@ -1182,7 +938,9 @@ class InventoryItemTable(BaseTable):
|
||||
class VirtualChassisTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
accessor=Accessor('master__name'),
|
||||
linkify=True
|
||||
)
|
||||
master = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
member_count = tables.Column(
|
||||
@@ -1194,8 +952,8 @@ class VirtualChassisTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VirtualChassis
|
||||
fields = ('pk', 'name', 'domain', 'member_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'domain', 'member_count')
|
||||
fields = ('pk', 'name', 'domain', 'master', 'member_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -28,6 +28,43 @@ class AppTest(APITestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class Mixins:
|
||||
|
||||
class ComponentTraceMixin(APITestCase):
|
||||
peer_termination_type = None
|
||||
|
||||
def test_trace(self):
|
||||
"""
|
||||
Test tracing a device component's attached cable.
|
||||
"""
|
||||
obj = self.model.objects.unrestricted().first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.unrestricted().first(),
|
||||
device_type=DeviceType.objects.unrestricted().first(),
|
||||
device_role=DeviceRole.objects.unrestricted().first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
if self.peer_termination_type is None:
|
||||
raise NotImplementedError("Test case must set peer_termination_type")
|
||||
peer_obj = self.peer_termination_type.objects.create(
|
||||
device=peer_device,
|
||||
name='Peer Termination'
|
||||
)
|
||||
cable = Cable(termination_a=obj, termination_b=peer_obj, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
|
||||
url = reverse(f'dcim-api:{self.model._meta.model_name}-trace', kwargs={'pk': obj.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], obj.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], peer_obj.name)
|
||||
|
||||
|
||||
class RegionTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Region
|
||||
brief_fields = ['id', 'name', 'site_count', 'slug', 'url']
|
||||
@@ -107,7 +144,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
|
||||
Graph.objects.bulk_create(graphs)
|
||||
|
||||
self.add_permissions('dcim.view_site')
|
||||
url = reverse('dcim-api:site-graphs', kwargs={'pk': Site.objects.first().pk})
|
||||
url = reverse('dcim-api:site-graphs', kwargs={'pk': Site.objects.unrestricted().unrestricted().first().pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(len(response.data), 3)
|
||||
@@ -246,7 +283,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
"""
|
||||
GET a single rack elevation.
|
||||
"""
|
||||
rack = Rack.objects.first()
|
||||
rack = Rack.objects.unrestricted().first()
|
||||
self.add_permissions('dcim.view_rack')
|
||||
url = reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk})
|
||||
|
||||
@@ -266,7 +303,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
"""
|
||||
GET a single rack elevation in SVG format.
|
||||
"""
|
||||
rack = Rack.objects.first()
|
||||
rack = Rack.objects.unrestricted().first()
|
||||
self.add_permissions('dcim.view_rack')
|
||||
url = '{}?render=svg'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk}))
|
||||
|
||||
@@ -281,9 +318,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
user = User.objects.create(username='user1', is_active=True)
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
|
||||
cls.racks = (
|
||||
@@ -878,7 +913,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
Graph.objects.bulk_create(graphs)
|
||||
|
||||
self.add_permissions('dcim.view_device')
|
||||
url = reverse('dcim-api:device-graphs', kwargs={'pk': Device.objects.first().pk})
|
||||
url = reverse('dcim-api:device-graphs', kwargs={'pk': Device.objects.unrestricted().first().pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(len(response.data), 3)
|
||||
@@ -908,7 +943,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
"""
|
||||
Check that creating a device with a duplicate name within a site fails.
|
||||
"""
|
||||
device = Device.objects.first()
|
||||
device = Device.objects.unrestricted().first()
|
||||
data = {
|
||||
'device_type': device.device_type.pk,
|
||||
'device_role': device.device_role.pk,
|
||||
@@ -923,9 +958,10 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ConsolePortTest(APIViewTestCases.APIViewTestCase):
|
||||
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = ConsolePort
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = ConsoleServerPort
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -957,39 +993,11 @@ class ConsolePortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_consoleport(self):
|
||||
"""
|
||||
Test tracing a ConsolePort cable.
|
||||
"""
|
||||
consoleport = ConsolePort.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
consoleserverport = ConsoleServerPort.objects.create(
|
||||
device=peer_device,
|
||||
name='Console Server Port 1'
|
||||
)
|
||||
cable = Cable(termination_a=consoleport, termination_b=consoleserverport, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_consoleport')
|
||||
url = reverse('dcim-api:consoleport-trace', kwargs={'pk': consoleport.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], consoleport.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], consoleserverport.name)
|
||||
|
||||
|
||||
class ConsoleServerPortTest(APIViewTestCases.APIViewTestCase):
|
||||
class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = ConsoleServerPort
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = ConsolePort
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1021,39 +1029,11 @@ class ConsoleServerPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_consoleserverport(self):
|
||||
"""
|
||||
Test tracing a ConsoleServerPort cable.
|
||||
"""
|
||||
consoleserverport = ConsoleServerPort.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
consoleport = ConsolePort.objects.create(
|
||||
device=peer_device,
|
||||
name='Console Port 1'
|
||||
)
|
||||
cable = Cable(termination_a=consoleserverport, termination_b=consoleport, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_consoleserverport')
|
||||
url = reverse('dcim-api:consoleserverport-trace', kwargs={'pk': consoleserverport.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], consoleserverport.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], consoleport.name)
|
||||
|
||||
|
||||
class PowerPortTest(APIViewTestCases.APIViewTestCase):
|
||||
class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = PowerPort
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = PowerOutlet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1085,39 +1065,11 @@ class PowerPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_powerport(self):
|
||||
"""
|
||||
Test tracing a PowerPort cable.
|
||||
"""
|
||||
powerport = PowerPort.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
poweroutlet = PowerOutlet.objects.create(
|
||||
device=peer_device,
|
||||
name='Power Outlet 1'
|
||||
)
|
||||
cable = Cable(termination_a=powerport, termination_b=poweroutlet, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_powerport')
|
||||
url = reverse('dcim-api:powerport-trace', kwargs={'pk': powerport.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], powerport.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], poweroutlet.name)
|
||||
|
||||
|
||||
class PowerOutletTest(APIViewTestCases.APIViewTestCase):
|
||||
class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = PowerOutlet
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = PowerPort
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1149,39 +1101,11 @@ class PowerOutletTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_poweroutlet(self):
|
||||
"""
|
||||
Test tracing a PowerOutlet cable.
|
||||
"""
|
||||
poweroutlet = PowerOutlet.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
powerport = PowerPort.objects.create(
|
||||
device=peer_device,
|
||||
name='Power Port 1'
|
||||
)
|
||||
cable = Cable(termination_a=poweroutlet, termination_b=powerport, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_poweroutlet')
|
||||
url = reverse('dcim-api:poweroutlet-trace', kwargs={'pk': poweroutlet.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], poweroutlet.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], powerport.name)
|
||||
|
||||
|
||||
class InterfaceTest(APIViewTestCases.APIViewTestCase):
|
||||
class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = Interface
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = Interface
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1245,45 +1169,17 @@ class InterfaceTest(APIViewTestCases.APIViewTestCase):
|
||||
Graph.objects.bulk_create(graphs)
|
||||
|
||||
self.add_permissions('dcim.view_interface')
|
||||
url = reverse('dcim-api:interface-graphs', kwargs={'pk': Interface.objects.first().pk})
|
||||
url = reverse('dcim-api:interface-graphs', kwargs={'pk': Interface.objects.unrestricted().first().pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(len(response.data), 3)
|
||||
self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?interface=Interface 1&foo=1')
|
||||
|
||||
def test_trace_interface(self):
|
||||
"""
|
||||
Test tracing an Interface cable.
|
||||
"""
|
||||
interface_a = Interface.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
interface_b = Interface.objects.create(
|
||||
device=peer_device,
|
||||
name='Interface X'
|
||||
)
|
||||
cable = Cable(termination_a=interface_a, termination_b=interface_b, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_interface')
|
||||
url = reverse('dcim-api:interface-trace', kwargs={'pk': interface_a.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], interface_a.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], interface_b.name)
|
||||
|
||||
|
||||
class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||
class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = FrontPort
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = Interface
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1334,39 +1230,11 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_frontport(self):
|
||||
"""
|
||||
Test tracing a FrontPort cable.
|
||||
"""
|
||||
frontport = FrontPort.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
interface = Interface.objects.create(
|
||||
device=peer_device,
|
||||
name='Interface X'
|
||||
)
|
||||
cable = Cable(termination_a=frontport, termination_b=interface, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_frontport')
|
||||
url = reverse('dcim-api:frontport-trace', kwargs={'pk': frontport.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], frontport.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], interface.name)
|
||||
|
||||
|
||||
class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
class RearPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = RearPort
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = Interface
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1401,35 +1269,6 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_rearport(self):
|
||||
"""
|
||||
Test tracing a RearPort cable.
|
||||
"""
|
||||
rearport = RearPort.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
interface = Interface.objects.create(
|
||||
device=peer_device,
|
||||
name='Interface X'
|
||||
)
|
||||
cable = Cable(termination_a=rearport, termination_b=interface, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_rearport')
|
||||
url = reverse('dcim-api:rearport-trace', kwargs={'pk': rearport.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], rearport.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], interface.name)
|
||||
|
||||
|
||||
class DeviceBayTest(APIViewTestCases.APIViewTestCase):
|
||||
model = DeviceBay
|
||||
@@ -1640,11 +1479,11 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Cable.objects.count(), 1)
|
||||
self.assertEqual(Cable.objects.unrestricted().count(), 1)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
consoleport1 = ConsolePort.objects.get(pk=consoleport1.pk)
|
||||
consoleserverport1 = ConsoleServerPort.objects.get(pk=consoleserverport1.pk)
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
consoleport1 = ConsolePort.objects.unrestricted().get(pk=consoleport1.pk)
|
||||
consoleserverport1 = ConsoleServerPort.objects.unrestricted().get(pk=consoleserverport1.pk)
|
||||
|
||||
self.assertEqual(cable.termination_a, consoleport1)
|
||||
self.assertEqual(cable.termination_b, consoleserverport1)
|
||||
@@ -1705,12 +1544,12 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
self.assertEqual(cable.termination_a.cable, cable)
|
||||
self.assertEqual(cable.termination_b.cable, cable)
|
||||
|
||||
consoleport1 = ConsolePort.objects.get(pk=consoleport1.pk)
|
||||
consoleserverport1 = ConsoleServerPort.objects.get(pk=consoleserverport1.pk)
|
||||
consoleport1 = ConsolePort.objects.unrestricted().get(pk=consoleport1.pk)
|
||||
consoleserverport1 = ConsoleServerPort.objects.unrestricted().get(pk=consoleserverport1.pk)
|
||||
self.assertEqual(consoleport1.connected_endpoint, consoleserverport1)
|
||||
self.assertEqual(consoleserverport1.connected_endpoint, consoleport1)
|
||||
|
||||
@@ -1735,11 +1574,11 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Cable.objects.count(), 1)
|
||||
self.assertEqual(Cable.objects.unrestricted().count(), 1)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
powerport1 = PowerPort.objects.get(pk=powerport1.pk)
|
||||
poweroutlet1 = PowerOutlet.objects.get(pk=poweroutlet1.pk)
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
powerport1 = PowerPort.objects.unrestricted().get(pk=powerport1.pk)
|
||||
poweroutlet1 = PowerOutlet.objects.unrestricted().get(pk=poweroutlet1.pk)
|
||||
|
||||
self.assertEqual(cable.termination_a, powerport1)
|
||||
self.assertEqual(cable.termination_b, poweroutlet1)
|
||||
@@ -1771,11 +1610,11 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Cable.objects.count(), 1)
|
||||
self.assertEqual(Cable.objects.unrestricted().count(), 1)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
interface1 = Interface.objects.get(pk=interface1.pk)
|
||||
interface2 = Interface.objects.get(pk=interface2.pk)
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
|
||||
interface2 = Interface.objects.unrestricted().get(pk=interface2.pk)
|
||||
|
||||
self.assertEqual(cable.termination_a, interface1)
|
||||
self.assertEqual(cable.termination_b, interface2)
|
||||
@@ -1836,12 +1675,12 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
self.assertEqual(cable.termination_a.cable, cable)
|
||||
self.assertEqual(cable.termination_b.cable, cable)
|
||||
|
||||
interface1 = Interface.objects.get(pk=interface1.pk)
|
||||
interface2 = Interface.objects.get(pk=interface2.pk)
|
||||
interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
|
||||
interface2 = Interface.objects.unrestricted().get(pk=interface2.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, interface2)
|
||||
self.assertEqual(interface2.connected_endpoint, interface1)
|
||||
|
||||
@@ -1875,11 +1714,11 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Cable.objects.count(), 1)
|
||||
self.assertEqual(Cable.objects.unrestricted().count(), 1)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
interface1 = Interface.objects.get(pk=interface1.pk)
|
||||
circuittermination1 = CircuitTermination.objects.get(pk=circuittermination1.pk)
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
|
||||
circuittermination1 = CircuitTermination.objects.unrestricted().get(pk=circuittermination1.pk)
|
||||
|
||||
self.assertEqual(cable.termination_a, interface1)
|
||||
self.assertEqual(cable.termination_b, circuittermination1)
|
||||
@@ -1949,12 +1788,12 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
self.assertEqual(cable.termination_a.cable, cable)
|
||||
self.assertEqual(cable.termination_b.cable, cable)
|
||||
|
||||
interface1 = Interface.objects.get(pk=interface1.pk)
|
||||
circuittermination1 = CircuitTermination.objects.get(pk=circuittermination1.pk)
|
||||
interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
|
||||
circuittermination1 = CircuitTermination.objects.unrestricted().get(pk=circuittermination1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, circuittermination1)
|
||||
self.assertEqual(circuittermination1.connected_endpoint, interface1)
|
||||
|
||||
@@ -2003,7 +1842,7 @@ class ConnectedDeviceTest(APITestCase):
|
||||
|
||||
class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualChassis
|
||||
brief_fields = ['id', 'master', 'member_count', 'url']
|
||||
brief_fields = ['id', 'master', 'member_count', 'name', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -2040,34 +1879,35 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
# Create three VirtualChassis with three members each
|
||||
virtual_chassis = (
|
||||
VirtualChassis(master=devices[0], domain='domain-1'),
|
||||
VirtualChassis(master=devices[3], domain='domain-2'),
|
||||
VirtualChassis(master=devices[6], domain='domain-3'),
|
||||
VirtualChassis(name='Virtual Chassis 1', master=devices[0], domain='domain-1'),
|
||||
VirtualChassis(name='Virtual Chassis 2', master=devices[3], domain='domain-2'),
|
||||
VirtualChassis(name='Virtual Chassis 3', master=devices[6], domain='domain-3'),
|
||||
)
|
||||
VirtualChassis.objects.bulk_create(virtual_chassis)
|
||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis[0], vc_position=2)
|
||||
Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=virtual_chassis[0], vc_position=3)
|
||||
Device.objects.filter(pk=devices[4].pk).update(virtual_chassis=virtual_chassis[1], vc_position=2)
|
||||
Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[1], vc_position=3)
|
||||
Device.objects.filter(pk=devices[7].pk).update(virtual_chassis=virtual_chassis[2], vc_position=2)
|
||||
Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=virtual_chassis[2], vc_position=3)
|
||||
Device.objects.unrestricted().filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis[0], vc_position=2)
|
||||
Device.objects.unrestricted().filter(pk=devices[2].pk).update(virtual_chassis=virtual_chassis[0], vc_position=3)
|
||||
Device.objects.unrestricted().filter(pk=devices[4].pk).update(virtual_chassis=virtual_chassis[1], vc_position=2)
|
||||
Device.objects.unrestricted().filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[1], vc_position=3)
|
||||
Device.objects.unrestricted().filter(pk=devices[7].pk).update(virtual_chassis=virtual_chassis[2], vc_position=2)
|
||||
Device.objects.unrestricted().filter(pk=devices[8].pk).update(virtual_chassis=virtual_chassis[2], vc_position=3)
|
||||
|
||||
cls.update_data = {
|
||||
'master': devices[1].pk,
|
||||
'name': 'Virtual Chassis X',
|
||||
'domain': 'domain-x',
|
||||
'master': devices[1].pk,
|
||||
}
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'master': devices[9].pk,
|
||||
'name': 'Virtual Chassis 4',
|
||||
'domain': 'domain-4',
|
||||
},
|
||||
{
|
||||
'master': devices[10].pk,
|
||||
'name': 'Virtual Chassis 5',
|
||||
'domain': 'domain-5',
|
||||
},
|
||||
{
|
||||
'master': devices[11].pk,
|
||||
'name': 'Virtual Chassis 6',
|
||||
'domain': 'domain-6',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1254,8 +1254,8 @@ class DeviceTestCase(TestCase):
|
||||
|
||||
# Assign primary IPs for filtering
|
||||
ipaddresses = (
|
||||
IPAddress(address='192.0.2.1/24', interface=interfaces[0]),
|
||||
IPAddress(address='192.0.2.2/24', interface=interfaces[1]),
|
||||
IPAddress(address='192.0.2.1/24', assigned_object=interfaces[0]),
|
||||
IPAddress(address='192.0.2.2/24', assigned_object=interfaces[1]),
|
||||
)
|
||||
IPAddress.objects.bulk_create(ipaddresses)
|
||||
Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0])
|
||||
|
||||
@@ -363,6 +363,7 @@ class CableTestCase(TestCase):
|
||||
)
|
||||
self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
|
||||
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
|
||||
self.interface3 = Interface.objects.create(device=self.device2, name='eth1')
|
||||
self.cable = Cable(termination_a=self.interface1, termination_b=self.interface2)
|
||||
self.cable.save()
|
||||
|
||||
@@ -370,10 +371,27 @@ class CableTestCase(TestCase):
|
||||
self.patch_pannel = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='TestPatchPannel', site=site
|
||||
)
|
||||
self.rear_port = RearPort.objects.create(device=self.patch_pannel, name='R1', type=1000)
|
||||
self.front_port = FrontPort.objects.create(
|
||||
device=self.patch_pannel, name='F1', type=1000, rear_port=self.rear_port
|
||||
self.rear_port1 = RearPort.objects.create(device=self.patch_pannel, name='RP1', type='8p8c')
|
||||
self.front_port1 = FrontPort.objects.create(
|
||||
device=self.patch_pannel, name='FP1', type='8p8c', rear_port=self.rear_port1, rear_port_position=1
|
||||
)
|
||||
self.rear_port2 = RearPort.objects.create(device=self.patch_pannel, name='RP2', type='8p8c', positions=2)
|
||||
self.front_port2 = FrontPort.objects.create(
|
||||
device=self.patch_pannel, name='FP2', type='8p8c', rear_port=self.rear_port2, rear_port_position=1
|
||||
)
|
||||
self.rear_port3 = RearPort.objects.create(device=self.patch_pannel, name='RP3', type='8p8c', positions=3)
|
||||
self.front_port3 = FrontPort.objects.create(
|
||||
device=self.patch_pannel, name='FP3', type='8p8c', rear_port=self.rear_port3, rear_port_position=1
|
||||
)
|
||||
self.rear_port4 = RearPort.objects.create(device=self.patch_pannel, name='RP4', type='8p8c', positions=3)
|
||||
self.front_port4 = FrontPort.objects.create(
|
||||
device=self.patch_pannel, name='FP4', type='8p8c', rear_port=self.rear_port4, rear_port_position=1
|
||||
)
|
||||
self.provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
self.circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
self.circuit = Circuit.objects.create(provider=self.provider, type=self.circuittype, cid='1')
|
||||
self.circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=site, term_side='A', port_speed=1000)
|
||||
self.circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=site, term_side='Z', port_speed=1000)
|
||||
|
||||
def test_cable_creation(self):
|
||||
"""
|
||||
@@ -405,7 +423,7 @@ class CableTestCase(TestCase):
|
||||
cable = Cable.objects.filter(pk=self.cable.pk).first()
|
||||
self.assertIsNone(cable)
|
||||
|
||||
def test_cable_validates_compatibale_types(self):
|
||||
def test_cable_validates_compatible_types(self):
|
||||
"""
|
||||
The clean method should have a check to ensure only compatible port types can be connected by a cable
|
||||
"""
|
||||
@@ -426,7 +444,7 @@ class CableTestCase(TestCase):
|
||||
"""
|
||||
A cable cannot connect a front port to its corresponding rear port
|
||||
"""
|
||||
cable = Cable(termination_a=self.front_port, termination_b=self.rear_port)
|
||||
cable = Cable(termination_a=self.front_port1, termination_b=self.rear_port1)
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
@@ -439,7 +457,94 @@ class CableTestCase(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
def test_cable_cannot_terminate_to_a_virtual_inteface(self):
|
||||
def test_connection_via_single_position_rearport(self):
|
||||
"""
|
||||
A RearPort with one position can be connected to anything.
|
||||
|
||||
[CableTermination X]---[RP(pos=1) FP]---[CableTermination Y]
|
||||
|
||||
is allowed anywhere
|
||||
|
||||
[CableTermination X]---[CableTermination Y]
|
||||
|
||||
is allowed.
|
||||
|
||||
A RearPort with multiple positions may not be directly connected to a path endpoint or another RearPort
|
||||
with a different number of positions. RearPorts with a single position on the other hand may be connected
|
||||
to such CableTerminations. Check that this is indeed allowed.
|
||||
"""
|
||||
# Connecting a single-position RearPort to a multi-position RearPort is ok
|
||||
Cable(termination_a=self.rear_port1, termination_b=self.rear_port2).full_clean()
|
||||
|
||||
# Connecting a single-position RearPort to an Interface is ok
|
||||
Cable(termination_a=self.rear_port1, termination_b=self.interface3).full_clean()
|
||||
|
||||
# Connecting a single-position RearPort to a CircuitTermination is ok
|
||||
Cable(termination_a=self.rear_port1, termination_b=self.circuittermination1).full_clean()
|
||||
|
||||
def test_connection_via_multi_position_rearport(self):
|
||||
"""
|
||||
A RearPort with multiple positions may not be directly connected to a path endpoint or another RearPort
|
||||
with a different number of positions.
|
||||
|
||||
The following scenario's are allowed (with x>1):
|
||||
|
||||
~----------+ +---------~
|
||||
| |
|
||||
RP2(pos=x)|---|RP(pos=x)
|
||||
| |
|
||||
~----------+ +---------~
|
||||
|
||||
~----------+ +---------~
|
||||
| |
|
||||
RP2(pos=x)|---|RP(pos=1)
|
||||
| |
|
||||
~----------+ +---------~
|
||||
|
||||
~----------+ +------------------~
|
||||
| |
|
||||
RP2(pos=x)|---|CircuitTermination
|
||||
| |
|
||||
~----------+ +------------------~
|
||||
|
||||
These scenarios are NOT allowed (with x>1):
|
||||
|
||||
~----------+ +----------~
|
||||
| |
|
||||
RP2(pos=x)|---|RP(pos!=x)
|
||||
| |
|
||||
~----------+ +----------~
|
||||
|
||||
~----------+ +----------~
|
||||
| |
|
||||
RP2(pos=x)|---|Interface
|
||||
| |
|
||||
~----------+ +----------~
|
||||
|
||||
These scenarios are tested in this order below.
|
||||
"""
|
||||
# Connecting a multi-position RearPort to another RearPort with the same number of positions is ok
|
||||
Cable(termination_a=self.rear_port3, termination_b=self.rear_port4).full_clean()
|
||||
|
||||
# Connecting a multi-position RearPort to a single-position RearPort is ok
|
||||
Cable(termination_a=self.rear_port2, termination_b=self.rear_port1).full_clean()
|
||||
|
||||
# Connecting a multi-position RearPort to a CircuitTermination is ok
|
||||
Cable(termination_a=self.rear_port2, termination_b=self.circuittermination1).full_clean()
|
||||
|
||||
with self.assertRaises(
|
||||
ValidationError,
|
||||
msg='Connecting a 2-position RearPort to a 3-position RearPort should fail'
|
||||
):
|
||||
Cable(termination_a=self.rear_port2, termination_b=self.rear_port3).full_clean()
|
||||
|
||||
with self.assertRaises(
|
||||
ValidationError,
|
||||
msg='Connecting a multi-position RearPort to an Interface should fail'
|
||||
):
|
||||
Cable(termination_a=self.rear_port2, termination_b=self.interface3).full_clean()
|
||||
|
||||
def test_cable_cannot_terminate_to_a_virtual_interface(self):
|
||||
"""
|
||||
A cable cannot terminate to a virtual interface
|
||||
"""
|
||||
@@ -448,7 +553,7 @@ class CableTestCase(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
def test_cable_cannot_terminate_to_a_wireless_inteface(self):
|
||||
def test_cable_cannot_terminate_to_a_wireless_interface(self):
|
||||
"""
|
||||
A cable cannot terminate to a wireless interface
|
||||
"""
|
||||
@@ -501,9 +606,13 @@ class CablePathTestCase(TestCase):
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 2', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 3', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 4', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 5', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 6', site=site),
|
||||
)
|
||||
Device.objects.bulk_create(patch_panels)
|
||||
for patch_panel in patch_panels:
|
||||
|
||||
# Create patch panels with 4 positions
|
||||
for patch_panel in patch_panels[:4]:
|
||||
rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=4, type=PortTypeChoices.TYPE_8P8C)
|
||||
FrontPort.objects.bulk_create((
|
||||
FrontPort(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C),
|
||||
@@ -512,6 +621,11 @@ class CablePathTestCase(TestCase):
|
||||
FrontPort(device=patch_panel, name='Front Port 4', rear_port=rearport, rear_port_position=4, type=PortTypeChoices.TYPE_8P8C),
|
||||
))
|
||||
|
||||
# Create 1-on-1 patch panels
|
||||
for patch_panel in patch_panels[4:]:
|
||||
rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=1, type=PortTypeChoices.TYPE_8P8C)
|
||||
FrontPort.objects.create(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C)
|
||||
|
||||
def test_direct_connection(self):
|
||||
"""
|
||||
Test a direct connection between two interfaces.
|
||||
@@ -524,6 +638,7 @@ class CablePathTestCase(TestCase):
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable.full_clean()
|
||||
cable.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@@ -551,22 +666,25 @@ class CablePathTestCase(TestCase):
|
||||
|
||||
def test_connection_via_single_rear_port(self):
|
||||
"""
|
||||
Test a connection which passes through a single front/rear port pair.
|
||||
Test a connection which passes through a rear port with exactly one front port.
|
||||
|
||||
1 2
|
||||
[Device 1] ----- [Panel 1] ----- [Device 2]
|
||||
[Device 1] ----- [Panel 5] ----- [Device 2]
|
||||
Iface1 FP1 RP1 Iface1
|
||||
"""
|
||||
# Create cables
|
||||
# Create cables (FP first, RP second)
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
self.assertEqual(cable2.termination_a.positions, 1) # Sanity check
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@@ -592,6 +710,97 @@ class CablePathTestCase(TestCase):
|
||||
self.assertIsNone(endpoint_a.connection_status)
|
||||
self.assertIsNone(endpoint_b.connection_status)
|
||||
|
||||
def test_connections_via_nested_single_position_rearport(self):
|
||||
"""
|
||||
Test a connection which passes through a single front/rear port pair between two multi-position rear ports.
|
||||
|
||||
Test two connections via patched rear ports:
|
||||
Device 1 <---> Device 2
|
||||
Device 3 <---> Device 4
|
||||
|
||||
1 2
|
||||
[Device 1] -----------+ +----------- [Device 2]
|
||||
Iface1 | | Iface1
|
||||
FP1 | 3 4 | FP1
|
||||
[Panel 1] ----- [Panel 5] ----- [Panel 2]
|
||||
FP2 | RP1 RP1 FP1 RP1 | FP2
|
||||
Iface1 | | Iface1
|
||||
[Device 3] -----------+ +----------- [Device 4]
|
||||
5 6
|
||||
"""
|
||||
# Create cables (Panel 5 RP first, FP second)
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
|
||||
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
cable3 = Cable(
|
||||
termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1')
|
||||
)
|
||||
cable3.full_clean()
|
||||
cable3.save()
|
||||
cable4 = Cable(
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1'),
|
||||
termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
|
||||
)
|
||||
cable4.full_clean()
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2'),
|
||||
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1')
|
||||
)
|
||||
cable5.full_clean()
|
||||
cable5.save()
|
||||
cable6 = Cable(
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'),
|
||||
termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
)
|
||||
cable6.full_clean()
|
||||
cable6.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
|
||||
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
|
||||
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
|
||||
# Validate connections
|
||||
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
|
||||
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
|
||||
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
|
||||
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
|
||||
self.assertTrue(endpoint_a.connection_status)
|
||||
self.assertTrue(endpoint_b.connection_status)
|
||||
self.assertTrue(endpoint_c.connection_status)
|
||||
self.assertTrue(endpoint_d.connection_status)
|
||||
|
||||
# Delete cable 3
|
||||
cable3.delete()
|
||||
|
||||
# Refresh endpoints
|
||||
endpoint_a.refresh_from_db()
|
||||
endpoint_b.refresh_from_db()
|
||||
endpoint_c.refresh_from_db()
|
||||
endpoint_d.refresh_from_db()
|
||||
|
||||
# Check that connections have been nullified
|
||||
self.assertIsNone(endpoint_a.connected_endpoint)
|
||||
self.assertIsNone(endpoint_b.connected_endpoint)
|
||||
self.assertIsNone(endpoint_c.connected_endpoint)
|
||||
self.assertIsNone(endpoint_d.connected_endpoint)
|
||||
self.assertIsNone(endpoint_a.connection_status)
|
||||
self.assertIsNone(endpoint_b.connection_status)
|
||||
self.assertIsNone(endpoint_c.connection_status)
|
||||
self.assertIsNone(endpoint_d.connection_status)
|
||||
|
||||
def test_connections_via_patch(self):
|
||||
"""
|
||||
Test two connections via patched rear ports:
|
||||
@@ -613,28 +822,33 @@ class CablePathTestCase(TestCase):
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
|
||||
cable3 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
|
||||
)
|
||||
cable3.full_clean()
|
||||
cable3.save()
|
||||
|
||||
cable4 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
|
||||
)
|
||||
cable4.full_clean()
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2')
|
||||
)
|
||||
cable5.full_clean()
|
||||
cable5.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@@ -693,43 +907,51 @@ class CablePathTestCase(TestCase):
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
cable3 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable3.full_clean()
|
||||
cable3.save()
|
||||
|
||||
cable4 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
|
||||
)
|
||||
cable4.full_clean()
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
|
||||
)
|
||||
cable5.full_clean()
|
||||
cable5.save()
|
||||
|
||||
cable6 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
|
||||
)
|
||||
cable6.full_clean()
|
||||
cable6.save()
|
||||
cable7 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 2')
|
||||
)
|
||||
cable7.full_clean()
|
||||
cable7.save()
|
||||
cable8 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
|
||||
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
)
|
||||
cable8.full_clean()
|
||||
cable8.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@@ -789,38 +1011,45 @@ class CablePathTestCase(TestCase):
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
|
||||
cable3 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
|
||||
)
|
||||
cable3.full_clean()
|
||||
cable3.save()
|
||||
cable4 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1')
|
||||
)
|
||||
cable4.full_clean()
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
|
||||
)
|
||||
cable5.full_clean()
|
||||
cable5.save()
|
||||
|
||||
cable6 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
|
||||
)
|
||||
cable6.full_clean()
|
||||
cable6.save()
|
||||
cable7 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
|
||||
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
)
|
||||
cable7.full_clean()
|
||||
cable7.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@@ -870,11 +1099,13 @@ class CablePathTestCase(TestCase):
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=CircuitTermination.objects.get(term_side='A')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=CircuitTermination.objects.get(term_side='Z'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@@ -903,30 +1134,34 @@ class CablePathTestCase(TestCase):
|
||||
def test_connection_via_patched_circuit(self):
|
||||
"""
|
||||
1 2 3 4
|
||||
[Device 1] ----- [Panel 1] ----- [Circuit] ----- [Panel 2] ----- [Device 2]
|
||||
[Device 1] ----- [Panel 5] ----- [Circuit] ----- [Panel 6] ----- [Device 2]
|
||||
Iface1 FP1 RP1 A Z RP1 FP1 Iface1
|
||||
|
||||
"""
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'),
|
||||
termination_b=CircuitTermination.objects.get(term_side='A')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
cable3 = Cable(
|
||||
termination_a=CircuitTermination.objects.get(term_side='Z'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
|
||||
termination_b=RearPort.objects.get(device__name='Panel 6', name='Rear Port 1')
|
||||
)
|
||||
cable3.full_clean()
|
||||
cable3.save()
|
||||
cable4 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 6', name='Front Port 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable4.full_clean()
|
||||
cable4.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
|
||||
@@ -813,14 +813,7 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
|
||||
}
|
||||
|
||||
|
||||
# TODO: Change base class to DeviceComponentTemplateViewTestCase
|
||||
# Blocked by absence of bulk edit view for DeviceBays
|
||||
class DeviceBayTemplateTestCase(
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
ViewTestCases.BulkCreateObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||
):
|
||||
class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = DeviceBayTemplate
|
||||
|
||||
@classmethod
|
||||
@@ -848,6 +841,10 @@ class DeviceBayTemplateTestCase(
|
||||
'name_pattern': 'Device Bay Template [4-6]',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'Foo bar',
|
||||
}
|
||||
|
||||
|
||||
class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = DeviceRole
|
||||
@@ -1194,10 +1191,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTestCase(
|
||||
ViewTestCases.GetObjectViewTestCase,
|
||||
ViewTestCases.DeviceComponentViewTestCase,
|
||||
):
|
||||
class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = Interface
|
||||
|
||||
@classmethod
|
||||
@@ -1563,16 +1557,7 @@ class CableTestCase(
|
||||
}
|
||||
|
||||
|
||||
# TODO: Change base class to PrimaryObjectViewTestCase
|
||||
# Blocked by standard creation, bulk creation views for VirtualChassis (member devices must be selected in bulk)
|
||||
class VirtualChassisTestCase(
|
||||
ViewTestCases.GetObjectViewTestCase,
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
ViewTestCases.ListObjectsViewTestCase,
|
||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||
):
|
||||
class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualChassis
|
||||
|
||||
@classmethod
|
||||
@@ -1587,7 +1572,6 @@ class VirtualChassisTestCase(
|
||||
name='Device Role', slug='device-role-1'
|
||||
)
|
||||
|
||||
# Create 9 member Devices
|
||||
devices = (
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 1', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 2', site=site),
|
||||
@@ -1598,23 +1582,29 @@ class VirtualChassisTestCase(
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 7', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 8', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 9', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 10', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 11', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 12', site=site),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
# Create three VirtualChassis with two members each
|
||||
vc1 = VirtualChassis.objects.create(master=devices[0], domain='domain-1')
|
||||
# Create three VirtualChassis with three members each
|
||||
vc1 = VirtualChassis.objects.create(name='VC1', master=devices[0], domain='domain-1')
|
||||
Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=vc1, vc_position=1)
|
||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=vc1, vc_position=2)
|
||||
Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=vc1, vc_position=3)
|
||||
vc2 = VirtualChassis.objects.create(master=devices[3], domain='domain-2')
|
||||
vc2 = VirtualChassis.objects.create(name='VC2', master=devices[3], domain='domain-2')
|
||||
Device.objects.filter(pk=devices[3].pk).update(virtual_chassis=vc2, vc_position=1)
|
||||
Device.objects.filter(pk=devices[4].pk).update(virtual_chassis=vc2, vc_position=2)
|
||||
Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=vc2, vc_position=3)
|
||||
vc3 = VirtualChassis.objects.create(master=devices[6], domain='domain-3')
|
||||
vc3 = VirtualChassis.objects.create(name='VC3', master=devices[6], domain='domain-3')
|
||||
Device.objects.filter(pk=devices[6].pk).update(virtual_chassis=vc3, vc_position=1)
|
||||
Device.objects.filter(pk=devices[7].pk).update(virtual_chassis=vc3, vc_position=2)
|
||||
Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=vc3, vc_position=3)
|
||||
|
||||
cls.form_data = {
|
||||
'master': devices[1].pk,
|
||||
'domain': 'domain-x',
|
||||
'name': 'VC4',
|
||||
'domain': 'domain-4',
|
||||
# Management form data for VC members
|
||||
'form-TOTAL_FORMS': 0,
|
||||
'form-INITIAL_FORMS': 3,
|
||||
@@ -1622,6 +1612,13 @@ class VirtualChassisTestCase(
|
||||
'form-MAX_NUM_FORMS': 1000,
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,domain,master",
|
||||
"VC4,Domain 4,Device 10",
|
||||
"VC5,Domain 5,Device 11",
|
||||
"VC6,Domain 6,Device 12",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'domain': 'domain-x',
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ from extras.views import ObjectChangeLogView, ImageAttachmentEditView
|
||||
from ipam.views import ServiceEditView
|
||||
from . import views
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform,
|
||||
PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site,
|
||||
VirtualChassis,
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, FrontPort, Interface,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup,
|
||||
RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
|
||||
)
|
||||
|
||||
app_name = 'dcim'
|
||||
@@ -18,6 +18,7 @@ urlpatterns = [
|
||||
path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
|
||||
path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||
path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
|
||||
path('regions/<int:pk>/delete/', views.RegionDeleteView.as_view(), name='region_delete'),
|
||||
path('regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
|
||||
|
||||
# Sites
|
||||
@@ -38,6 +39,7 @@ urlpatterns = [
|
||||
path('rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
|
||||
path('rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
||||
path('rack-groups/<int:pk>/edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||
path('rack-groups/<int:pk>/delete/', views.RackGroupDeleteView.as_view(), name='rackgroup_delete'),
|
||||
path('rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
|
||||
|
||||
# Rack roles
|
||||
@@ -46,6 +48,7 @@ urlpatterns = [
|
||||
path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
|
||||
path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||
path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
|
||||
path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
|
||||
|
||||
# Rack reservations
|
||||
@@ -78,6 +81,7 @@ urlpatterns = [
|
||||
path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
|
||||
path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
||||
path('manufacturers/<slug:slug>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||
path('manufacturers/<slug:slug>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
|
||||
path('manufacturers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
|
||||
|
||||
# Device types
|
||||
@@ -142,7 +146,7 @@ urlpatterns = [
|
||||
|
||||
# Device bay templates
|
||||
path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'),
|
||||
# path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
|
||||
path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
|
||||
path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'),
|
||||
path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
|
||||
path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),
|
||||
@@ -153,6 +157,7 @@ urlpatterns = [
|
||||
path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
|
||||
path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
||||
path('device-roles/<slug:slug>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||
path('device-roles/<slug:slug>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
|
||||
path('device-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
|
||||
|
||||
# Platforms
|
||||
@@ -161,6 +166,7 @@ urlpatterns = [
|
||||
path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
|
||||
path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
||||
path('platforms/<slug:slug>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||
path('platforms/<slug:slug>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
|
||||
path('platforms/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
|
||||
|
||||
# Devices
|
||||
@@ -187,12 +193,15 @@ urlpatterns = [
|
||||
path('console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||
path('console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
|
||||
path('console-ports/edit/', views.ConsolePortBulkEditView.as_view(), name='consoleport_bulk_edit'),
|
||||
# TODO: Bulk rename, disconnect views for ConsolePorts
|
||||
path('console-ports/rename/', views.ConsolePortBulkRenameView.as_view(), name='consoleport_bulk_rename'),
|
||||
path('console-ports/disconnect/', views.ConsolePortBulkDisconnectView.as_view(), name='consoleport_bulk_disconnect'),
|
||||
path('console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||
path('console-ports/<int:pk>/', views.ConsolePortView.as_view(), name='consoleport'),
|
||||
path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||
path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||
path('console-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}),
|
||||
path('console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
|
||||
path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||
path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||
|
||||
# Console server ports
|
||||
@@ -203,10 +212,12 @@ urlpatterns = [
|
||||
path('console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
|
||||
path('console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
||||
path('console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
path('console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
|
||||
path('console-server-ports/<int:pk>/', views.ConsoleServerPortView.as_view(), name='consoleserverport'),
|
||||
path('console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||
path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
path('console-server-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}),
|
||||
path('console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
|
||||
path('console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
|
||||
path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||
|
||||
# Power ports
|
||||
@@ -214,12 +225,15 @@ urlpatterns = [
|
||||
path('power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||
path('power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
|
||||
path('power-ports/edit/', views.PowerPortBulkEditView.as_view(), name='powerport_bulk_edit'),
|
||||
# TODO: Bulk rename, disconnect views for PowerPorts
|
||||
path('power-ports/rename/', views.PowerPortBulkRenameView.as_view(), name='powerport_bulk_rename'),
|
||||
path('power-ports/disconnect/', views.PowerPortBulkDisconnectView.as_view(), name='powerport_bulk_disconnect'),
|
||||
path('power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||
path('power-ports/<int:pk>/', views.PowerPortView.as_view(), name='powerport'),
|
||||
path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||
path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||
path('power-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}),
|
||||
path('power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
|
||||
path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||
path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||
|
||||
# Power outlets
|
||||
@@ -230,10 +244,12 @@ urlpatterns = [
|
||||
path('power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
|
||||
path('power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
||||
path('power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
path('power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
|
||||
path('power-outlets/<int:pk>/', views.PowerOutletView.as_view(), name='poweroutlet'),
|
||||
path('power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
path('power-outlets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}),
|
||||
path('power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
|
||||
path('power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
|
||||
path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
|
||||
# Interfaces
|
||||
@@ -244,12 +260,12 @@ urlpatterns = [
|
||||
path('interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||
path('interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
||||
path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
path('interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
path('interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
|
||||
path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||
path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
|
||||
path('interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
|
||||
path('interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
|
||||
# Front ports
|
||||
@@ -260,10 +276,12 @@ urlpatterns = [
|
||||
path('front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
|
||||
path('front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
|
||||
path('front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
|
||||
path('front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
|
||||
path('front-ports/<int:pk>/', views.FrontPortView.as_view(), name='frontport'),
|
||||
path('front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
|
||||
path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
|
||||
path('front-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}),
|
||||
path('front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
|
||||
path('front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
|
||||
# path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
|
||||
|
||||
# Rear ports
|
||||
@@ -274,10 +292,12 @@ urlpatterns = [
|
||||
path('rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
|
||||
path('rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
|
||||
path('rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
|
||||
path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
|
||||
path('rear-ports/<int:pk>/', views.RearPortView.as_view(), name='rearport'),
|
||||
path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
|
||||
path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
||||
path('rear-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}),
|
||||
path('rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
|
||||
path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
|
||||
path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||
|
||||
# Device bays
|
||||
@@ -287,8 +307,10 @@ urlpatterns = [
|
||||
path('device-bays/edit/', views.DeviceBayBulkEditView.as_view(), name='devicebay_bulk_edit'),
|
||||
path('device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
|
||||
path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
path('device-bays/<int:pk>/', views.DeviceBayView.as_view(), name='devicebay'),
|
||||
path('device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
path('device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||
path('device-bays/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicebay_changelog', kwargs={'model': DeviceBay}),
|
||||
path('device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
|
||||
path('device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
|
||||
path('devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||
@@ -298,10 +320,13 @@ urlpatterns = [
|
||||
path('inventory-items/add/', views.InventoryItemCreateView.as_view(), name='inventoryitem_add'),
|
||||
path('inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
|
||||
path('inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
|
||||
# TODO: Bulk rename view for InventoryItems
|
||||
path('inventory-items/rename/', views.InventoryItemBulkRenameView.as_view(), name='inventoryitem_bulk_rename'),
|
||||
path('inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
|
||||
path('inventory-items/<int:pk>/', views.InventoryItemView.as_view(), name='inventoryitem'),
|
||||
path('inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
|
||||
path('inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
|
||||
path('inventory-items/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='inventoryitem_changelog', kwargs={'model': InventoryItem}),
|
||||
path('devices/inventory-items/add/', views.DeviceBulkAddInventoryItemView.as_view(), name='device_bulk_add_inventoryitem'),
|
||||
|
||||
# Cables
|
||||
path('cables/', views.CableListView.as_view(), name='cable_list'),
|
||||
@@ -321,6 +346,7 @@ urlpatterns = [
|
||||
# Virtual chassis
|
||||
path('virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
|
||||
path('virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
|
||||
path('virtual-chassis/import/', views.VirtualChassisBulkImportView.as_view(), name='virtualchassis_import'),
|
||||
path('virtual-chassis/edit/', views.VirtualChassisBulkEditView.as_view(), name='virtualchassis_bulk_edit'),
|
||||
path('virtual-chassis/delete/', views.VirtualChassisBulkDeleteView.as_view(), name='virtualchassis_bulk_delete'),
|
||||
path('virtual-chassis/<int:pk>/', views.VirtualChassisView.as_view(), name='virtualchassis'),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -110,7 +110,7 @@ class ExportTemplateViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class TagViewSet(ModelViewSet):
|
||||
queryset = Tag.restricted.annotate(
|
||||
queryset = Tag.objects.annotate(
|
||||
tagged_items=Count('extras_taggeditem_items', distinct=True)
|
||||
)
|
||||
serializer_class = serializers.TagSerializer
|
||||
|
||||
@@ -6,6 +6,7 @@ from django import get_version
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
|
||||
@@ -52,6 +53,7 @@ class Command(BaseCommand):
|
||||
pass
|
||||
|
||||
# Additional objects to include
|
||||
namespace['ContentType'] = ContentType
|
||||
namespace['User'] = User
|
||||
|
||||
# Load convenience commands
|
||||
|
||||
@@ -540,7 +540,7 @@ class ConfigContextModel(models.Model):
|
||||
|
||||
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
|
||||
data = OrderedDict()
|
||||
for context in ConfigContext.objects.get_for_object(self):
|
||||
for context in ConfigContext.objects.unrestricted().get_for_object(self):
|
||||
data = deepmerge(data, context.data)
|
||||
|
||||
# If the object has local config context data defined, merge it last
|
||||
|
||||
@@ -22,14 +22,10 @@ class Tag(TagBase, ChangeLoggedModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
restricted = RestrictedQuerySet.as_manager()
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'color', 'description']
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:tag', args=[self.slug])
|
||||
|
||||
def slugify(self, tag, i=None):
|
||||
# Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names)
|
||||
slug = slugify(tag, allow_unicode=True)
|
||||
|
||||
@@ -173,7 +173,7 @@ class ChoiceVar(ScriptVariable):
|
||||
|
||||
class ObjectVar(ScriptVariable):
|
||||
"""
|
||||
NetBox object representation. The provided QuerySet will determine the choices available.
|
||||
A single object within NetBox.
|
||||
"""
|
||||
form_field = DynamicModelChoiceField
|
||||
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, ToggleColumn
|
||||
from .models import ConfigContext, ObjectChange, JobResult, Tag, TaggedItem
|
||||
|
||||
TAG_ACTIONS = """
|
||||
<a href="{% url 'extras:tag_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.taggit.change_tag %}
|
||||
<a href="{% url 'extras:tag_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% if perms.taggit.delete_tag %}
|
||||
<a href="{% url 'extras:tag_delete' slug=record.slug %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-trash" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
TAGGED_ITEM = """
|
||||
{% if value.get_absolute_url %}
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
@@ -64,16 +52,8 @@ OBJECTCHANGE_REQUEST_ID = """
|
||||
|
||||
class TagTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(
|
||||
viewname='extras:tag',
|
||||
args=[Accessor('slug')]
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=TAG_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
color = ColorColumn()
|
||||
actions = ButtonsColumn(Tag, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Tag
|
||||
|
||||
@@ -10,7 +10,7 @@ from extras.models import ConfigContext, ObjectChange, Tag
|
||||
from utilities.testing import ViewTestCases, TestCase
|
||||
|
||||
|
||||
class TagTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = Tag
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -13,7 +13,6 @@ urlpatterns = [
|
||||
path('tags/import/', views.TagBulkImportView.as_view(), name='tag_import'),
|
||||
path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
|
||||
path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
path('tags/<str:slug>/', views.TagView.as_view(), name='tag'),
|
||||
path('tags/<str:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
|
||||
path('tags/<str:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
path('tags/<str:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
|
||||
|
||||
@@ -4,13 +4,15 @@ from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Count, Q
|
||||
from django.db.models import Count, Prefetch, Q
|
||||
from django.http import Http404, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.utils import copy_safe_request, shallow_compare_dict
|
||||
@@ -18,6 +20,7 @@ from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
ContentTypePermissionRequiredMixin,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from . import filters, forms, tables
|
||||
from .choices import JobResultStatusChoices
|
||||
from .models import ConfigContext, ImageAttachment, ObjectChange, Report, JobResult, Script, Tag, TaggedItem
|
||||
@@ -30,7 +33,7 @@ from .scripts import get_scripts, run_script
|
||||
#
|
||||
|
||||
class TagListView(ObjectListView):
|
||||
queryset = Tag.restricted.annotate(
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('extras_taggeditem_items', distinct=True)
|
||||
).order_by(
|
||||
'name'
|
||||
@@ -40,71 +43,39 @@ class TagListView(ObjectListView):
|
||||
table = tables.TagTable
|
||||
|
||||
|
||||
class TagView(ObjectView):
|
||||
queryset = Tag.restricted.all()
|
||||
|
||||
def get(self, request, slug):
|
||||
|
||||
tag = get_object_or_404(self.queryset, slug=slug)
|
||||
tagged_items = TaggedItem.objects.filter(
|
||||
tag=tag
|
||||
).prefetch_related(
|
||||
'content_type', 'content_object'
|
||||
)
|
||||
|
||||
# Generate a table of all items tagged with this Tag
|
||||
items_table = tables.TaggedItemTable(tagged_items)
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(items_table)
|
||||
|
||||
return render(request, 'extras/tag.html', {
|
||||
'tag': tag,
|
||||
'items_count': tagged_items.count(),
|
||||
'items_table': items_table,
|
||||
})
|
||||
|
||||
|
||||
class TagEditView(ObjectEditView):
|
||||
queryset = Tag.restricted.all()
|
||||
queryset = Tag.objects.all()
|
||||
model_form = forms.TagForm
|
||||
default_return_url = 'extras:tag_list'
|
||||
template_name = 'extras/tag_edit.html'
|
||||
|
||||
|
||||
class TagDeleteView(ObjectDeleteView):
|
||||
queryset = Tag.restricted.all()
|
||||
default_return_url = 'extras:tag_list'
|
||||
queryset = Tag.objects.all()
|
||||
|
||||
|
||||
class TagBulkImportView(BulkImportView):
|
||||
queryset = Tag.restricted.all()
|
||||
queryset = Tag.objects.all()
|
||||
model_form = forms.TagCSVForm
|
||||
table = tables.TagTable
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
|
||||
class TagBulkEditView(BulkEditView):
|
||||
queryset = Tag.restricted.annotate(
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('extras_taggeditem_items', distinct=True)
|
||||
).order_by(
|
||||
'name'
|
||||
)
|
||||
table = tables.TagTable
|
||||
form = forms.TagBulkEditForm
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
|
||||
class TagBulkDeleteView(BulkDeleteView):
|
||||
queryset = Tag.restricted.annotate(
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('extras_taggeditem_items')
|
||||
).order_by(
|
||||
'name'
|
||||
)
|
||||
table = tables.TagTable
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -123,6 +94,18 @@ class ConfigContextView(ObjectView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
|
||||
def get(self, request, pk):
|
||||
# Extend queryset to prefetch related objects
|
||||
self.queryset = self.queryset.prefetch_related(
|
||||
Prefetch('regions', queryset=Region.objects.restrict(request.user)),
|
||||
Prefetch('sites', queryset=Site.objects.restrict(request.user)),
|
||||
Prefetch('roles', queryset=DeviceRole.objects.restrict(request.user)),
|
||||
Prefetch('platforms', queryset=Platform.objects.restrict(request.user)),
|
||||
Prefetch('clusters', queryset=Cluster.objects.restrict(request.user)),
|
||||
Prefetch('cluster_groups', queryset=ClusterGroup.objects.restrict(request.user)),
|
||||
Prefetch('tenants', queryset=Tenant.objects.restrict(request.user)),
|
||||
Prefetch('tenant_groups', queryset=TenantGroup.objects.restrict(request.user)),
|
||||
)
|
||||
|
||||
configcontext = get_object_or_404(self.queryset, pk=pk)
|
||||
|
||||
# Determine user's preferred output format
|
||||
@@ -144,7 +127,6 @@ class ConfigContextView(ObjectView):
|
||||
class ConfigContextEditView(ObjectEditView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
model_form = forms.ConfigContextForm
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
template_name = 'extras/configcontext_edit.html'
|
||||
|
||||
|
||||
@@ -153,18 +135,15 @@ class ConfigContextBulkEditView(BulkEditView):
|
||||
filterset = filters.ConfigContextFilterSet
|
||||
table = tables.ConfigContextTable
|
||||
form = forms.ConfigContextBulkEditForm
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
|
||||
class ConfigContextDeleteView(ObjectDeleteView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
|
||||
class ConfigContextBulkDeleteView(BulkDeleteView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
table = tables.ConfigContextTable
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
|
||||
class ObjectConfigContextView(ObjectView):
|
||||
@@ -264,9 +243,11 @@ class ObjectChangeLogView(View):
|
||||
|
||||
def get(self, request, model, **kwargs):
|
||||
|
||||
# Get object my model and kwargs (e.g. slug='foo')
|
||||
queryset = model.objects.restrict(request.user, 'view')
|
||||
obj = get_object_or_404(queryset, **kwargs)
|
||||
# Handle QuerySet restriction of parent object if needed
|
||||
if hasattr(model.objects, 'restrict'):
|
||||
obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs)
|
||||
else:
|
||||
obj = get_object_or_404(model, **kwargs)
|
||||
|
||||
# Gather all changes for this object (and its related objects)
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
@@ -299,6 +280,7 @@ class ObjectChangeLogView(View):
|
||||
|
||||
return render(request, 'extras/object_changelog.html', {
|
||||
object_var: obj,
|
||||
'instance': obj, # We'll eventually standardize on 'instance` for the object variable name
|
||||
'table': objectchanges_table,
|
||||
'base_template': base_template,
|
||||
'active_tab': 'changelog',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
@@ -9,10 +11,12 @@ from dcim.models import Interface
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from ipam.choices import *
|
||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from utilities.api import (
|
||||
ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
get_serializer_for_model,
|
||||
)
|
||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
||||
from .nested_serializers import *
|
||||
@@ -228,18 +232,31 @@ class IPAddressSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
|
||||
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
|
||||
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
||||
assigned_object_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS),
|
||||
required=False
|
||||
)
|
||||
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
nat_outside = NestedIPAddressSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'nat_inside',
|
||||
'nat_outside', 'dns_name', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', 'assigned_object_id',
|
||||
'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
read_only_fields = ['family']
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_assigned_object(self, obj):
|
||||
if obj.assigned_object is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.assigned_object, prefix='Nested')
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.assigned_object, context=context).data
|
||||
|
||||
|
||||
class AvailableIPSerializer(serializers.Serializer):
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.conf import settings
|
||||
from django.db.models import Count
|
||||
from django.db.models import Count, Prefetch
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_pglocks import advisory_lock
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
@@ -233,8 +233,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
|
||||
class IPAddressViewSet(CustomFieldModelViewSet):
|
||||
queryset = IPAddress.objects.prefetch_related(
|
||||
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine',
|
||||
'nat_outside', 'tags',
|
||||
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags',
|
||||
)
|
||||
serializer_class = serializers.IPAddressSerializer
|
||||
filterset_class = filters.IPAddressFilterSet
|
||||
@@ -271,6 +270,9 @@ class VLANViewSet(CustomFieldModelViewSet):
|
||||
#
|
||||
|
||||
class ServiceViewSet(ModelViewSet):
|
||||
queryset = Service.objects.prefetch_related('device').prefetch_related('tags')
|
||||
queryset = Service.objects.prefetch_related(
|
||||
Prefetch('ipaddresses', queryset=IPAddress.objects.unrestricted()),
|
||||
'device', 'virtual_machine', 'tags'
|
||||
)
|
||||
serializer_class = serializers.ServiceSerializer
|
||||
filterset_class = filters.ServiceFilterSet
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from django.db.models import Q
|
||||
|
||||
from .choices import IPAddressRoleChoices
|
||||
|
||||
# BGP ASN bounds
|
||||
@@ -29,6 +31,11 @@ PREFIX_LENGTH_MAX = 127 # IPv6
|
||||
# IPAddresses
|
||||
#
|
||||
|
||||
IPADDRESS_ASSIGNMENT_MODELS = Q(
|
||||
Q(app_label='dcim', model='interface') |
|
||||
Q(app_label='virtualization', model='vminterface')
|
||||
)
|
||||
|
||||
IPADDRESS_MASK_LENGTH_MIN = 1
|
||||
IPADDRESS_MASK_LENGTH_MAX = 128 # IPv6
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from utilities.filters import (
|
||||
BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, TagFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
@@ -309,27 +309,38 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet,
|
||||
field_name='pk',
|
||||
label='Device (ID)',
|
||||
)
|
||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__virtual_machine',
|
||||
queryset=VirtualMachine.objects.unrestricted(),
|
||||
label='Virtual machine (ID)',
|
||||
)
|
||||
virtual_machine = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__virtual_machine__name',
|
||||
queryset=VirtualMachine.objects.unrestricted(),
|
||||
to_field_name='name',
|
||||
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.unrestricted(),
|
||||
to_field_name='name',
|
||||
label='Interface (ID)',
|
||||
label='Interface (name)',
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface',
|
||||
queryset=Interface.objects.unrestricted(),
|
||||
label='Interface (ID)',
|
||||
)
|
||||
vminterface = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vminterface__name',
|
||||
queryset=VMInterface.objects.unrestricted(),
|
||||
to_field_name='name',
|
||||
label='VM interface (name)',
|
||||
)
|
||||
vminterface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vminterface',
|
||||
queryset=VMInterface.objects.unrestricted(),
|
||||
label='VM interface (ID)',
|
||||
)
|
||||
assigned_to_interface = django_filters.BooleanFilter(
|
||||
method='_assigned_to_interface',
|
||||
label='Is assigned to an interface',
|
||||
@@ -379,17 +390,29 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet,
|
||||
return queryset.filter(address__net_mask_length=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
try:
|
||||
devices = Device.objects.prefetch_related('device_type').filter(**{'{}__in'.format(name): value})
|
||||
vc_interface_ids = []
|
||||
for device in devices:
|
||||
vc_interface_ids.extend([i['id'] for i in device.vc_interfaces.values('id')])
|
||||
return queryset.filter(interface_id__in=vc_interface_ids)
|
||||
except Device.DoesNotExist:
|
||||
devices = Device.objects.filter(**{'{}__in'.format(name): 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(**{'{}__in'.format(name): 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
|
||||
)
|
||||
|
||||
def _assigned_to_interface(self, queryset, name, value):
|
||||
return queryset.exclude(interface__isnull=value)
|
||||
return queryset.exclude(assigned_object_id__isnull=value)
|
||||
|
||||
|
||||
class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
||||
@@ -14,7 +14,7 @@ from utilities.forms import (
|
||||
ExpandableIPAddressField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
@@ -522,10 +522,33 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
||||
#
|
||||
|
||||
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
|
||||
interface = forms.ModelChoiceField(
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
filter_for={
|
||||
'interface': 'device_id'
|
||||
}
|
||||
)
|
||||
)
|
||||
interface = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False
|
||||
)
|
||||
virtual_machine = DynamicModelChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
filter_for={
|
||||
'vminterface': 'virtual_machine_id'
|
||||
}
|
||||
)
|
||||
)
|
||||
vminterface = DynamicModelChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
label='Interface'
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@@ -597,8 +620,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'interface', 'primary_for_parent',
|
||||
'nat_site', 'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags',
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack',
|
||||
'nat_inside', 'tenant_group', 'tenant', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'status': StaticSelect2(),
|
||||
@@ -610,32 +633,26 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
if instance and instance.nat_inside and instance.nat_inside.device is not None:
|
||||
initial['nat_site'] = instance.nat_inside.device.site
|
||||
initial['nat_rack'] = instance.nat_inside.device.rack
|
||||
initial['nat_device'] = instance.nat_inside.device
|
||||
if instance:
|
||||
if type(instance.assigned_object) is Interface:
|
||||
initial['device'] = instance.assigned_object.device
|
||||
initial['interface'] = instance.assigned_object
|
||||
elif type(instance.assigned_object) is VMInterface:
|
||||
initial['virtual_machine'] = instance.assigned_object.virtual_machine
|
||||
initial['vminterface'] = instance.assigned_object
|
||||
if instance.nat_inside and instance.nat_inside.device is not None:
|
||||
initial['nat_site'] = instance.nat_inside.device.site
|
||||
initial['nat_rack'] = instance.nat_inside.device.rack
|
||||
initial['nat_device'] = instance.nat_inside.device
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
# Limit interface selections to those belonging to the parent device/VM
|
||||
if self.instance and self.instance.interface:
|
||||
self.fields['interface'].queryset = Interface.objects.filter(
|
||||
device=self.instance.interface.device, virtual_machine=self.instance.interface.virtual_machine
|
||||
).prefetch_related(
|
||||
'device__primary_ip4',
|
||||
'device__primary_ip6',
|
||||
'virtual_machine__primary_ip4',
|
||||
'virtual_machine__primary_ip6',
|
||||
) # We prefetch the primary address fields to ensure cache invalidation does not balk on the save()
|
||||
else:
|
||||
self.fields['interface'].choices = []
|
||||
|
||||
# Initialize primary_for_parent if IP address is already assigned
|
||||
if self.instance.pk and self.instance.interface is not None:
|
||||
parent = self.instance.interface.parent
|
||||
if self.instance.pk and self.instance.assigned_object:
|
||||
parent = self.instance.assigned_object.parent
|
||||
if (
|
||||
self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
|
||||
self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
|
||||
@@ -645,32 +662,39 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Cannot select both a device interface and a VM interface
|
||||
if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'):
|
||||
raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface")
|
||||
|
||||
# Primary IP assignment is only available if an interface has been assigned.
|
||||
if self.cleaned_data.get('primary_for_parent') and not self.cleaned_data.get('interface'):
|
||||
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||
if self.cleaned_data.get('primary_for_parent') and not interface:
|
||||
self.add_error(
|
||||
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Set assigned object
|
||||
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||
if interface:
|
||||
self.instance.assigned_object = interface
|
||||
|
||||
ipaddress = super().save(*args, **kwargs)
|
||||
|
||||
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
|
||||
if self.cleaned_data['primary_for_parent']:
|
||||
parent = self.cleaned_data['interface'].parent
|
||||
if interface and self.cleaned_data['primary_for_parent']:
|
||||
if ipaddress.address.version == 4:
|
||||
parent.primary_ip4 = ipaddress
|
||||
interface.parent.primary_ip4 = ipaddress
|
||||
else:
|
||||
parent.primary_ip6 = ipaddress
|
||||
parent.save()
|
||||
elif self.cleaned_data['interface']:
|
||||
parent = self.cleaned_data['interface'].parent
|
||||
if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
|
||||
parent.primary_ip4 = None
|
||||
parent.save()
|
||||
elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
|
||||
parent.primary_ip6 = None
|
||||
parent.save()
|
||||
interface.primary_ip6 = ipaddress
|
||||
interface.parent.save()
|
||||
elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress:
|
||||
interface.parent.primary_ip4 = None
|
||||
interface.parent.save()
|
||||
elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress:
|
||||
interface.parent.primary_ip4 = None
|
||||
interface.parent.save()
|
||||
|
||||
return ipaddress
|
||||
|
||||
@@ -742,7 +766,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
|
||||
help_text='Parent VM of assigned interface (if any)'
|
||||
)
|
||||
interface = CSVModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
queryset=Interface.objects.none(), # Can also refer to VMInterface
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned interface'
|
||||
@@ -761,21 +785,17 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
if data:
|
||||
|
||||
# Limit interface queryset by assigned device or virtual machine
|
||||
# Limit interface queryset by assigned device
|
||||
if data.get('device'):
|
||||
params = {
|
||||
f"device__{self.fields['device'].to_field_name}": 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'):
|
||||
params = {
|
||||
f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data.get('virtual_machine')
|
||||
}
|
||||
else:
|
||||
params = {
|
||||
'device': None,
|
||||
'virtual_machine': None,
|
||||
}
|
||||
self.fields['interface'].queryset = self.fields['interface'].queryset.filter(**params)
|
||||
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()
|
||||
@@ -790,6 +810,10 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Set interface assignment
|
||||
if self.cleaned_data['interface']:
|
||||
self.instance.assigned_object = self.cleaned_data['interface']
|
||||
|
||||
ipaddress = super().save(*args, **kwargs)
|
||||
|
||||
# Set as primary for device/VM
|
||||
@@ -1194,13 +1218,12 @@ class ServiceForm(BootstrapMixin, CustomFieldModelForm):
|
||||
|
||||
# Limit IP address choices to those assigned to interfaces of the parent device/VM
|
||||
if self.instance.device:
|
||||
vc_interface_ids = [i['id'] for i in self.instance.device.vc_interfaces.values('id')]
|
||||
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
||||
interface_id__in=vc_interface_ids
|
||||
interface__in=self.instance.device.vc_interfaces.values_list('id', flat=True)
|
||||
)
|
||||
elif self.instance.virtual_machine:
|
||||
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
||||
interface__virtual_machine=self.instance.virtual_machine
|
||||
vminterface__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True)
|
||||
)
|
||||
else:
|
||||
self.fields['ipaddresses'].choices = []
|
||||
|
||||
40
netbox/ipam/migrations/0037_ipaddress_assignment.py
Normal file
40
netbox/ipam/migrations/0037_ipaddress_assignment.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def set_assigned_object_type(apps, schema_editor):
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
IPAddress = apps.get_model('ipam', 'IPAddress')
|
||||
|
||||
device_ct = ContentType.objects.get(app_label='dcim', model='interface').pk
|
||||
IPAddress.objects.update(assigned_object_type=device_ct)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('ipam', '0036_standardize_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='ipaddress',
|
||||
old_name='interface',
|
||||
new_name='assigned_object_id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='assigned_object_id',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ipaddress',
|
||||
name='assigned_object_type',
|
||||
field=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'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=set_assigned_object_type
|
||||
),
|
||||
]
|
||||
@@ -1,10 +1,11 @@
|
||||
import netaddr
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import F
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
@@ -14,7 +15,7 @@ from extras.utils import extras_features
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import serialize_object
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .fields import IPNetworkField, IPAddressField
|
||||
@@ -215,7 +216,9 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
||||
})
|
||||
|
||||
# Ensure that the aggregate being added is not covered by an existing aggregate
|
||||
covering_aggregates = Aggregate.objects.filter(prefix__net_contains_or_equals=str(self.prefix))
|
||||
covering_aggregates = Aggregate.objects.unrestricted().filter(
|
||||
prefix__net_contains_or_equals=str(self.prefix)
|
||||
)
|
||||
if self.pk:
|
||||
covering_aggregates = covering_aggregates.exclude(pk=self.pk)
|
||||
if covering_aggregates:
|
||||
@@ -226,7 +229,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
||||
})
|
||||
|
||||
# Ensure that the aggregate being added does not cover an existing aggregate
|
||||
covered_aggregates = Aggregate.objects.filter(prefix__net_contained=str(self.prefix))
|
||||
covered_aggregates = Aggregate.objects.unrestricted().filter(prefix__net_contained=str(self.prefix))
|
||||
if self.pk:
|
||||
covered_aggregates = covered_aggregates.exclude(pk=self.pk)
|
||||
if covered_aggregates:
|
||||
@@ -254,7 +257,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
Determine the prefix utilization of the aggregate and return it as a percentage.
|
||||
"""
|
||||
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
|
||||
queryset = Prefix.objects.unrestricted().filter(prefix__net_contained_or_equal=str(self.prefix))
|
||||
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
|
||||
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
||||
|
||||
@@ -552,7 +555,10 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
||||
"container", calculate utilization based on child prefixes. For all others, count child IP addresses.
|
||||
"""
|
||||
if self.status == PrefixStatusChoices.STATUS_CONTAINER:
|
||||
queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
|
||||
queryset = Prefix.objects.unrestricted().filter(
|
||||
prefix__net_contained=str(self.prefix),
|
||||
vrf=self.vrf
|
||||
)
|
||||
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
|
||||
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
||||
else:
|
||||
@@ -606,13 +612,22 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True,
|
||||
help_text='The functional role of this IP'
|
||||
)
|
||||
interface = models.ForeignKey(
|
||||
to='dcim.Interface',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='ip_addresses',
|
||||
assigned_object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
limit_choices_to=IPADDRESS_ASSIGNMENT_MODELS,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
assigned_object_id = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
assigned_object = GenericForeignKey(
|
||||
ct_field='assigned_object_type',
|
||||
fk_field='assigned_object_id'
|
||||
)
|
||||
nat_inside = models.OneToOneField(
|
||||
to='self',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -643,11 +658,11 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
objects = IPAddressManager()
|
||||
|
||||
csv_headers = [
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', 'assigned_object_id', 'is_primary',
|
||||
'dns_name', 'description',
|
||||
]
|
||||
clone_fields = [
|
||||
'vrf', 'tenant', 'status', 'role', 'description', 'interface',
|
||||
'vrf', 'tenant', 'status', 'role', 'description',
|
||||
]
|
||||
|
||||
STATUS_CLASS_MAP = {
|
||||
@@ -707,32 +722,31 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
)
|
||||
})
|
||||
|
||||
if self.pk:
|
||||
|
||||
# Check for primary IP assignment that doesn't match the assigned device/VM
|
||||
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||
# Check for primary IP assignment that doesn't match the assigned device/VM
|
||||
if self.pk and type(self.assigned_object) is Interface:
|
||||
device = Device.objects.unrestricted().filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||
if device:
|
||||
if self.interface is None:
|
||||
if self.assigned_object is None:
|
||||
raise ValidationError({
|
||||
'interface': "IP address is primary for device {} but not assigned".format(device)
|
||||
'interface': f"IP address is primary for device {device} but not assigned to an interface"
|
||||
})
|
||||
elif (device.primary_ip4 == self or device.primary_ip6 == self) and self.interface.device != device:
|
||||
elif self.assigned_object.device != device:
|
||||
raise ValidationError({
|
||||
'interface': "IP address is primary for device {} but assigned to {} ({})".format(
|
||||
device, self.interface.device, self.interface
|
||||
)
|
||||
'interface': f"IP address is primary for device {device} but assigned to "
|
||||
f"{self.assigned_object.device} ({self.assigned_object})"
|
||||
})
|
||||
vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||
elif self.pk and type(self.assigned_object) is VMInterface:
|
||||
vm = VirtualMachine.unrestricted().objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||
if vm:
|
||||
if self.interface is None:
|
||||
if self.assigned_object is None:
|
||||
raise ValidationError({
|
||||
'interface': "IP address is primary for virtual machine {} but not assigned".format(vm)
|
||||
'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to an "
|
||||
f"interface"
|
||||
})
|
||||
elif (vm.primary_ip4 == self or vm.primary_ip6 == self) and self.interface.virtual_machine != vm:
|
||||
elif self.interface.virtual_machine != vm:
|
||||
raise ValidationError({
|
||||
'interface': "IP address is primary for virtual machine {} but assigned to {} ({})".format(
|
||||
vm, self.interface.virtual_machine, self.interface
|
||||
)
|
||||
'vminterface': f"IP address is primary for virtual machine {vm} but assigned to "
|
||||
f"{self.assigned_object.virtual_machine} ({self.assigned_object})"
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -743,29 +757,27 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the assigned Interface (if any)
|
||||
try:
|
||||
parent_obj = self.interface
|
||||
except ObjectDoesNotExist:
|
||||
parent_obj = None
|
||||
|
||||
# Annotate the assigned object, if any
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=parent_obj,
|
||||
related_object=self.assigned_object,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
|
||||
def to_csv(self):
|
||||
|
||||
# Determine if this IP is primary for a Device
|
||||
is_primary = False
|
||||
if self.address.version == 4 and getattr(self, 'primary_ip4_for', False):
|
||||
is_primary = True
|
||||
elif self.address.version == 6 and getattr(self, 'primary_ip6_for', False):
|
||||
is_primary = True
|
||||
else:
|
||||
is_primary = False
|
||||
|
||||
obj_type = None
|
||||
if self.assigned_object_type:
|
||||
obj_type = f'{self.assigned_object_type.app_label}.{self.assigned_object_type.model}'
|
||||
|
||||
return (
|
||||
self.address,
|
||||
@@ -773,9 +785,8 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.get_status_display(),
|
||||
self.get_role_display(),
|
||||
self.device.identifier if self.device else None,
|
||||
self.virtual_machine.name if self.virtual_machine else None,
|
||||
self.interface.name if self.interface else None,
|
||||
obj_type,
|
||||
self.assigned_object_id,
|
||||
is_primary,
|
||||
self.dns_name,
|
||||
self.description,
|
||||
@@ -796,18 +807,6 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
self.address.prefixlen = value
|
||||
mask_length = property(fset=_set_mask_length)
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
if self.interface:
|
||||
return self.interface.device
|
||||
return None
|
||||
|
||||
@property
|
||||
def virtual_machine(self):
|
||||
if self.interface:
|
||||
return self.interface.virtual_machine
|
||||
return None
|
||||
|
||||
def get_status_class(self):
|
||||
return self.STATUS_CLASS_MAP.get(self.status)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Interface
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, BooleanColumn, TagColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, TagColumn, ToggleColumn
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
RIR_UTILIZATION = """
|
||||
@@ -25,15 +25,6 @@ RIR_UTILIZATION = """
|
||||
</div>
|
||||
"""
|
||||
|
||||
RIR_ACTIONS = """
|
||||
<a href="{% url 'ipam:rir_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_rir %}
|
||||
<a href="{% url 'ipam:rir_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}—{% endif %}
|
||||
@@ -47,15 +38,6 @@ ROLE_VLAN_COUNT = """
|
||||
<a href="{% url 'ipam:vlan_list' %}?role={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
<a href="{% url 'ipam:role_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_role %}
|
||||
<a href="{% url 'ipam:role_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
PREFIX_LINK = """
|
||||
{% if record.has_children %}
|
||||
<span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
|
||||
@@ -92,14 +74,6 @@ IPADDRESS_ASSIGN_LINK = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
IPADDRESS_PARENT = """
|
||||
{% if record.interface %}
|
||||
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VRF_LINK = """
|
||||
{% if record.vrf %}
|
||||
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
||||
@@ -144,10 +118,7 @@ VLAN_ROLE_LINK = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VLANGROUP_ACTIONS = """
|
||||
<a href="{% url 'ipam:vlangroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
VLANGROUP_ADD_VLAN = """
|
||||
{% with next_vid=record.get_next_available_vid %}
|
||||
{% if next_vid and perms.ipam.add_vlan %}
|
||||
<a href="{% url 'ipam:vlan_add' %}?site={{ record.site_id }}&group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
|
||||
@@ -155,9 +126,6 @@ VLANGROUP_ACTIONS = """
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if perms.ipam.change_vlangroup %}
|
||||
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VLAN_MEMBER_UNTAGGED = """
|
||||
@@ -168,7 +136,7 @@ VLAN_MEMBER_UNTAGGED = """
|
||||
|
||||
VLAN_MEMBER_ACTIONS = """
|
||||
{% if perms.dcim.change_interface %}
|
||||
<a href="{% if record.device %}{% url 'dcim:interface_edit' pk=record.pk %}{% else %}{% url 'virtualization:interface_edit' pk=record.pk %}{% endif %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil"></i></a>
|
||||
<a href="{% if record.device %}{% url 'dcim:interface_edit' pk=record.pk %}{% else %}{% url 'virtualization:vminterface_edit' pk=record.pk %}{% endif %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -222,11 +190,7 @@ class RIRTable(BaseTable):
|
||||
aggregate_count = tables.Column(
|
||||
verbose_name='Aggregates'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RIR_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(RIR, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RIR
|
||||
@@ -330,11 +294,7 @@ class RoleTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='VLANs'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=ROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(Role, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Role
|
||||
@@ -431,18 +391,14 @@ class IPAddressTable(BaseTable):
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=TENANT_LINK
|
||||
)
|
||||
parent = tables.TemplateColumn(
|
||||
template_code=IPADDRESS_PARENT,
|
||||
orderable=False
|
||||
)
|
||||
interface = tables.Column(
|
||||
orderable=False
|
||||
assigned = tables.BooleanColumn(
|
||||
accessor='assigned_object_id'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = (
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description',
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||
@@ -465,11 +421,11 @@ class IPAddressDetailTable(IPAddressTable):
|
||||
|
||||
class Meta(IPAddressTable.Meta):
|
||||
fields = (
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'dns_name',
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name',
|
||||
'description', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description',
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
|
||||
)
|
||||
|
||||
|
||||
@@ -481,17 +437,13 @@ class IPAddressAssignTable(BaseTable):
|
||||
status = tables.TemplateColumn(
|
||||
template_code=STATUS_LABEL
|
||||
)
|
||||
parent = tables.TemplateColumn(
|
||||
template_code=IPADDRESS_PARENT,
|
||||
orderable=False
|
||||
)
|
||||
interface = tables.Column(
|
||||
assigned_object = tables.Column(
|
||||
orderable=False
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description')
|
||||
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'description')
|
||||
orderable = False
|
||||
|
||||
|
||||
@@ -532,10 +484,9 @@ class VLANGroupTable(BaseTable):
|
||||
vlan_count = tables.Column(
|
||||
verbose_name='VLANs'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=VLANGROUP_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
actions = ButtonsColumn(
|
||||
model=VLANGroup,
|
||||
prepend_template=VLANGROUP_ADD_VLAN
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
|
||||
@@ -416,7 +416,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
|
||||
"""
|
||||
Attempt and fail to delete a VLAN with a Prefix assigned to it.
|
||||
"""
|
||||
vlan = VLAN.objects.first()
|
||||
vlan = VLAN.objects.unrestricted().first()
|
||||
Prefix.objects.create(prefix=IPNetwork('192.0.2.0/24'), vlan=vlan)
|
||||
|
||||
self.add_permissions('ipam.delete_vlan')
|
||||
|
||||
@@ -4,7 +4,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer,
|
||||
from ipam.choices import *
|
||||
from ipam.filters import *
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
||||
from virtualization.models import Cluster, ClusterType, VirtualMachine, VMInterface
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
|
||||
|
||||
@@ -375,6 +375,13 @@ class IPAddressTestCase(TestCase):
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
interfaces = (
|
||||
Interface(device=devices[0], name='Interface 1'),
|
||||
Interface(device=devices[1], name='Interface 2'),
|
||||
Interface(device=devices[2], name='Interface 3'),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||
cluster = Cluster.objects.create(type=clustertype, name='Cluster 1')
|
||||
|
||||
@@ -385,15 +392,12 @@ class IPAddressTestCase(TestCase):
|
||||
)
|
||||
VirtualMachine.objects.bulk_create(virtual_machines)
|
||||
|
||||
interfaces = (
|
||||
Interface(device=devices[0], name='Interface 1'),
|
||||
Interface(device=devices[1], name='Interface 2'),
|
||||
Interface(device=devices[2], name='Interface 3'),
|
||||
Interface(virtual_machine=virtual_machines[0], name='Interface 1'),
|
||||
Interface(virtual_machine=virtual_machines[1], name='Interface 2'),
|
||||
Interface(virtual_machine=virtual_machines[2], name='Interface 3'),
|
||||
vminterfaces = (
|
||||
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'),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
VMInterface.objects.bulk_create(vminterfaces)
|
||||
|
||||
tenant_groups = (
|
||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||
@@ -411,16 +415,16 @@ class IPAddressTestCase(TestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
ipaddresses = (
|
||||
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], interface=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], interface=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], interface=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], interface=interfaces[3], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], interface=interfaces[4], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], interface=interfaces[5], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
)
|
||||
IPAddress.objects.bulk_create(ipaddresses)
|
||||
|
||||
@@ -487,7 +491,14 @@ class IPAddressTestCase(TestCase):
|
||||
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'interface': ['Interface 1', 'Interface 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_vminterface(self):
|
||||
vminterfaces = VMInterface.objects.all()[:2]
|
||||
params = {'vminterface_id': [vminterfaces[0].pk, vminterfaces[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'vminterface': ['Interface 1', 'Interface 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_assigned_to_interface(self):
|
||||
params = {'assigned_to_interface': 'true'}
|
||||
|
||||
@@ -43,7 +43,7 @@ class TestPrefix(TestCase):
|
||||
Prefix(prefix=netaddr.IPNetwork('192.0.2.0/24')),
|
||||
Prefix(prefix=netaddr.IPNetwork('192.0.2.0/24')),
|
||||
))
|
||||
duplicate_prefix_pks = [p.pk for p in prefixes[0].get_duplicates()]
|
||||
duplicate_prefix_pks = [p.pk for p in prefixes[0].get_duplicates().unrestricted()]
|
||||
|
||||
self.assertSetEqual(set(duplicate_prefix_pks), {prefixes[1].pk, prefixes[2].pk})
|
||||
|
||||
@@ -227,7 +227,7 @@ class TestIPAddress(TestCase):
|
||||
IPAddress(address=netaddr.IPNetwork('192.0.2.1/24')),
|
||||
IPAddress(address=netaddr.IPNetwork('192.0.2.1/24')),
|
||||
))
|
||||
duplicate_ip_pks = [p.pk for p in ips[0].get_duplicates()]
|
||||
duplicate_ip_pks = [p.pk for p in ips[0].get_duplicates().unrestricted()]
|
||||
|
||||
self.assertSetEqual(set(duplicate_ip_pks), {ips[1].pk, ips[2].pk})
|
||||
|
||||
|
||||
@@ -236,7 +236,6 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tenant': None,
|
||||
'status': IPAddressStatusChoices.STATUS_RESERVED,
|
||||
'role': IPAddressRoleChoices.ROLE_ANYCAST,
|
||||
'interface': None,
|
||||
'nat_inside': None,
|
||||
'dns_name': 'example',
|
||||
'description': 'A new IP address',
|
||||
|
||||
@@ -24,6 +24,7 @@ urlpatterns = [
|
||||
path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
|
||||
path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
|
||||
path('rirs/<slug:slug>/edit/', views.RIREditView.as_view(), name='rir_edit'),
|
||||
path('rirs/<slug:slug>/delete/', views.RIRDeleteView.as_view(), name='rir_delete'),
|
||||
path('vrfs/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
|
||||
|
||||
# Aggregates
|
||||
@@ -43,6 +44,7 @@ urlpatterns = [
|
||||
path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
|
||||
path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
|
||||
path('roles/<slug:slug>/edit/', views.RoleEditView.as_view(), name='role_edit'),
|
||||
path('roles/<slug:slug>/delete/', views.RoleDeleteView.as_view(), name='role_delete'),
|
||||
path('roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
|
||||
|
||||
# Prefixes
|
||||
@@ -77,6 +79,7 @@ urlpatterns = [
|
||||
path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
|
||||
path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
||||
path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||
path('vlan-groups/<int:pk>/delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'),
|
||||
path('vlan-groups/<int:pk>/vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
|
||||
path('vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
|
||||
|
||||
|
||||
93
netbox/ipam/utils.py
Normal file
93
netbox/ipam/utils.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import netaddr
|
||||
|
||||
from .constants import *
|
||||
from .models import Prefix, VLAN
|
||||
|
||||
|
||||
def add_available_prefixes(parent, prefix_list):
|
||||
"""
|
||||
Create fake Prefix objects for all unallocated space within a prefix.
|
||||
"""
|
||||
|
||||
# Find all unallocated space
|
||||
available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
|
||||
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
|
||||
|
||||
# Concatenate and sort complete list of children
|
||||
prefix_list = list(prefix_list) + available_prefixes
|
||||
prefix_list.sort(key=lambda p: p.prefix)
|
||||
|
||||
return prefix_list
|
||||
|
||||
|
||||
def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
|
||||
"""
|
||||
Annotate ranges of available IP addresses within a given prefix. If is_pool is True, the first and last IP will be
|
||||
considered usable (regardless of mask length).
|
||||
"""
|
||||
|
||||
output = []
|
||||
prev_ip = None
|
||||
|
||||
# Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31.
|
||||
if prefix.version == 4 and prefix.prefixlen < 31 and not is_pool:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
|
||||
else:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
|
||||
|
||||
if not ipaddress_list:
|
||||
return [(
|
||||
int(last_ip_in_prefix - first_ip_in_prefix + 1),
|
||||
'{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
)]
|
||||
|
||||
# Account for any available IPs before the first real IP
|
||||
if ipaddress_list[0].address.ip > first_ip_in_prefix:
|
||||
skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
|
||||
first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
|
||||
# Iterate through existing IPs and annotate free ranges
|
||||
for ip in ipaddress_list:
|
||||
if prev_ip:
|
||||
diff = int(ip.address.ip - prev_ip.address.ip)
|
||||
if diff > 1:
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((diff - 1, first_skipped))
|
||||
output.append(ip)
|
||||
prev_ip = ip
|
||||
|
||||
# Include any remaining available IPs
|
||||
if prev_ip.address.ip < last_ip_in_prefix:
|
||||
skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def add_available_vlans(vlan_group, vlans):
|
||||
"""
|
||||
Create fake records for all gaps between used VLANs
|
||||
"""
|
||||
if not vlans:
|
||||
return [{'vid': VLAN_VID_MIN, 'available': VLAN_VID_MAX - VLAN_VID_MIN + 1}]
|
||||
|
||||
prev_vid = VLAN_VID_MAX
|
||||
new_vlans = []
|
||||
for vlan in vlans:
|
||||
if vlan.vid - prev_vid > 1:
|
||||
new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1})
|
||||
prev_vid = vlan.vid
|
||||
|
||||
if vlans[0].vid > VLAN_VID_MIN:
|
||||
new_vlans.append({'vid': VLAN_VID_MIN, 'available': vlans[0].vid - VLAN_VID_MIN})
|
||||
if prev_vid < VLAN_VID_MAX:
|
||||
new_vlans.append({'vid': prev_vid + 1, 'available': VLAN_VID_MAX - prev_vid})
|
||||
|
||||
vlans = list(vlans) + new_vlans
|
||||
vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])
|
||||
|
||||
return vlans
|
||||
@@ -1,6 +1,6 @@
|
||||
import netaddr
|
||||
from django.conf import settings
|
||||
from django.db.models import Count, Q
|
||||
from django.db.models import Count, Prefetch
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django_tables2 import RequestConfig
|
||||
@@ -11,100 +11,12 @@ from utilities.views import (
|
||||
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView,
|
||||
ObjectListView,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from . import filters, forms, tables
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
def add_available_prefixes(parent, prefix_list):
|
||||
"""
|
||||
Create fake Prefix objects for all unallocated space within a prefix.
|
||||
"""
|
||||
|
||||
# Find all unallocated space
|
||||
available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
|
||||
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
|
||||
|
||||
# Concatenate and sort complete list of children
|
||||
prefix_list = list(prefix_list) + available_prefixes
|
||||
prefix_list.sort(key=lambda p: p.prefix)
|
||||
|
||||
return prefix_list
|
||||
|
||||
|
||||
def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
|
||||
"""
|
||||
Annotate ranges of available IP addresses within a given prefix. If is_pool is True, the first and last IP will be
|
||||
considered usable (regardless of mask length).
|
||||
"""
|
||||
|
||||
output = []
|
||||
prev_ip = None
|
||||
|
||||
# Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31.
|
||||
if prefix.version == 4 and prefix.prefixlen < 31 and not is_pool:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
|
||||
else:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
|
||||
|
||||
if not ipaddress_list:
|
||||
return [(
|
||||
int(last_ip_in_prefix - first_ip_in_prefix + 1),
|
||||
'{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
)]
|
||||
|
||||
# Account for any available IPs before the first real IP
|
||||
if ipaddress_list[0].address.ip > first_ip_in_prefix:
|
||||
skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
|
||||
first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
|
||||
# Iterate through existing IPs and annotate free ranges
|
||||
for ip in ipaddress_list:
|
||||
if prev_ip:
|
||||
diff = int(ip.address.ip - prev_ip.address.ip)
|
||||
if diff > 1:
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((diff - 1, first_skipped))
|
||||
output.append(ip)
|
||||
prev_ip = ip
|
||||
|
||||
# Include any remaining available IPs
|
||||
if prev_ip.address.ip < last_ip_in_prefix:
|
||||
skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def add_available_vlans(vlan_group, vlans):
|
||||
"""
|
||||
Create fake records for all gaps between used VLANs
|
||||
"""
|
||||
if not vlans:
|
||||
return [{'vid': VLAN_VID_MIN, 'available': VLAN_VID_MAX - VLAN_VID_MIN + 1}]
|
||||
|
||||
prev_vid = VLAN_VID_MAX
|
||||
new_vlans = []
|
||||
for vlan in vlans:
|
||||
if vlan.vid - prev_vid > 1:
|
||||
new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1})
|
||||
prev_vid = vlan.vid
|
||||
|
||||
if vlans[0].vid > VLAN_VID_MIN:
|
||||
new_vlans.append({'vid': VLAN_VID_MIN, 'available': vlans[0].vid - VLAN_VID_MIN})
|
||||
if prev_vid < VLAN_VID_MAX:
|
||||
new_vlans.append({'vid': prev_vid + 1, 'available': VLAN_VID_MAX - prev_vid})
|
||||
|
||||
vlans = list(vlans) + new_vlans
|
||||
vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])
|
||||
|
||||
return vlans
|
||||
from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
|
||||
|
||||
|
||||
#
|
||||
@@ -136,19 +48,16 @@ class VRFEditView(ObjectEditView):
|
||||
queryset = VRF.objects.all()
|
||||
model_form = forms.VRFForm
|
||||
template_name = 'ipam/vrf_edit.html'
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFDeleteView(ObjectDeleteView):
|
||||
queryset = VRF.objects.all()
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFBulkImportView(BulkImportView):
|
||||
queryset = VRF.objects.all()
|
||||
model_form = forms.VRFCSVForm
|
||||
table = tables.VRFTable
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFBulkEditView(BulkEditView):
|
||||
@@ -156,14 +65,12 @@ class VRFBulkEditView(BulkEditView):
|
||||
filterset = filters.VRFFilterSet
|
||||
table = tables.VRFTable
|
||||
form = forms.VRFBulkEditForm
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFBulkDeleteView(BulkDeleteView):
|
||||
queryset = VRF.objects.prefetch_related('tenant')
|
||||
filterset = filters.VRFFilterSet
|
||||
table = tables.VRFTable
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -196,10 +103,12 @@ class RIRListView(ObjectListView):
|
||||
'deprecated': 0,
|
||||
'available': 0,
|
||||
}
|
||||
aggregate_list = Aggregate.objects.filter(prefix__family=family, rir=rir)
|
||||
aggregate_list = Aggregate.objects.restrict(request.user).filter(prefix__family=family, rir=rir)
|
||||
for aggregate in aggregate_list:
|
||||
|
||||
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
|
||||
queryset = Prefix.objects.restrict(request.user).filter(
|
||||
prefix__net_contained_or_equal=str(aggregate.prefix)
|
||||
)
|
||||
|
||||
# Find all consumed space for each prefix status (we ignore containers for this purpose).
|
||||
active_prefixes = netaddr.cidr_merge(
|
||||
@@ -249,21 +158,22 @@ class RIRListView(ObjectListView):
|
||||
class RIREditView(ObjectEditView):
|
||||
queryset = RIR.objects.all()
|
||||
model_form = forms.RIRForm
|
||||
default_return_url = 'ipam:rir_list'
|
||||
|
||||
|
||||
class RIRDeleteView(ObjectDeleteView):
|
||||
queryset = RIR.objects.all()
|
||||
|
||||
|
||||
class RIRBulkImportView(BulkImportView):
|
||||
queryset = RIR.objects.all()
|
||||
model_form = forms.RIRCSVForm
|
||||
table = tables.RIRTable
|
||||
default_return_url = 'ipam:rir_list'
|
||||
|
||||
|
||||
class RIRBulkDeleteView(BulkDeleteView):
|
||||
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
||||
filterset = filters.RIRFilterSet
|
||||
table = tables.RIRTable
|
||||
default_return_url = 'ipam:rir_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -345,19 +255,16 @@ class AggregateEditView(ObjectEditView):
|
||||
queryset = Aggregate.objects.all()
|
||||
model_form = forms.AggregateForm
|
||||
template_name = 'ipam/aggregate_edit.html'
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateDeleteView(ObjectDeleteView):
|
||||
queryset = Aggregate.objects.all()
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateBulkImportView(BulkImportView):
|
||||
queryset = Aggregate.objects.all()
|
||||
model_form = forms.AggregateCSVForm
|
||||
table = tables.AggregateTable
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateBulkEditView(BulkEditView):
|
||||
@@ -365,14 +272,12 @@ class AggregateBulkEditView(BulkEditView):
|
||||
filterset = filters.AggregateFilterSet
|
||||
table = tables.AggregateTable
|
||||
form = forms.AggregateBulkEditForm
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateBulkDeleteView(BulkDeleteView):
|
||||
queryset = Aggregate.objects.prefetch_related('rir')
|
||||
filterset = filters.AggregateFilterSet
|
||||
table = tables.AggregateTable
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -387,20 +292,21 @@ class RoleListView(ObjectListView):
|
||||
class RoleEditView(ObjectEditView):
|
||||
queryset = Role.objects.all()
|
||||
model_form = forms.RoleForm
|
||||
default_return_url = 'ipam:role_list'
|
||||
|
||||
|
||||
class RoleDeleteView(ObjectDeleteView):
|
||||
queryset = Role.objects.all()
|
||||
|
||||
|
||||
class RoleBulkImportView(BulkImportView):
|
||||
queryset = Role.objects.all()
|
||||
model_form = forms.RoleCSVForm
|
||||
table = tables.RoleTable
|
||||
default_return_url = 'ipam:role_list'
|
||||
|
||||
|
||||
class RoleBulkDeleteView(BulkDeleteView):
|
||||
queryset = Role.objects.all()
|
||||
table = tables.RoleTable
|
||||
default_return_url = 'ipam:role_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -517,7 +423,7 @@ class PrefixIPAddressesView(ObjectView):
|
||||
|
||||
# Find all IPAddresses belonging to this Prefix
|
||||
ipaddresses = prefix.get_child_ips().restrict(request.user, 'view').prefetch_related(
|
||||
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
|
||||
'vrf', 'primary_ip4_for', 'primary_ip6_for'
|
||||
)
|
||||
|
||||
# Add available IP addresses to the table if requested
|
||||
@@ -556,20 +462,17 @@ class PrefixEditView(ObjectEditView):
|
||||
queryset = Prefix.objects.all()
|
||||
model_form = forms.PrefixForm
|
||||
template_name = 'ipam/prefix_edit.html'
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
class PrefixDeleteView(ObjectDeleteView):
|
||||
queryset = Prefix.objects.all()
|
||||
template_name = 'ipam/prefix_delete.html'
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
class PrefixBulkImportView(BulkImportView):
|
||||
queryset = Prefix.objects.all()
|
||||
model_form = forms.PrefixCSVForm
|
||||
table = tables.PrefixTable
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
class PrefixBulkEditView(BulkEditView):
|
||||
@@ -577,14 +480,12 @@ class PrefixBulkEditView(BulkEditView):
|
||||
filterset = filters.PrefixFilterSet
|
||||
table = tables.PrefixTable
|
||||
form = forms.PrefixBulkEditForm
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
class PrefixBulkDeleteView(BulkDeleteView):
|
||||
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
filterset = filters.PrefixFilterSet
|
||||
table = tables.PrefixTable
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -593,7 +494,7 @@ class PrefixBulkDeleteView(BulkDeleteView):
|
||||
|
||||
class IPAddressListView(ObjectListView):
|
||||
queryset = IPAddress.objects.prefetch_related(
|
||||
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device', 'interface__virtual_machine'
|
||||
'vrf__tenant', 'tenant', 'nat_inside'
|
||||
)
|
||||
filterset = filters.IPAddressFilterSet
|
||||
filterset_form = forms.IPAddressFilterForm
|
||||
@@ -622,7 +523,7 @@ class IPAddressView(ObjectView):
|
||||
).exclude(
|
||||
pk=ipaddress.pk
|
||||
).prefetch_related(
|
||||
'nat_inside', 'interface__device'
|
||||
'nat_inside'
|
||||
)
|
||||
# Exclude anycast IPs if this IP is anycast
|
||||
if ipaddress.role == IPAddressRoleChoices.ROLE_ANYCAST:
|
||||
@@ -630,9 +531,7 @@ class IPAddressView(ObjectView):
|
||||
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
|
||||
|
||||
# Related IP table
|
||||
related_ips = IPAddress.objects.restrict(request.user, 'view').prefetch_related(
|
||||
'interface__device'
|
||||
).exclude(
|
||||
related_ips = IPAddress.objects.restrict(request.user, 'view').exclude(
|
||||
address=str(ipaddress.address)
|
||||
).filter(
|
||||
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
|
||||
@@ -657,17 +556,21 @@ class IPAddressEditView(ObjectEditView):
|
||||
queryset = IPAddress.objects.all()
|
||||
model_form = forms.IPAddressForm
|
||||
template_name = 'ipam/ipaddress_edit.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
|
||||
interface_id = request.GET.get('interface')
|
||||
if interface_id:
|
||||
if 'interface' in request.GET:
|
||||
try:
|
||||
obj.interface = Interface.objects.get(pk=interface_id)
|
||||
obj.assigned_object = Interface.objects.get(pk=request.GET['interface'])
|
||||
except (ValueError, Interface.DoesNotExist):
|
||||
pass
|
||||
|
||||
elif 'vminterface' in request.GET:
|
||||
try:
|
||||
obj.assigned_object = VMInterface.objects.get(pk=request.GET['vminterface'])
|
||||
except (ValueError, VMInterface.DoesNotExist):
|
||||
pass
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
@@ -699,9 +602,7 @@ class IPAddressAssignView(ObjectView):
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
addresses = self.queryset.prefetch_related(
|
||||
'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
|
||||
)
|
||||
addresses = self.queryset.prefetch_related('vrf', 'tenant')
|
||||
# Limit to 100 results
|
||||
addresses = filters.IPAddressFilterSet(request.POST, addresses).qs[:100]
|
||||
table = tables.IPAddressAssignTable(addresses)
|
||||
@@ -715,37 +616,33 @@ class IPAddressAssignView(ObjectView):
|
||||
|
||||
class IPAddressDeleteView(ObjectDeleteView):
|
||||
queryset = IPAddress.objects.all()
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkCreateView(BulkCreateView):
|
||||
queryset = IPAddress.objects.all()
|
||||
form = forms.IPAddressBulkCreateForm
|
||||
model_form = forms.IPAddressBulkAddForm
|
||||
pattern_target = 'address'
|
||||
template_name = 'ipam/ipaddress_bulk_add.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkImportView(BulkImportView):
|
||||
queryset = IPAddress.objects.all()
|
||||
model_form = forms.IPAddressCSVForm
|
||||
table = tables.IPAddressTable
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkEditView(BulkEditView):
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||
filterset = filters.IPAddressFilterSet
|
||||
table = tables.IPAddressTable
|
||||
form = forms.IPAddressBulkEditForm
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkDeleteView(BulkDeleteView):
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||
filterset = filters.IPAddressFilterSet
|
||||
table = tables.IPAddressTable
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -762,21 +659,22 @@ class VLANGroupListView(ObjectListView):
|
||||
class VLANGroupEditView(ObjectEditView):
|
||||
queryset = VLANGroup.objects.all()
|
||||
model_form = forms.VLANGroupForm
|
||||
default_return_url = 'ipam:vlangroup_list'
|
||||
|
||||
|
||||
class VLANGroupDeleteView(ObjectDeleteView):
|
||||
queryset = VLANGroup.objects.all()
|
||||
|
||||
|
||||
class VLANGroupBulkImportView(BulkImportView):
|
||||
queryset = VLANGroup.objects.all()
|
||||
model_form = forms.VLANGroupCSVForm
|
||||
table = tables.VLANGroupTable
|
||||
default_return_url = 'ipam:vlangroup_list'
|
||||
|
||||
|
||||
class VLANGroupBulkDeleteView(BulkDeleteView):
|
||||
queryset = VLANGroup.objects.prefetch_related('site').annotate(vlan_count=Count('vlans'))
|
||||
filterset = filters.VLANGroupFilterSet
|
||||
table = tables.VLANGroupTable
|
||||
default_return_url = 'ipam:vlangroup_list'
|
||||
|
||||
|
||||
class VLANGroupVLANsView(ObjectView):
|
||||
@@ -785,7 +683,9 @@ class VLANGroupVLANsView(ObjectView):
|
||||
def get(self, request, pk):
|
||||
vlan_group = get_object_or_404(self.queryset, pk=pk)
|
||||
|
||||
vlans = VLAN.objects.restrict(request.user, 'view').filter(group_id=pk)
|
||||
vlans = VLAN.objects.restrict(request.user, 'view').filter(group_id=pk).prefetch_related(
|
||||
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user))
|
||||
)
|
||||
vlans = add_available_vlans(vlan_group, vlans)
|
||||
|
||||
vlan_table = tables.VLANDetailTable(vlans)
|
||||
@@ -871,19 +771,16 @@ class VLANEditView(ObjectEditView):
|
||||
queryset = VLAN.objects.all()
|
||||
model_form = forms.VLANForm
|
||||
template_name = 'ipam/vlan_edit.html'
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANDeleteView(ObjectDeleteView):
|
||||
queryset = VLAN.objects.all()
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANBulkImportView(BulkImportView):
|
||||
queryset = VLAN.objects.all()
|
||||
model_form = forms.VLANCSVForm
|
||||
table = tables.VLANTable
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANBulkEditView(BulkEditView):
|
||||
@@ -891,14 +788,12 @@ class VLANBulkEditView(BulkEditView):
|
||||
filterset = filters.VLANFilterSet
|
||||
table = tables.VLANTable
|
||||
form = forms.VLANBulkEditForm
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANBulkDeleteView(BulkDeleteView):
|
||||
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
||||
filterset = filters.VLANFilterSet
|
||||
table = tables.VLANTable
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -945,7 +840,6 @@ class ServiceBulkImportView(BulkImportView):
|
||||
queryset = Service.objects.all()
|
||||
model_form = forms.ServiceCSVForm
|
||||
table = tables.ServiceTable
|
||||
default_return_url = 'ipam:service_list'
|
||||
|
||||
|
||||
class ServiceDeleteView(ObjectDeleteView):
|
||||
@@ -957,11 +851,9 @@ class ServiceBulkEditView(BulkEditView):
|
||||
filterset = filters.ServiceFilterSet
|
||||
table = tables.ServiceTable
|
||||
form = forms.ServiceBulkEditForm
|
||||
default_return_url = 'ipam:service_list'
|
||||
|
||||
|
||||
class ServiceBulkDeleteView(BulkDeleteView):
|
||||
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
|
||||
filterset = filters.ServiceFilterSet
|
||||
table = tables.ServiceTable
|
||||
default_return_url = 'ipam:service_list'
|
||||
|
||||
@@ -24,7 +24,7 @@ class ObjectPermissionBackend(ModelBackend):
|
||||
Return all permissions granted to the user by an ObjectPermission.
|
||||
"""
|
||||
# Retrieve all assigned ObjectPermissions
|
||||
object_permissions = ObjectPermission.objects.filter(
|
||||
object_permissions = ObjectPermission.objects.unrestricted().filter(
|
||||
Q(users=user_obj) |
|
||||
Q(groups__user=user_obj)
|
||||
).prefetch_related('object_types')
|
||||
|
||||
@@ -208,6 +208,10 @@ PLUGINS = []
|
||||
# prefer IPv4 instead.
|
||||
PREFER_IPV4 = False
|
||||
|
||||
# Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1.
|
||||
RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = 22
|
||||
RACK_ELEVATION_DEFAULT_UNIT_WIDTH = 220
|
||||
|
||||
# Remote authentication support
|
||||
REMOTE_AUTH_ENABLED = False
|
||||
REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'
|
||||
|
||||
@@ -99,6 +99,8 @@ PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
|
||||
PLUGINS = getattr(configuration, 'PLUGINS', [])
|
||||
PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
|
||||
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
||||
RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = getattr(configuration, 'RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 22)
|
||||
RACK_ELEVATION_DEFAULT_UNIT_WIDTH = getattr(configuration, 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH', 220)
|
||||
REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False)
|
||||
REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
|
||||
REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', [])
|
||||
|
||||
@@ -168,356 +168,6 @@ class ExternalAuthenticationTestCase(TestCase):
|
||||
self.assertTrue(new_user.has_perms(['dcim.add_site', 'dcim.change_site']))
|
||||
|
||||
|
||||
class ObjectPermissionViewTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
cls.sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
Site(name='Site 3', slug='site-3'),
|
||||
)
|
||||
Site.objects.bulk_create(cls.sites)
|
||||
|
||||
cls.prefixes = (
|
||||
Prefix(prefix=IPNetwork('10.0.0.0/24'), site=cls.sites[0]),
|
||||
Prefix(prefix=IPNetwork('10.0.1.0/24'), site=cls.sites[0]),
|
||||
Prefix(prefix=IPNetwork('10.0.2.0/24'), site=cls.sites[0]),
|
||||
Prefix(prefix=IPNetwork('10.0.3.0/24'), site=cls.sites[1]),
|
||||
Prefix(prefix=IPNetwork('10.0.4.0/24'), site=cls.sites[1]),
|
||||
Prefix(prefix=IPNetwork('10.0.5.0/24'), site=cls.sites[1]),
|
||||
Prefix(prefix=IPNetwork('10.0.6.0/24'), site=cls.sites[2]),
|
||||
Prefix(prefix=IPNetwork('10.0.7.0/24'), site=cls.sites[2]),
|
||||
Prefix(prefix=IPNetwork('10.0.8.0/24'), site=cls.sites[2]),
|
||||
)
|
||||
Prefix.objects.bulk_create(cls.prefixes)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_get_object(self):
|
||||
|
||||
# Attempt to retrieve object without permission
|
||||
response = self.client.get(self.prefixes[0].get_absolute_url())
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Retrieve permitted object
|
||||
response = self.client.get(self.prefixes[0].get_absolute_url())
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
# Attempt to retrieve non-permitted object
|
||||
response = self.client.get(self.prefixes[3].get_absolute_url())
|
||||
self.assertHttpStatus(response, 404)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_list_objects(self):
|
||||
|
||||
# Attempt to list objects without permission
|
||||
response = self.client.get(reverse('ipam:prefix_list'))
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Retrieve all objects. Only permitted objects should be returned.
|
||||
response = self.client.get(reverse('ipam:prefix_list'))
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertIn(str(self.prefixes[0].prefix), str(response.content))
|
||||
self.assertNotIn(str(self.prefixes[3].prefix), str(response.content))
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_create_object(self):
|
||||
initial_count = Prefix.objects.count()
|
||||
form_data = {
|
||||
'prefix': '10.0.9.0/24',
|
||||
'site': self.sites[1].pk,
|
||||
'status': PrefixStatusChoices.STATUS_ACTIVE,
|
||||
}
|
||||
|
||||
# Attempt to create an object without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_add'),
|
||||
'data': form_data,
|
||||
'follow': False, # Do not follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
self.assertEqual(initial_count, Prefix.objects.count())
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view', 'add']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Attempt to create a non-permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_add'),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertEqual(Prefix.objects.count(), initial_count)
|
||||
|
||||
# Create a permitted object
|
||||
form_data['site'] = self.sites[0].pk
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_add'),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertEqual(Prefix.objects.count(), initial_count + 1)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_edit_object(self):
|
||||
form_data = {
|
||||
'prefix': '10.0.9.0/24',
|
||||
'site': self.sites[0].pk,
|
||||
'status': PrefixStatusChoices.STATUS_RESERVED,
|
||||
}
|
||||
|
||||
# Attempt to edit an object without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_edit', kwargs={'pk': self.prefixes[0].pk}),
|
||||
'data': form_data,
|
||||
'follow': False, # Do not follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view', 'change']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Attempt to edit a non-permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_edit', kwargs={'pk': self.prefixes[3].pk}),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 404)
|
||||
|
||||
# Edit a permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_edit', kwargs={'pk': self.prefixes[0].pk}),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
prefix = Prefix.objects.get(pk=self.prefixes[0].pk)
|
||||
self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_RESERVED)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_delete_object(self):
|
||||
form_data = {
|
||||
'confirm': True
|
||||
}
|
||||
|
||||
# Attempt to delete object without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_delete', kwargs={'pk': self.prefixes[0].pk}),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view', 'delete']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Delete permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_delete', kwargs={'pk': self.prefixes[0].pk}),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertFalse(Prefix.objects.filter(pk=self.prefixes[0].pk).exists())
|
||||
|
||||
# Attempt to delete non-permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_delete', kwargs={'pk': self.prefixes[3].pk}),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 404)
|
||||
self.assertTrue(Prefix.objects.filter(pk=self.prefixes[3].pk).exists())
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_bulk_import_objects(self):
|
||||
initial_count = Prefix.objects.count()
|
||||
form_data = {
|
||||
'csv': "prefix,status,site\n"
|
||||
"10.0.9.0/24,Active,Site 1\n"
|
||||
"10.0.10.0/24,Active,Site 2\n"
|
||||
"10.0.11.0/24,Active,Site 3\n",
|
||||
}
|
||||
|
||||
# Attempt to import objects without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_import'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
self.assertEqual(initial_count, Prefix.objects.count())
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['add']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Attempt to create non-permitted objects
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_import'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertEqual(Prefix.objects.count(), initial_count)
|
||||
|
||||
# Create a permitted object
|
||||
form_data = {
|
||||
'csv': "prefix,status,site\n"
|
||||
"10.0.9.0/24,Active,Site 1\n"
|
||||
"10.0.10.0/24,Active,Site 1\n"
|
||||
"10.0.11.0/24,Active,Site 1\n",
|
||||
}
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_import'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertEqual(Prefix.objects.count(), initial_count + 3)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_bulk_edit_objects(self):
|
||||
form_data = {
|
||||
'pk': [p.pk for p in self.prefixes],
|
||||
'status': 'reserved',
|
||||
'_apply': True,
|
||||
}
|
||||
|
||||
# Attempt to edit objects without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_edit'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['change']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Attempt to edit non-permitted objects
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_edit'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
self.assertEqual(Prefix.objects.get(pk=self.prefixes[3].pk).status, 'active')
|
||||
|
||||
# Edit permitted objects
|
||||
form_data['pk'] = [p.pk for p in self.prefixes[:3]]
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_edit'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
self.assertEqual(Prefix.objects.get(pk=self.prefixes[0].pk).status, 'reserved')
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_bulk_delete_objects(self):
|
||||
form_data = {
|
||||
'pk': [p.pk for p in self.prefixes],
|
||||
'confirm': True,
|
||||
'_confirm': True,
|
||||
}
|
||||
|
||||
# Attempt to delete objects without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_delete'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view', 'delete']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Attempt to delete non-permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_delete'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertTrue(Prefix.objects.filter(pk=self.prefixes[3].pk).exists())
|
||||
|
||||
# Delete permitted objects
|
||||
form_data['pk'] = [p.pk for p in self.prefixes[:3]]
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_delete'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
self.assertFalse(Prefix.objects.filter(pk=self.prefixes[0].pk).exists())
|
||||
|
||||
|
||||
class ObjectPermissionAPIViewTestCase(TestCase):
|
||||
client_class = APIClient
|
||||
|
||||
|
||||
@@ -183,13 +183,6 @@ nav ul.pagination {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
/* Racks */
|
||||
div.rack_header {
|
||||
margin-left: 32px;
|
||||
text-align: center;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
/* Devices */
|
||||
table.component-list td.subtable {
|
||||
padding: 0;
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from utilities.tables import BaseTable, TagColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, ButtonsColumn, TagColumn, ToggleColumn
|
||||
from .models import SecretRole, Secret
|
||||
|
||||
SECRETROLE_ACTIONS = """
|
||||
<a href="{% url 'secrets:secretrole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.secrets.change_secretrole %}
|
||||
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# Secret roles
|
||||
@@ -23,11 +14,7 @@ class SecretRoleTable(BaseTable):
|
||||
secret_count = tables.Column(
|
||||
verbose_name='Secrets'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=SECRETROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(SecretRole, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SecretRole
|
||||
|
||||
@@ -13,6 +13,7 @@ urlpatterns = [
|
||||
path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
|
||||
path('secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
|
||||
path('secret-roles/<slug:slug>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
|
||||
path('secret-roles/<slug:slug>/delete/', views.SecretRoleDeleteView.as_view(), name='secretrole_delete'),
|
||||
path('secret-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
|
||||
|
||||
# Secrets
|
||||
|
||||
@@ -36,20 +36,21 @@ class SecretRoleListView(ObjectListView):
|
||||
class SecretRoleEditView(ObjectEditView):
|
||||
queryset = SecretRole.objects.all()
|
||||
model_form = forms.SecretRoleForm
|
||||
default_return_url = 'secrets:secretrole_list'
|
||||
|
||||
|
||||
class SecretRoleDeleteView(ObjectDeleteView):
|
||||
queryset = SecretRole.objects.all()
|
||||
|
||||
|
||||
class SecretRoleBulkImportView(BulkImportView):
|
||||
queryset = SecretRole.objects.all()
|
||||
model_form = forms.SecretRoleCSVForm
|
||||
table = tables.SecretRoleTable
|
||||
default_return_url = 'secrets:secretrole_list'
|
||||
|
||||
|
||||
class SecretRoleBulkDeleteView(BulkDeleteView):
|
||||
queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
||||
table = tables.SecretRoleTable
|
||||
default_return_url = 'secrets:secretrole_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -147,7 +148,6 @@ class SecretEditView(ObjectEditView):
|
||||
|
||||
class SecretDeleteView(ObjectDeleteView):
|
||||
queryset = Secret.objects.all()
|
||||
default_return_url = 'secrets:secret_list'
|
||||
|
||||
|
||||
class SecretBulkImportView(BulkImportView):
|
||||
@@ -155,7 +155,6 @@ class SecretBulkImportView(BulkImportView):
|
||||
model_form = forms.SecretCSVForm
|
||||
table = tables.SecretTable
|
||||
template_name = 'secrets/secret_import.html'
|
||||
default_return_url = 'secrets:secret_list'
|
||||
widget_attrs = {'class': 'requires-session-key'}
|
||||
|
||||
master_key = None
|
||||
@@ -203,11 +202,9 @@ class SecretBulkEditView(BulkEditView):
|
||||
filterset = filters.SecretFilterSet
|
||||
table = tables.SecretTable
|
||||
form = forms.SecretBulkEditForm
|
||||
default_return_url = 'secrets:secret_list'
|
||||
|
||||
|
||||
class SecretBulkDeleteView(BulkDeleteView):
|
||||
queryset = Secret.objects.prefetch_related('role', 'device')
|
||||
filterset = filters.SecretFilterSet
|
||||
table = tables.SecretTable
|
||||
default_return_url = 'secrets:secret_list'
|
||||
|
||||
@@ -88,6 +88,16 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% elif position_stack %}
|
||||
<div class="col-md-11 col-md-offset-1">
|
||||
<h3 class="text-warning text-center">
|
||||
{% with last_position=position_stack|last %}
|
||||
Trace completed, but there is no Front Port corresponding to
|
||||
<a href="{{ last_position.device.get_absolute_url }}">{{ last_position.device }}</a> {{ last_position }}.<br>
|
||||
Therefore no end-to-end connection can be established.
|
||||
{% endwith %}
|
||||
</h3>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-md-11 col-md-offset-1">
|
||||
<h3 class="text-success text-center">Trace completed!</h3>
|
||||
|
||||
103
netbox/templates/dcim/consoleport.html
Normal file
103
netbox/templates/dcim/consoleport.html
Normal file
@@ -0,0 +1,103 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Port</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if instance.connected_endpoint %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:consoleport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.connection_status %}
|
||||
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
103
netbox/templates/dcim/consoleserverport.html
Normal file
103
netbox/templates/dcim/consoleserverport.html
Normal file
@@ -0,0 +1,103 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Server Port</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if instance.connected_endpoint %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:consoleserverport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.connection_status %}
|
||||
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -74,6 +74,9 @@
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<li><a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Device Bays</a></li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<li><a href="{% url 'dcim:inventoryitem_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Inventory Items</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -326,34 +329,85 @@
|
||||
{% plugin_left_page device %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if console_ports or power_ports %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console / Power</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for cp in console_ports %}
|
||||
{% include 'dcim/inc/consoleport.html' %}
|
||||
{% endfor %}
|
||||
{% for pp in power_ports %}
|
||||
{% include 'dcim/inc/powerport.html' %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
||||
</a>
|
||||
{% if console_ports %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Ports</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for cp in console_ports %}
|
||||
{% include 'dcim/inc/consoleport.html' %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if console_ports and perms.dcim.change_consoleport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
||||
</a>
|
||||
{% if console_ports and perms.dcim.delete_consoleport %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if console_ports and perms.dcim.add_consoleport %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if power_ports %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Ports</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for pp in power_ports %}
|
||||
{% include 'dcim/inc/powerport.html' %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if power_ports and perms.dcim.change_powerport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if power_ports and perms.dcim.delete_powerport %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if power_ports and perms.dcim.add_powerport %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if power_ports and poweroutlets %}
|
||||
<div class="panel panel-default">
|
||||
@@ -501,262 +555,242 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% if device_bays or device.device_type.is_parent_device %}
|
||||
{% if perms.dcim.delete_devicebay %}
|
||||
<form method="post">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Device Bays</strong>
|
||||
</div>
|
||||
<table class="table table-hover table-headings panel-body component-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Description</th>
|
||||
<th colspan="2">Installed Device</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for devicebay in device_bays %}
|
||||
{% include 'dcim/inc/devicebay.html' %}
|
||||
{% empty %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Device Bays</strong>
|
||||
</div>
|
||||
<table class="table table-hover table-headings panel-body component-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">— No device bays defined —</td>
|
||||
{% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Description</th>
|
||||
<th colspan="2">Installed Device</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if device_bays and perms.dcim.change_devicebay %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if device_bays and perms.dcim.delete_devicebay %}
|
||||
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.delete_devicebay %}
|
||||
</form>
|
||||
{% endif %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for devicebay in device_bays %}
|
||||
{% include 'dcim/inc/devicebay.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">— No device bays defined —</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if device_bays and perms.dcim.change_devicebay %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if device_bays and perms.dcim.delete_devicebay %}
|
||||
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if interfaces %}
|
||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<form method="post">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interfaces</strong>
|
||||
<div class="pull-right noprint">
|
||||
<button class="btn btn-default btn-xs toggle-ips" selected="selected">
|
||||
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2 pull-right noprint">
|
||||
<input class="form-control interface-filter" type="text" placeholder="Filter" title="RegEx-enabled" style="height: 23px" />
|
||||
</div>
|
||||
</div>
|
||||
<table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>LAG</th>
|
||||
<th>Description</th>
|
||||
<th>MTU</th>
|
||||
<th>Mode</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for iface in interfaces %}
|
||||
{% include 'dcim/inc/interface.html' %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if interfaces and perms.dcim.change_interface %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if interfaces and perms.dcim.change_interface %}
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if interfaces and perms.dcim.delete_interface %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
||||
</a>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interfaces</strong>
|
||||
<div class="pull-right noprint">
|
||||
<button class="btn btn-default btn-xs toggle-ips" selected="selected">
|
||||
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.delete_interface %}
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="col-md-2 pull-right noprint">
|
||||
<input class="form-control interface-filter" type="text" placeholder="Filter" title="Filter text (regular expressions supported)" style="height: 23px" />
|
||||
</div>
|
||||
</div>
|
||||
<table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>LAG</th>
|
||||
<th>Description</th>
|
||||
<th>MTU</th>
|
||||
<th>Mode</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for iface in interfaces %}
|
||||
{% include 'dcim/inc/interface.html' %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if interfaces and perms.dcim.change_interface %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if interfaces and perms.dcim.change_interface %}
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if interfaces and perms.dcim.delete_interface %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if consoleserverports %}
|
||||
{% if perms.dcim.delete_consoleserverport %}
|
||||
<form method="post">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Server Ports</strong>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Server Ports</strong>
|
||||
</div>
|
||||
<table class="table table-hover table-headings panel-body component-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for csp in consoleserverports %}
|
||||
{% include 'dcim/inc/consoleserverport.html' %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if consoleserverports and perms.dcim.change_consoleport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if consoleserverports and perms.dcim.delete_consoleserverport %}
|
||||
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_consoleserverport %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover table-headings panel-body component-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for csp in consoleserverports %}
|
||||
{% include 'dcim/inc/consoleserverport.html' %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if consoleserverports and perms.dcim.change_consoleport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if consoleserverports and perms.dcim.delete_consoleserverport %}
|
||||
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_consoleserverport %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.delete_consoleserverport %}
|
||||
</form>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if poweroutlets %}
|
||||
{% if perms.dcim.delete_poweroutlet %}
|
||||
<form method="post">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Outlets</strong>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Outlets</strong>
|
||||
</div>
|
||||
<table class="table table-hover table-headings panel-body component-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Input/Leg</th>
|
||||
<th>Description</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="3">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for po in poweroutlets %}
|
||||
{% include 'dcim/inc/poweroutlet.html' %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if poweroutlets and perms.dcim.change_powerport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if poweroutlets and perms.dcim.delete_poweroutlet %}
|
||||
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_poweroutlet %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover table-headings panel-body component-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Input/Leg</th>
|
||||
<th>Description</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="3">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for po in poweroutlets %}
|
||||
{% include 'dcim/inc/poweroutlet.html' %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if poweroutlets and perms.dcim.change_powerport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if poweroutlets and perms.dcim.delete_poweroutlet %}
|
||||
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_poweroutlet %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.delete_poweroutlet %}
|
||||
</form>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if front_ports %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Front Ports</strong>
|
||||
@@ -815,7 +849,6 @@
|
||||
{% if rear_ports %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Rear Ports</strong>
|
||||
|
||||
41
netbox/templates/dcim/device_component.html
Normal file
41
netbox/templates/dcim/device_component.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load helpers %}
|
||||
{% load perms %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row noprint">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
||||
<li><a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a></li>
|
||||
<li><a href="{% url instance|viewname:"list" %}?device_id={{ instance.device.pk }}">{{ instance|meta:"verbose_name_plural"|bettertitle }}</a></li>
|
||||
<li>{{ instance }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right noprint">
|
||||
{% plugin_buttons instance %}
|
||||
{% if request.user|can_change:instance %}
|
||||
<a href="{% url instance|viewname:"edit" pk=instance.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span> Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if request.user|can_delete:instance %}
|
||||
<a href="{% url instance|viewname:"delete" pk=instance.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span> Delete
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ instance.device }} / {{ instance }}{% endblock %}</h1>
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ instance.get_absolute_url }}">{{ instance|meta:"verbose_name"|bettertitle }}</a>
|
||||
</li>
|
||||
{% if perms.extras.view_objectchange %}
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url instance|viewname:"changelog" pk=instance.pk %}">Change Log</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
@@ -5,61 +5,61 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Chassis</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Model</td>
|
||||
<td>{{ device.device_type.display_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Serial Number</td>
|
||||
<td><span>{{ device.serial|placeholder }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Asset Tag</td>
|
||||
<td><span>{{ device.asset_tag|placeholder }}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Hardware</strong>
|
||||
</div>
|
||||
<table class="table table-hover table-condensed panel-body" id="hardware">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th></th>
|
||||
<th>Manufacturer</th>
|
||||
<th>Part ID</th>
|
||||
<th>Serial Number</th>
|
||||
<th>Asset Tag</th>
|
||||
<th>Description</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in inventory_items %}
|
||||
{% with template_name='dcim/inc/inventoryitem.html' indent=0 %}
|
||||
{% include template_name %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'dcim:inventoryitem_add' %}?device={{ device.pk }}&return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item
|
||||
</a>
|
||||
<div class="col-md-12">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Inventory Items</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<table class="table table-hover table-condensed panel-body" id="hardware">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if perms.dcim.change_inventoryitem or perms.dcim.delete_inventoryitem %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Manufacturer</th>
|
||||
<th>Part ID</th>
|
||||
<th>Serial Number</th>
|
||||
<th>Asset Tag</th>
|
||||
<th>Discovered</th>
|
||||
<th>Description</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in inventory_items %}
|
||||
{% with template_name='dcim/inc/inventoryitem.html' indent=0 %}
|
||||
{% include template_name %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if inventory_items and perms.dcim.change_inventoryitem %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:inventoryitem_bulk_edit' %}?device={{ device.pk }}&return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if inventory_items and perms.dcim.delete_inventoryitem %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:inventoryitem_bulk_delete' %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:inventoryitem_add' %}?device={{ device.pk }}&return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -14,12 +14,8 @@
|
||||
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
|
||||
{% if perms.dcim.add_rearport %}<li><a href="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Rear Ports</a></li>{% endif %}
|
||||
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
|
||||
{% if perms.dcim.add_inventoryitem %}<li><a href="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Inventory Items</a></li>{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_virtualchassis %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:virtualchassis_add' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-sm">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create Virtual Chassis
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
70
netbox/templates/dcim/devicebay.html
Normal file
70
netbox/templates/dcim/devicebay.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Device Bay</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Installed Device</strong>
|
||||
</div>
|
||||
{% if instance.installed_device %}
|
||||
{% with device=instance.installed_device %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ device.get_absolute_url }}">{{ device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device Type</td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
None
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -131,7 +131,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Instances</td>
|
||||
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
||||
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ instance_count }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -173,7 +173,7 @@
|
||||
{% if devicetype.is_parent_device or devicebay_table.rows %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicebaytemplate_add' edit_url=None delete_url='dcim:devicebaytemplate_bulk_delete' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicebaytemplate_add' edit_url='dcim:devicebaytemplate_bulk_edit' delete_url='dcim:devicebaytemplate_bulk_delete' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
91
netbox/templates/dcim/frontport.html
Normal file
91
netbox/templates/dcim/frontport.html
Normal file
@@ -0,0 +1,91 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Front Port</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rear Port</td>
|
||||
<td>
|
||||
<a href="{{ instance.rear_port.get_absolute_url }}">{{ instance.rear_port }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rear Port Position</td>
|
||||
<td>{{ instance.rear_port_position }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:frontport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.cable.status %}
|
||||
<span class="label label-success">{{ instance.cable.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.cable.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -16,7 +16,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Component</td>
|
||||
<td>{{ termination }}</td>
|
||||
<td>
|
||||
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{# Circuit termination #}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<tr class="consoleport{% if cp.cable %} {{ cp.cable.get_status_class }}{% endif %}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
|
||||
<i class="fa fa-fw fa-keyboard-o"></i>
|
||||
<a href="{{ cp.get_absolute_url }}">{{ cp }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
|
||||
<i class="fa fa-fw fa-keyboard-o"></i>
|
||||
<a href="{{ csp.get_absolute_url }}">{{ csp }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
|
||||
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i>
|
||||
<a href="{{ devicebay.get_absolute_url }}">{{ devicebay.name }}</a>
|
||||
</td>
|
||||
|
||||
{# Status #}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-square{% if not frontport.cable %}-o{% endif %}"></i> {{ frontport }}
|
||||
<i class="fa fa-fw fa-square{% if not frontport.cable %}-o{% endif %}"></i>
|
||||
<a href="{{ frontport.get_absolute_url }}">{{ frontport }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||
<a href="{% url 'dcim:interface_edit' pk=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -176,7 +176,7 @@
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_delete' pk=iface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=iface.pk %}{% endif %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||
<a href="{% url 'dcim:interface_delete' pk=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
{% load helpers %}
|
||||
<tr>
|
||||
<td style="padding-left: {{ indent|add:5 }}px">{{ item }}</td>
|
||||
<td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
|
||||
<td>{{ item.manufacturer|default:"" }}</td>
|
||||
<td>{{ item.part_id }}</td>
|
||||
<td>{{ item.serial }}</td>
|
||||
<td>{{ item.asset_tag|default:"" }}</td>
|
||||
<td>{{ item.description }}</td>
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_inventoryitem or perms.dcim.delete_inventoryitem %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ item.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td style="padding-left: {{ indent|add:5 }}px">
|
||||
<a href="{{ item.get_absolute_url }}">{{ item }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if item.manufacturer %}
|
||||
<a href="{{ item.manufacturer.get_absolute_url }}">{{ item.manufacturer }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.part_id|placeholder }}</td>
|
||||
<td>{{ item.serial|placeholder }}</td>
|
||||
<td>{{ item.asset_tag|placeholder }}</td>
|
||||
<td>
|
||||
{% if item.discovered %}
|
||||
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.description|placeholder }}</td>
|
||||
<td class="text-right noprint">
|
||||
{% if perms.dcim.change_inventoryitem %}
|
||||
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ po }}
|
||||
<i class="fa fa-fw fa-bolt"></i>
|
||||
<a href="{{ po.get_absolute_url }}">{{ po }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<tr class="powerport{% if pp.cable %} {{ pp.cable.get_status_class }}{% endif %}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_powerport or perms.dcim.delete_powerport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ pp.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ pp }}
|
||||
<i class="fa fa-fw fa-bolt"></i>
|
||||
<a href="{{ pp.get_absolute_url }}">{{ pp }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<object data="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg" class="rack_elevation"></object>
|
||||
<div style="margin-left: -30px">
|
||||
<object data="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg" class="rack_elevation"></object>
|
||||
</div>
|
||||
<div class="text-center text-small">
|
||||
<a href="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg">
|
||||
<i class="fa fa-download"></i> Save SVG
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-square{% if not rearport.cable %}-o{% endif %}"></i> {{ rearport }}
|
||||
<i class="fa fa-fw fa-square{% if not rearport.cable %}-o{% endif %}"></i>
|
||||
<a href="{{ rearport.get_absolute_url }}">{{ rearport }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
||||
@@ -1,258 +1,227 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row noprint">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
{% if interface.device %}
|
||||
<li><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{{ interface.parent.get_absolute_url }}">{{ interface.parent }}</a></li>
|
||||
<li>{{ interface }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.change_interface %}
|
||||
<a href="{% if interface.device %}{% url 'dcim:interface_edit' pk=interface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=interface.pk %}{% endif %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span> Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_interface %}
|
||||
<a href="{% if interface.device %}{% url 'dcim:interface_delete' pk=interface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=interface.pk %}{% endif %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span> Delete
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ interface.parent }} / {{ interface.name }}{% endblock %}</h1>
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ interface.get_absolute_url }}">Interface</a>
|
||||
</li>
|
||||
{% if perms.extras.view_objectchange %}
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:interface_changelog' pk=interface.pk %}">Change Log</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interface</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>{% if interface.device %}Device{% else %}Virtual Machine{% endif %}</td>
|
||||
<td>
|
||||
<a href="{{ interface.parent.get_absolute_url }}">{{ interface.parent }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ interface.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ interface.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ interface.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td>
|
||||
{% if interface.enabled %}
|
||||
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fa fa-close"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LAG</td>
|
||||
<td>
|
||||
{% if interface.lag%}
|
||||
<a href="{{ interface.lag.get_absolute_url }}">{{ interface.lag }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ interface.description|placeholder }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MTU</td>
|
||||
<td>{{ interface.mtu|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MAC Address</td>
|
||||
<td>{{ interface.mac_address|placeholder }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>802.1Q Mode</td>
|
||||
<td>{{ interface.get_mode_display }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=interface.tags.all %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if interface.is_connectable %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
<strong>Interface</strong>
|
||||
</div>
|
||||
{% if interface.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if connected_interface %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ connected_interface.parent.get_absolute_url }}">{{ connected_interface.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ connected_interface.get_absolute_url }}">{{ connected_interface.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ connected_interface.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td>
|
||||
{% if connected_interface.enabled %}
|
||||
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fa fa-close"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LAG</td>
|
||||
<td>
|
||||
{% if connected_interface.lag%}
|
||||
<a href="{{ connected_interface.lag.get_absolute_url }}">{{ connected_interface.lag }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ connected_interface.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MTU</td>
|
||||
<td>{{ connected_interface.mtu|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MAC Address</td>
|
||||
<td>{{ connected_interface.mac_address|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>802.1Q Mode</td>
|
||||
<td>{{ connected_interface.get_mode_display }}</td>
|
||||
</tr>
|
||||
{% elif connected_circuittermination %}
|
||||
{% with ct=connected_circuittermination %}
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td><a href="{{ ct.circuit.provider.get_absolute_url }}">{{ ct.circuit.provider }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Circuit</td>
|
||||
<td><a href="{{ ct.circuit.get_absolute_url }}">{{ ct.circuit }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Side</td>
|
||||
<td>{{ ct.term_side }}</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ interface.cable.get_absolute_url }}">{{ interface.cable }}</a>
|
||||
<a href="{% url 'dcim:interface_trace' pk=interface.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if interface.connection_status %}
|
||||
<span class="label label-success">{{ interface.get_connection_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ interface.get_connection_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if interface.is_lag %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>LAG Members</strong></div>
|
||||
<table class="table table-hover table-headings panel-body">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parent</th>
|
||||
<th>Interface</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in interface.member_interfaces.all %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ member.parent.get_absolute_url }}">{{ member.parent }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ member.get_absolute_url }}">{{ member }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ member.get_type_display }}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-muted">No member interfaces</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td>
|
||||
{% if instance.enabled %}
|
||||
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fa fa-close"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LAG</td>
|
||||
<td>
|
||||
{% if instance.lag%}
|
||||
<a href="{{ instance.lag.get_absolute_url }}">{{ instance.lag }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MTU</td>
|
||||
<td>{{ instance.mtu|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MAC Address</td>
|
||||
<td><span class="text-monospace">{{ instance.mac_address|placeholder }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>802.1Q Mode</td>
|
||||
<td>{{ instance.get_mode_display }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if instance.is_connectable %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if connected_interface %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ connected_interface.device.get_absolute_url }}">{{ connected_interface.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ connected_interface.get_absolute_url }}">{{ connected_interface.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ connected_interface.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td>
|
||||
{% if connected_interface.enabled %}
|
||||
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fa fa-close"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LAG</td>
|
||||
<td>
|
||||
{% if connected_interface.lag%}
|
||||
<a href="{{ connected_interface.lag.get_absolute_url }}">{{ connected_interface.lag }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ connected_interface.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MTU</td>
|
||||
<td>{{ connected_interface.mtu|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MAC Address</td>
|
||||
<td>{{ connected_interface.mac_address|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>802.1Q Mode</td>
|
||||
<td>{{ connected_interface.get_mode_display }}</td>
|
||||
</tr>
|
||||
{% elif connected_circuittermination %}
|
||||
{% with ct=connected_circuittermination %}
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td><a href="{{ ct.circuit.provider.get_absolute_url }}">{{ ct.circuit.provider }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Circuit</td>
|
||||
<td><a href="{{ ct.circuit.get_absolute_url }}">{{ ct.circuit }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Side</td>
|
||||
<td>{{ ct.term_side }}</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:interface_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.connection_status %}
|
||||
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if instance.is_lag %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>LAG Members</strong></div>
|
||||
<table class="table table-hover table-headings panel-body">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parent</th>
|
||||
<th>Interface</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in instance.member_interfaces.all %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ member.device.get_absolute_url }}">{{ member.device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ member.get_absolute_url }}">{{ member }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ member.get_type_display }}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-muted">No member interfaces</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
73
netbox/templates/dcim/inventoryitem.html
Normal file
73
netbox/templates/dcim/inventoryitem.html
Normal file
@@ -0,0 +1,73 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Inventory Item</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Parent Item</td>
|
||||
<td>
|
||||
{% if instance.parent %}
|
||||
<a href="{{ instance.parent.get_absolute_url }}">{{ instance.parent }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manufacturer</td>
|
||||
<td>
|
||||
{% if instance.manufacturer %}
|
||||
<a href="{{ instance.manufacturer.get_absolute_url }}">{{ instance.manufacturer }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Part ID</td>
|
||||
<td>{{ instance.part_id|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Serial</td>
|
||||
<td>{{ instance.serial|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Asset Tag</td>
|
||||
<td>{{ instance.asset_tag|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
111
netbox/templates/dcim/poweroutlet.html
Normal file
111
netbox/templates/dcim/poweroutlet.html
Normal file
@@ -0,0 +1,111 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Outlet</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Port</td>
|
||||
<td>{{ instance.power_port }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Feed Leg</td>
|
||||
<td>{{ instance.get_feed_leg_display }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if instance.connected_endpoint %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:poweroutlet_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.connection_status %}
|
||||
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
111
netbox/templates/dcim/powerport.html
Normal file
111
netbox/templates/dcim/powerport.html
Normal file
@@ -0,0 +1,111 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Port</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Maximum Draw</td>
|
||||
<td>{{ instance.maximum_draw|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Allocated Draw</td>
|
||||
<td>{{ instance.allocated_draw|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if instance.connected_endpoint %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:powerport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.connection_status %}
|
||||
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -318,16 +318,12 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="rack_header">
|
||||
<h4>Front</h4>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<h4>Front</h4>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='front' %}
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="rack_header">
|
||||
<h4>Rear</h4>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<h4>Rear</h4>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='rear' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
85
netbox/templates/dcim/rearport.html
Normal file
85
netbox/templates/dcim/rearport.html
Normal file
@@ -0,0 +1,85 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Rear Port</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Positions</td>
|
||||
<td>{{ instance.positions }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:rearport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.cable.status %}
|
||||
<span class="label label-success">{{ instance.cable.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.cable.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -9,7 +9,9 @@
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a></li>
|
||||
<li><a href="{% url 'dcim:virtualchassis_list' %}?site={{ virtualchassis.master.site.slug }}">{{ virtualchassis.master.site }}</a></li>
|
||||
{% if virtualchassis.master %}
|
||||
<li><a href="{% url 'dcim:virtualchassis_list' %}?site={{ virtualchassis.master.site.slug }}">{{ virtualchassis.master.site }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ virtualchassis }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
@@ -63,7 +65,17 @@
|
||||
<tr>
|
||||
<td>Domain</td>
|
||||
<td>{{ virtualchassis.domain|placeholder }}</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Master</td>
|
||||
<td>
|
||||
{% if virtualchassis.master %}
|
||||
<a href="{{ virtualchassis.master.get_absolute_url }}">{{ virtualchassis.master }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=virtualchassis.tags.all url='dcim:virtualchassis_list' %}
|
||||
@@ -81,7 +93,7 @@
|
||||
<th>Master</th>
|
||||
<th>Priority</th>
|
||||
</tr>
|
||||
{% for vc_member in virtualchassis.members.all %}
|
||||
{% for vc_member in members %}
|
||||
<tr{% if vc_member == device %} class="info"{% endif %}>
|
||||
<td>
|
||||
<a href="{{ vc_member.get_absolute_url }}">{{ vc_member }}</a>
|
||||
|
||||
22
netbox/templates/dcim/virtualchassis_add.html
Normal file
22
netbox/templates/dcim/virtualchassis_add.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Virtual Chassis</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.name %}
|
||||
{% render_field form.domain %}
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Member Devices</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.site %}
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.members %}
|
||||
{% render_field form.initial_position %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -4,7 +4,7 @@
|
||||
<strong>Tags</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% for tag in tags.unrestricted %}
|
||||
{% for tag in tags.all %}
|
||||
{% tag tag url %}
|
||||
{% empty %}
|
||||
<span class="text-muted">No tags assigned</span>
|
||||
|
||||
@@ -144,6 +144,12 @@
|
||||
<a href="{% url 'dcim:platform_list' %}">Platforms</a>
|
||||
</li>
|
||||
<li{% if not perms.dcim.view_virtualchassis %} class="disabled"{% endif %}>
|
||||
{% if perms.dcim.add_virtualchassis %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'dcim:virtualchassis_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||
<a href="{% url 'dcim:virtualchassis_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
@@ -167,16 +173,6 @@
|
||||
<a href="{% url 'dcim:manufacturer_list' %}">Manufacturers</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Inventory</li>
|
||||
<li{% if not perms.dcim.view_inventoryitem %} class="disabled"{% endif %}>
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:inventoryitem_list' %}">Inventory Items</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Connections</li>
|
||||
<li{% if not perms.dcim.view_cable %} class="disabled"{% endif %}>
|
||||
{% if perms.dcim.add_cable %}
|
||||
@@ -261,6 +257,14 @@
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:devicebay_list' %}">Device Bays</a>
|
||||
</li>
|
||||
<li{% if not perms.dcim.view_inventoryitem %} class="disabled"{% endif %}>
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:inventoryitem_list' %}">Inventory Items</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
@@ -372,6 +376,14 @@
|
||||
{% endif %}
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a>
|
||||
</li>
|
||||
<li{% if not perms.virtualization.view_vminterface%} class="disabled"{% endif %}>
|
||||
{% if perms.virtualization.add_vminterface %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'virtualization:vminterface_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'virtualization:vminterface_list' %}">Interfaces</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Clusters</li>
|
||||
<li{% if not perms.virtualization.view_cluster %} class="disabled"{% endif %}>
|
||||
|
||||
@@ -19,21 +19,21 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
<form method="get">
|
||||
{% for k, v_list in request.GET.lists %}
|
||||
{% if k != 'per_page' %}
|
||||
{% for v in v_list %}
|
||||
<input type="hidden" name="{{ k }}" value="{{ v }}" />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<select name="per_page" id="per_page">
|
||||
{% for n in settings.PER_PAGE_DEFAULTS %}
|
||||
<option value="{{ n }}"{% if page.paginator.per_page == n %} selected="selected"{% endif %}>{{ n }}</option>
|
||||
{% endfor %}
|
||||
</select> per page
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="get">
|
||||
{% for k, v_list in request.GET.lists %}
|
||||
{% if k != 'per_page' %}
|
||||
{% for v in v_list %}
|
||||
<input type="hidden" name="{{ k }}" value="{{ v }}" />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<select name="per_page" id="per_page">
|
||||
{% for n in settings.PER_PAGE_DEFAULTS %}
|
||||
<option value="{{ n }}"{% if page.paginator.per_page == n %} selected="selected"{% endif %}>{{ n }}</option>
|
||||
{% endfor %}
|
||||
</select> per page
|
||||
</form>
|
||||
{% if page %}
|
||||
<div class="text-right text-muted">
|
||||
Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }}
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
<tr>
|
||||
<td>Assignment</td>
|
||||
<td>
|
||||
{% if ipaddress.interface %}
|
||||
<span><a href="{{ ipaddress.interface.parent.get_absolute_url }}">{{ ipaddress.interface.parent }}</a> ({{ ipaddress.interface }})</span>
|
||||
{% if ipaddress.assigned_object %}
|
||||
<span><a href="{{ ipaddress.assigned_object.parent.get_absolute_url }}">{{ ipaddress.assigned_object.parent }}</a> ({{ ipaddress.assigned_object }})</span>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
@@ -132,8 +132,8 @@
|
||||
<td>
|
||||
{% if ipaddress.nat_inside %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=ipaddress.nat_inside.pk %}">{{ ipaddress.nat_inside }}</a>
|
||||
{% if ipaddress.nat_inside.interface %}
|
||||
(<a href="{{ ipaddress.nat_inside.interface.parent.get_absolute_url }}">{{ ipaddress.nat_inside.interface.parent }}</a>)
|
||||
{% if ipaddress.nat_inside.assigned_object %}
|
||||
(<a href="{{ ipaddress.nat_inside.assigned_object.parent.get_absolute_url }}">{{ ipaddress.nat_inside.assigned_object.parent }}</a>)
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
|
||||
@@ -28,25 +28,30 @@
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
{% if obj.interface %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interface Assignment</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">{{ obj.interface.parent|meta:"verbose_name"|bettertitle }}</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
<a href="{{ obj.interface.parent.get_absolute_url }}">{{ obj.interface.parent }}</a>
|
||||
</p>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interface Assignment</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% with vm_tab_active=obj.vminterface.exists %}
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation"{% if not vm_tab_active %} class="active"{% endif %}><a href="#device" role="tab" data-toggle="tab">Device</a></li>
|
||||
<li role="presentation"{% if vm_tab_active %} class="active"{% endif %}><a href="#virtualmachine" role="tab" data-toggle="tab">Virtual Machine</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane{% if not vm_tab_active %} active{% endif %}" id="device">
|
||||
{% render_field form.device %}
|
||||
{% render_field form.interface %}
|
||||
</div>
|
||||
<div class="tab-pane{% if vm_tab_active %} active{% endif %}" id="virtualmachine">
|
||||
{% render_field form.virtual_machine %}
|
||||
{% render_field form.vminterface %}
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface %}
|
||||
{% render_field form.primary_for_parent %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% render_field form.primary_for_parent %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>NAT IP (Inside)</strong></div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#tableconfig" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
|
||||
{% endif %}
|
||||
{% if permissions.add and 'add' in action_buttons %}
|
||||
{% add_button content_type.model_class|url_name:"add" %}
|
||||
{% add_button content_type.model_class|validated_viewname:"add" %}
|
||||
{% endif %}
|
||||
{% if permissions.add and 'import' in action_buttons %}
|
||||
{% import_button content_type.model_class|url_name:"import" %}
|
||||
{% import_button content_type.model_class|validated_viewname:"import" %}
|
||||
{% endif %}
|
||||
{% if 'export' in action_buttons %}
|
||||
{% export_button content_type %}
|
||||
@@ -21,7 +21,7 @@
|
||||
<h1>{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-{% if filter_form %}9{% else %}12{% endif %}">
|
||||
{% with bulk_edit_url=content_type.model_class|url_name:"bulk_edit" bulk_delete_url=content_type.model_class|url_name:"bulk_delete" %}
|
||||
{% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
|
||||
{% if permissions.change or permissions.delete %}
|
||||
<form method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
|
||||
141
netbox/templates/virtualization/inc/vminterface.html
Normal file
141
netbox/templates/virtualization/inc/vminterface.html
Normal file
@@ -0,0 +1,141 @@
|
||||
{% load helpers %}
|
||||
<tr class="interface{% if not iface.enabled %} danger{% endif %}" id="interface_{{ iface.name }}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<a href="{{ iface.get_absolute_url }}">{{ iface }}</a>
|
||||
</td>
|
||||
|
||||
{# MAC address #}
|
||||
<td class="text-monospace">
|
||||
{{ iface.mac_address|default:"—" }}
|
||||
</td>
|
||||
|
||||
{# MTU #}
|
||||
<td>{{ iface.mtu|default:"—" }}</td>
|
||||
|
||||
{# 802.1Q mode #}
|
||||
<td>{{ iface.get_mode_display|default:"—" }}</td>
|
||||
|
||||
{# Description/tags #}
|
||||
<td>
|
||||
{% if iface.description %}
|
||||
{{ iface.description }}<br/>
|
||||
{% endif %}
|
||||
{% for tag in iface.tags.all %}
|
||||
{% tag tag %}
|
||||
{% empty %}
|
||||
{% if not iface.description %}—{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
|
||||
{# Buttons #}
|
||||
<td class="text-right text-nowrap noprint">
|
||||
{% if show_interface_graphs %}
|
||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ virtualmachine.name }} - {{ iface.name }}" data-url="{% url 'virtualization-api:vminterface-graphs' pk=iface.pk %}" title="Show graphs">
|
||||
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?vminterface={{ iface.pk }}&return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.change_interface %}
|
||||
<a href="{% url 'virtualization:vminterface_edit' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.delete_interface %}
|
||||
<a href="{% url 'virtualization:vminterface_delete' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% with ipaddresses=iface.ip_addresses.all %}
|
||||
{% if ipaddresses %}
|
||||
<tr class="ipaddresses">
|
||||
{# Placeholder #}
|
||||
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
||||
{# IP addresses table #}
|
||||
<td colspan="9" style="padding: 0">
|
||||
<table class="table table-condensed interface-ips">
|
||||
<thead>
|
||||
<tr class="text-muted">
|
||||
<th class="col-md-3">IP Address</th>
|
||||
<th class="col-md-2">Status/Role</th>
|
||||
<th class="col-md-3">VRF</th>
|
||||
<th class="col-md-3">Description</th>
|
||||
<th class="col-md-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for ip in iface.ip_addresses.all %}
|
||||
<tr>
|
||||
|
||||
{# IP address #}
|
||||
<td>
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
</td>
|
||||
|
||||
{# Primary/status/role #}
|
||||
<td>
|
||||
{% if virtualmachine.primary_ip4 == ip or virtualmachine.primary_ip6 == ip %}
|
||||
<span class="label label-success">Primary</span>
|
||||
{% endif %}
|
||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||
{% if ip.role %}
|
||||
<span class="label label-{{ ip.get_role_class }}">{{ ip.get_role_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# VRF #}
|
||||
<td>
|
||||
{% if ip.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}" title="{{ ip.vrf.rd }}">{{ ip.vrf.name }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Global</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Description #}
|
||||
<td>
|
||||
{% if ip.description %}
|
||||
{{ ip.description }}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Buttons #}
|
||||
<td class="text-right text-nowrap noprint">
|
||||
{% if perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user