Jeremy Stretch 7 лет назад
Родитель
Сommit
55c632ace7
4 измененных файлов с 130 добавлено и 473 удалено
  1. 103 442
      netbox/dcim/forms.py
  2. 2 4
      netbox/dcim/urls.py
  3. 20 27
      netbox/dcim/views.py
  4. 5 0
      netbox/templates/inc/nav_menu.html

+ 103 - 442
netbox/dcim/forms.py

@@ -4,6 +4,7 @@ from django import forms
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.postgres.forms.array import SimpleArrayField
+from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import Count, Q
 from mptt.forms import TreeNodeChoiceField
 from natsort import natsorted
@@ -1271,157 +1272,6 @@ class ConsolePortCreateForm(ComponentForm):
     tags = TagField(required=False)
 
 
-class ConsoleConnectionCSVForm(forms.ModelForm):
-    console_server = FlexibleModelChoiceField(
-        queryset=Device.objects.all(),
-        to_field_name='name',
-        help_text='Console server name or ID',
-        error_messages={
-            'invalid_choice': 'Console server not found',
-        }
-    )
-    connected_endpoint = forms.CharField(
-        help_text='Console server port'
-    )
-    device = FlexibleModelChoiceField(
-        queryset=Device.objects.all(),
-        to_field_name='name',
-        help_text='Device name or ID',
-        error_messages={
-            'invalid_choice': 'Device not found',
-        }
-    )
-    console_port = forms.CharField(
-        help_text='Console port name'
-    )
-    connection_status = CSVChoiceField(
-        choices=CONNECTION_STATUS_CHOICES,
-        help_text='Connection status'
-    )
-
-    class Meta:
-        model = ConsolePort
-        fields = ['console_server', 'connected_endpoint', 'device', 'console_port', 'connection_status']
-
-    def clean_console_port(self):
-
-        console_port_name = self.cleaned_data.get('console_port')
-        if not self.cleaned_data.get('device') or not console_port_name:
-            return None
-
-        try:
-            # Retrieve console port by name
-            consoleport = ConsolePort.objects.get(
-                device=self.cleaned_data['device'], name=console_port_name
-            )
-            # Check if the console port is already connected
-            if consoleport.connected_endpoint is not None:
-                raise forms.ValidationError("{} {} is already connected".format(
-                    self.cleaned_data['device'], console_port_name
-                ))
-        except ConsolePort.DoesNotExist:
-            raise forms.ValidationError("Invalid console port ({} {})".format(
-                self.cleaned_data['device'], console_port_name
-            ))
-
-        self.instance = consoleport
-        return consoleport
-
-    def clean_connected_endpoint(self):
-
-        consoleserverport_name = self.cleaned_data.get('connected_endpoint')
-        if not self.cleaned_data.get('console_server') or not consoleserverport_name:
-            return None
-
-        try:
-            # Retrieve console server port by name
-            consoleserverport = ConsoleServerPort.objects.get(
-                device=self.cleaned_data['console_server'], name=consoleserverport_name
-            )
-            # Check if the console server port is already connected
-            if ConsolePort.objects.filter(connected_endpoint=consoleserverport).count():
-                raise forms.ValidationError("{} {} is already connected".format(
-                    self.cleaned_data['console_server'], consoleserverport_name
-                ))
-        except ConsoleServerPort.DoesNotExist:
-            raise forms.ValidationError("Invalid console server port ({} {})".format(
-                self.cleaned_data['console_server'], consoleserverport_name
-            ))
-
-        return consoleserverport
-
-
-class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
-    site = forms.ModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        widget=forms.Select(
-            attrs={'filter-for': 'rack'}
-        )
-    )
-    rack = ChainedModelChoiceField(
-        queryset=Rack.objects.all(),
-        chains=(
-            ('site', 'site'),
-        ),
-        label='Rack',
-        required=False,
-        widget=APISelect(
-            api_url='/api/dcim/racks/?site_id={{site}}',
-            attrs={'filter-for': 'console_server', 'nullable': 'true'}
-        )
-    )
-    console_server = ChainedModelChoiceField(
-        queryset=Device.objects.all(),
-        chains=(
-            ('site', 'site'),
-            ('rack', 'rack'),
-        ),
-        label='Console Server',
-        required=False,
-        widget=APISelect(
-            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
-            display_field='display_name',
-            attrs={'filter-for': 'connected_endpoint'}
-        )
-    )
-    livesearch = forms.CharField(
-        required=False,
-        label='Console Server',
-        widget=Livesearch(
-            query_key='q',
-            query_url='dcim-api:device-list',
-            field_to_update='console_server',
-        )
-    )
-    connected_endpoint = ChainedModelChoiceField(
-        queryset=ConsoleServerPort.objects.all(),
-        chains=(
-            ('device', 'console_server'),
-        ),
-        label='Port',
-        widget=APISelect(
-            api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
-            disabled_indicator='cable',
-        )
-    )
-
-    class Meta:
-        model = ConsolePort
-        fields = ['site', 'rack', 'console_server', 'livesearch', 'connected_endpoint', 'connection_status']
-        labels = {
-            'connected_endpoint': 'Port',
-            'connection_status': 'Status',
-        }
-
-    def __init__(self, *args, **kwargs):
-
-        super(ConsolePortConnectionForm, self).__init__(*args, **kwargs)
-
-        if not self.instance.pk:
-            raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
-
-
 #
 # Console server ports
 #
@@ -1442,76 +1292,6 @@ class ConsoleServerPortCreateForm(ComponentForm):
     tags = TagField(required=False)
 
 
-class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
-    site = forms.ModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        widget=forms.Select(
-            attrs={'filter-for': 'rack'}
-        )
-    )
-    rack = ChainedModelChoiceField(
-        queryset=Rack.objects.all(),
-        chains=(
-            ('site', 'site'),
-        ),
-        label='Rack',
-        required=False,
-        widget=APISelect(
-            api_url='/api/dcim/racks/?site_id={{site}}',
-            attrs={'filter-for': 'device', 'nullable': 'true'}
-        )
-    )
-    device = ChainedModelChoiceField(
-        queryset=Device.objects.all(),
-        chains=(
-            ('site', 'site'),
-            ('rack', 'rack'),
-        ),
-        label='Device',
-        required=False,
-        widget=APISelect(
-            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
-            display_field='display_name',
-            attrs={'filter-for': 'port'}
-        )
-    )
-    livesearch = forms.CharField(
-        required=False,
-        label='Device',
-        widget=Livesearch(
-            query_key='q',
-            query_url='dcim-api:device-list',
-            field_to_update='device'
-        )
-    )
-    port = ChainedModelChoiceField(
-        queryset=ConsolePort.objects.all(),
-        chains=(
-            ('device', 'device'),
-        ),
-        label='Port',
-        widget=APISelect(
-            api_url='/api/dcim/console-ports/?device_id={{device}}',
-            disabled_indicator='cable'
-        )
-    )
-    connection_status = forms.BooleanField(
-        required=False,
-        initial=CONNECTION_STATUS_CONNECTED,
-        label='Status',
-        widget=forms.Select(
-            choices=CONNECTION_STATUS_CHOICES
-        )
-    )
-
-    class Meta:
-        fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
-        labels = {
-            'connection_status': 'Status',
-        }
-
-
 class ConsoleServerPortBulkRenameForm(BulkRenameForm):
     pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
 
@@ -1540,157 +1320,6 @@ class PowerPortCreateForm(ComponentForm):
     tags = TagField(required=False)
 
 
-class PowerConnectionCSVForm(forms.ModelForm):
-    pdu = FlexibleModelChoiceField(
-        queryset=Device.objects.all(),
-        to_field_name='name',
-        help_text='PDU name or ID',
-        error_messages={
-            'invalid_choice': 'PDU not found.',
-        }
-    )
-    connected_endpoint = forms.CharField(
-        help_text='Power outlet name'
-    )
-    device = FlexibleModelChoiceField(
-        queryset=Device.objects.all(),
-        to_field_name='name',
-        help_text='Device name or ID',
-        error_messages={
-            'invalid_choice': 'Device not found',
-        }
-    )
-    power_port = forms.CharField(
-        help_text='Power port name'
-    )
-    connection_status = CSVChoiceField(
-        choices=CONNECTION_STATUS_CHOICES,
-        help_text='Connection status'
-    )
-
-    class Meta:
-        model = PowerPort
-        fields = ['pdu', 'connected_endpoint', 'device', 'power_port', 'connection_status']
-
-    def clean_power_port(self):
-
-        power_port_name = self.cleaned_data.get('power_port')
-        if not self.cleaned_data.get('device') or not power_port_name:
-            return None
-
-        try:
-            # Retrieve power port by name
-            powerport = PowerPort.objects.get(
-                device=self.cleaned_data['device'], name=power_port_name
-            )
-            # Check if the power port is already connected
-            if powerport.connected_endpoint is not None:
-                raise forms.ValidationError("{} {} is already connected".format(
-                    self.cleaned_data['device'], power_port_name
-                ))
-        except PowerPort.DoesNotExist:
-            raise forms.ValidationError("Invalid power port ({} {})".format(
-                self.cleaned_data['device'], power_port_name
-            ))
-
-        self.instance = powerport
-        return powerport
-
-    def clean_connected_endpoint(self):
-
-        poweroutlet_name = self.cleaned_data.get('connected_endpoint')
-        if not self.cleaned_data.get('pdu') or not poweroutlet_name:
-            return None
-
-        try:
-            # Retrieve power outlet by name
-            poweroutlet = PowerOutlet.objects.get(
-                device=self.cleaned_data['pdu'], name=poweroutlet_name
-            )
-            # Check if the power outlet is already connected
-            if PowerPort.objects.filter(connected_endpoint=poweroutlet).count():
-                raise forms.ValidationError("{} {} is already connected".format(
-                    self.cleaned_data['pdu'], poweroutlet_name
-                ))
-        except PowerOutlet.DoesNotExist:
-            raise forms.ValidationError("Invalid power outlet ({} {})".format(
-                self.cleaned_data['pdu'], poweroutlet_name
-            ))
-
-        return poweroutlet
-
-
-class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
-    site = forms.ModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        widget=forms.Select(
-            attrs={'filter-for': 'rack'}
-        )
-    )
-    rack = ChainedModelChoiceField(
-        queryset=Rack.objects.all(),
-        chains=(
-            ('site', 'site'),
-        ),
-        label='Rack',
-        required=False,
-        widget=APISelect(
-            api_url='/api/dcim/racks/?site_id={{site}}',
-            attrs={'filter-for': 'pdu', 'nullable': 'true'}
-        )
-    )
-    pdu = ChainedModelChoiceField(
-        queryset=Device.objects.all(),
-        chains=(
-            ('site', 'site'),
-            ('rack', 'rack'),
-        ),
-        label='PDU',
-        required=False,
-        widget=APISelect(
-            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
-            display_field='display_name',
-            attrs={'filter-for': 'connected_endpoint'}
-        )
-    )
-    livesearch = forms.CharField(
-        required=False,
-        label='PDU',
-        widget=Livesearch(
-            query_key='q',
-            query_url='dcim-api:device-list',
-            field_to_update='pdu'
-        )
-    )
-    connected_endpoint = ChainedModelChoiceField(
-        queryset=PowerOutlet.objects.all(),
-        chains=(
-            ('device', 'pdu'),
-        ),
-        label='Outlet',
-        widget=APISelect(
-            api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
-            disabled_indicator='cable'
-        )
-    )
-
-    class Meta:
-        model = PowerPort
-        fields = ['site', 'rack', 'pdu', 'livesearch', 'connected_endpoint', 'connection_status']
-        labels = {
-            'connected_endpoint': 'Outlet',
-            'connection_status': 'Status',
-        }
-
-    def __init__(self, *args, **kwargs):
-
-        super(PowerPortConnectionForm, self).__init__(*args, **kwargs)
-
-        if not self.instance.pk:
-            raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
-
-
 #
 # Power outlets
 #
@@ -1711,76 +1340,6 @@ class PowerOutletCreateForm(ComponentForm):
     tags = TagField(required=False)
 
 
-class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
-    site = forms.ModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        widget=forms.Select(
-            attrs={'filter-for': 'rack'}
-        )
-    )
-    rack = ChainedModelChoiceField(
-        queryset=Rack.objects.all(),
-        chains=(
-            ('site', 'site'),
-        ),
-        label='Rack',
-        required=False,
-        widget=APISelect(
-            api_url='/api/dcim/racks/?site_id={{site}}',
-            attrs={'filter-for': 'device', 'nullable': 'true'}
-        )
-    )
-    device = ChainedModelChoiceField(
-        queryset=Device.objects.all(),
-        chains=(
-            ('site', 'site'),
-            ('rack', 'rack'),
-        ),
-        label='Device',
-        required=False,
-        widget=APISelect(
-            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
-            display_field='display_name',
-            attrs={'filter-for': 'port'}
-        )
-    )
-    livesearch = forms.CharField(
-        required=False,
-        label='Device',
-        widget=Livesearch(
-            query_key='q',
-            query_url='dcim-api:device-list',
-            field_to_update='device'
-        )
-    )
-    port = ChainedModelChoiceField(
-        queryset=PowerPort.objects.all(),
-        chains=(
-            ('device', 'device'),
-        ),
-        label='Port',
-        widget=APISelect(
-            api_url='/api/dcim/power-ports/?device_id={{device}}',
-            disabled_indicator='cable'
-        )
-    )
-    connection_status = forms.BooleanField(
-        required=False,
-        initial=CONNECTION_STATUS_CONNECTED,
-        label='Status',
-        widget=forms.Select(
-            choices=CONNECTION_STATUS_CHOICES
-        )
-    )
-
-    class Meta:
-        fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
-        labels = {
-            'connection_status': 'Status',
-        }
-
-
 class PowerOutletBulkRenameForm(BulkRenameForm):
     pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
 
@@ -2199,6 +1758,108 @@ class CableForm(BootstrapMixin, forms.ModelForm):
         fields = ('type', 'status', 'label', 'color', 'length', 'length_unit')
 
 
+class CableCSVForm(forms.ModelForm):
+
+    # Termination A
+    side_a_device = FlexibleModelChoiceField(
+        queryset=Device.objects.all(),
+        to_field_name='name',
+        help_text='Console server name or ID',
+        error_messages={
+            'invalid_choice': 'Side A device not found',
+        }
+    )
+    side_a_type = forms.ModelChoiceField(
+        queryset=ContentType.objects.all(),
+        limit_choices_to={'model__in': CABLE_TERMINATION_TYPES},
+        to_field_name='model'
+    )
+    side_a_name = forms.CharField()
+
+    # Termination B
+    side_b_device = FlexibleModelChoiceField(
+        queryset=Device.objects.all(),
+        to_field_name='name',
+        help_text='Console server name or ID',
+        error_messages={
+            'invalid_choice': 'Side B device not found',
+        }
+    )
+    side_b_type = forms.ModelChoiceField(
+        queryset=ContentType.objects.all(),
+        limit_choices_to={'model__in': CABLE_TERMINATION_TYPES},
+        to_field_name='model'
+    )
+    side_b_name = forms.CharField()
+
+    # Cable attributes
+    status = CSVChoiceField(
+        choices=CONNECTION_STATUS_CHOICES,
+        required=False,
+        help_text='Connection status'
+    )
+
+    class Meta:
+        model = Cable
+        fields = [
+            'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'status',
+            'label',
+        ]
+
+    # TODO: Merge the clean() methods for either end
+    def clean_side_a_name(self):
+
+        device = self.cleaned_data.get('side_a_device')
+        content_type = self.cleaned_data.get('side_a_type')
+        name = self.cleaned_data.get('side_a_name')
+        if not device or not content_type or not name:
+            return None
+
+        model = content_type.model_class()
+        try:
+            termination_object = model.objects.get(
+                device=device,
+                name=name
+            )
+            if termination_object.cable is not None:
+                raise forms.ValidationError(
+                    "Side A: {} {} is already connected".format(device, termination_object)
+                )
+        except ObjectDoesNotExist:
+            raise forms.ValidationError(
+                "A side termination not found: {} {}".format(device, name)
+            )
+
+        self.instance.termination_a = termination_object
+        return termination_object
+
+    def clean_side_b_name(self):
+
+        device = self.cleaned_data.get('side_b_device')
+        content_type = self.cleaned_data.get('side_b_type')
+        name = self.cleaned_data.get('side_b_name')
+        if not device or not content_type or not name:
+            return None
+
+        model = content_type.model_class()
+        try:
+            termination_object = model.objects.get(
+                device=device,
+                name=name
+            )
+            if termination_object.cable is not None:
+                raise forms.ValidationError(
+                    "Side B: {} {} is already connected".format(device, termination_object)
+                )
+        except ObjectDoesNotExist:
+            raise forms.ValidationError(
+                "B side termination not found: {} {}".format(device, name)
+            )
+
+        self.instance.termination_b = termination_object
+        return termination_object
+
+
 class CableFilterForm(BootstrapMixin, forms.Form):
     model = Cable
     q = forms.CharField(required=False, label='Search')

+ 2 - 4
netbox/dcim/urls.py

@@ -251,17 +251,15 @@ urlpatterns = [
 
     # Cables
     url(r'^cables/$', views.CableListView.as_view(), name='cable_list'),
+    url(r'^cables/import/$', views.CableBulkImportView.as_view(), name='cable_import'),
     url(r'^cables/(?P<pk>\d+)/$', views.CableView.as_view(), name='cable'),
     url(r'^cables/(?P<pk>\d+)/edit/$', views.CableEditView.as_view(), name='cable_edit'),
     url(r'^cables/(?P<pk>\d+)/delete/$', views.CableDeleteView.as_view(), name='cable_delete'),
 
-    # Console/power/interface connections
+    # Console/power/interface connections (read-only)
     url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
-    # url(r'^console-connections/import/$', views.ConsoleConnectionsBulkImportView.as_view(), name='console_connections_import'),
     url(r'^power-connections/$', views.PowerConnectionsListView.as_view(), name='power_connections_list'),
-    # url(r'^power-connections/import/$', views.PowerConnectionsBulkImportView.as_view(), name='power_connections_import'),
     url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
-    # url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
 
     # Virtual chassis
     url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),

+ 20 - 27
netbox/dcim/views.py

@@ -1121,13 +1121,6 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     table = tables.ConsolePortTable
 
 
-class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
-    permission_required = 'dcim.change_consoleport'
-    model_form = forms.ConsoleConnectionCSVForm
-    table = tables.ConsoleConnectionTable
-    default_return_url = 'dcim:console_connections_list'
-
-
 #
 # Console server ports
 #
@@ -1212,13 +1205,6 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     table = tables.PowerPortTable
 
 
-class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
-    permission_required = 'dcim.change_powerport'
-    model_form = forms.PowerConnectionCSVForm
-    table = tables.PowerConnectionTable
-    default_return_url = 'dcim:power_connections_list'
-
-
 #
 # Power outlets
 #
@@ -1645,6 +1631,21 @@ class CableView(View):
         })
 
 
+class CableTraceView(View):
+    """
+    Trace a cable path beginning from the given termination.
+    """
+
+    def get(self, request, model, pk):
+
+        obj = get_object_or_404(model, pk=pk)
+
+        return render(request, 'dcim/cable_trace.html', {
+            'obj': obj,
+            'trace': obj.trace(),
+        })
+
+
 class CableCreateView(PermissionRequiredMixin, ObjectEditView):
     permission_required = 'dcim.add_cable'
     model = Cable
@@ -1674,19 +1675,11 @@ class CableDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     default_return_url = 'dcim:cable_list'
 
 
-class CableTraceView(View):
-    """
-    Trace a cable path beginning from the given termination.
-    """
-
-    def get(self, request, model, pk):
-
-        obj = get_object_or_404(model, pk=pk)
-
-        return render(request, 'dcim/cable_trace.html', {
-            'obj': obj,
-            'trace': obj.trace(),
-        })
+class CableBulkImportView(PermissionRequiredMixin, BulkImportView):
+    permission_required = 'dcim.add_cable'
+    model_form = forms.CableCSVForm
+    table = tables.CableTable
+    default_return_url = 'dcim:cable_list'
 
 
 #

+ 5 - 0
netbox/templates/inc/nav_menu.html

@@ -180,6 +180,11 @@
                         <li class="divider"></li>
                         <li class="dropdown-header">Connections</li>
                         <li>
+                            {% if perms.dcim.add_cable %}
+                                <div class="buttons pull-right">
+                                    <a href="{% url 'dcim:cable_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
+                                </div>
+                            {% endif %}
                             <a href="{% url 'dcim:cable_list' %}">Cables</a>
                         </li>
                         <li>