diff --git a/CHANGELOG.md b/CHANGELOG.md index cf51633f830..b03164bfb72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +v2.5.6 (2019-02-13) + +## Enhancements + +* [#2758](https://github.com/digitalocean/netbox/issues/2758) - Add cable trace button to pass-through ports +* [#2839](https://github.com/digitalocean/netbox/issues/2839) - Add "110 punch" type for pass-through ports +* [#2854](https://github.com/digitalocean/netbox/issues/2854) - Enable bulk editing of pass-through ports +* [#2866](https://github.com/digitalocean/netbox/issues/2866) - Add cellular interface types (GSM/CDMA/LTE) + +## Bug Fixes + +* [#2841](https://github.com/digitalocean/netbox/issues/2841) - Fix filtering by VRF for prefix and IP address lists +* [#2844](https://github.com/digitalocean/netbox/issues/2844) - Correct display of far cable end for pass-through ports +* [#2845](https://github.com/digitalocean/netbox/issues/2845) - Enable filtering of rack unit list by unit ID +* [#2856](https://github.com/digitalocean/netbox/issues/2856) - Fix navigation links between LAG interfaces and their members on device view +* [#2857](https://github.com/digitalocean/netbox/issues/2857) - Add `display_name` to DeviceType API serializer; fix DeviceType list for bulk device edit +* [#2862](https://github.com/digitalocean/netbox/issues/2862) - Follow return URL when connecting a cable +* [#2864](https://github.com/digitalocean/netbox/issues/2864) - Correct display of VRF name when no RD is assigned +* [#2877](https://github.com/digitalocean/netbox/issues/2877) - Fixed device role label display on light background color +* [#2880](https://github.com/digitalocean/netbox/issues/2880) - Sanitize user password if an exception is raised during login + +--- + v2.5.5 (2019-01-31) ## Enhancements diff --git a/docs/core-functionality/devices.md b/docs/core-functionality/devices.md index e51bf541c29..176a7067617 100644 --- a/docs/core-functionality/devices.md +++ b/docs/core-functionality/devices.md @@ -13,6 +13,10 @@ Some devices house child devices which share physical resources, like space and !!! note This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. + For that application you should create a single Device for the chassis, and add Interfaces directly to it. Interfaces can be created in bulk using range patterns, e.g. "Gi1/[1-24]". + + Add Inventory Items if you want to record the line cards themselves as separate entities. There is no explicit relationship between each interface and its line card, but it may be implied by the naming (e.g. interfaces "Gi1/x" are on line card 1) + ## Manufacturers Each device type must be assigned to a manufacturer. The model number of a device type must be unique to its manufacturer. @@ -93,6 +97,10 @@ Pass-through ports can also be used to model "bump in the wire" devices, such as Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations, but they are included in the "Non-Racked Devices" list within the rack view. +Child devices are first-class Devices in their own right: that is, fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and interfaces. You cannot create a LAG between interfaces in different child devices. + +Therefore, Device bays are **not** suitable for modeling chassis-based switches and routers. These should instead be modeled as a single Device, with the line cards as Inventory Items. + ## Device Roles Devices can be organized by functional roles. These roles are fully customizable. For example, you might create roles for core switches, distribution switches, and access switches. @@ -111,7 +119,7 @@ The assignment of platforms to devices is an optional feature, and may be disreg # Inventory Items -Inventory items represent hardware components installed within a device, such as a power supply or CPU. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer. +Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer. --- diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 4d747859545..e53259e942f 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -100,7 +100,7 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer): class Meta: model = DeviceType - fields = ['id', 'url', 'manufacturer', 'model', 'slug'] + fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name'] class NestedRearPortTemplateSerializer(WritableNestedSerializer): diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 765ed83dd08..c17400a359b 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -180,8 +180,8 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): class Meta: model = DeviceType fields = [ - 'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count', + 'id', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth', + 'subdevice_role', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count', ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index d01358447dc..4e14d816379 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -159,6 +159,11 @@ class RackViewSet(CustomFieldModelViewSet): exclude_pk = None elevation = rack.get_rack_units(face, exclude_pk) + # Enable filtering rack units by ID + q = request.GET.get('q', None) + if q: + elevation = [u for u in elevation if q in str(u['id'])] + page = self.paginate_queryset(elevation) if page is not None: rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request}) diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 27f8b6f7980..22d4468fc4a 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -91,6 +91,10 @@ IFACE_FF_80211G = 2610 IFACE_FF_80211N = 2620 IFACE_FF_80211AC = 2630 IFACE_FF_80211AD = 2640 +# Cellular +IFACE_FF_GSM = 2810 +IFACE_FF_CDMA = 2820 +IFACE_FF_LTE = 2830 # SONET IFACE_FF_SONET_OC3 = 6100 IFACE_FF_SONET_OC12 = 6200 @@ -174,6 +178,14 @@ IFACE_FF_CHOICES = [ [IFACE_FF_80211AD, 'IEEE 802.11ad'], ] ], + [ + 'Cellular', + [ + [IFACE_FF_GSM, 'GSM'], + [IFACE_FF_CDMA, 'CDMA'], + [IFACE_FF_LTE, 'LTE'], + ] + ], [ 'SONET', [ @@ -255,6 +267,7 @@ IFACE_MODE_CHOICES = [ # Pass-through port types PORT_TYPE_8P8C = 1000 +PORT_TYPE_110_PUNCH = 1100 PORT_TYPE_ST = 2000 PORT_TYPE_SC = 2100 PORT_TYPE_FC = 2200 @@ -267,6 +280,7 @@ PORT_TYPE_CHOICES = [ 'Copper', [ [PORT_TYPE_8P8C, '8P8C'], + [PORT_TYPE_110_PUNCH, '110 Punch'], ], ], [ diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 4d02506fec3..bf774dfcbd7 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1599,7 +1599,8 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF required=False, label='Type', widget=APISelect( - api_url="/api/dcim/device-types/" + api_url="/api/dcim/device-types/", + display_field='display_name' ) ) device_role = forms.ModelChoiceField( @@ -2359,6 +2360,27 @@ class FrontPortCreateForm(ComponentForm): } +class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Interface.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PORT_TYPE_CHOICES), + required=False, + widget=StaticSelect2() + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'description', + ] + + class FrontPortBulkRenameForm(BulkRenameForm): pk = forms.ModelMultipleChoiceField( queryset=FrontPort.objects.all(), @@ -2412,6 +2434,27 @@ class RearPortCreateForm(ComponentForm): ) +class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Interface.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PORT_TYPE_CHOICES), + required=False, + widget=StaticSelect2() + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'description', + ] + + class RearPortBulkRenameForm(BulkRenameForm): pk = forms.ModelMultipleChoiceField( queryset=RearPort.objects.all(), diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 8c6ed96502e..524689ecb75 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -169,9 +169,9 @@ class CableTermination(models.Model): def get_cable_peer(self): if self.cable is None: return None - if self._cabled_as_a: + if self._cabled_as_a.exists(): return self.cable.termination_b - if self._cabled_as_b: + if self._cabled_as_b.exists(): return self.cable.termination_a @@ -980,7 +980,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): }) @property - def full_name(self): + def display_name(self): return '{} {}'.format(self.manufacturer.name, self.model) @property diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 0ab1d594c42..3cbd9378dd1 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -136,7 +136,8 @@ PLATFORM_ACTIONS = """ """ DEVICE_ROLE = """ - +{% load helpers %} + """ STATUS_LABEL = """ @@ -517,7 +518,7 @@ class DeviceTable(BaseTable): device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') device_type = tables.LinkColumn( 'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', - text=lambda record: record.device_type.full_name + text=lambda record: record.device_type.display_name ) class Meta(BaseTable.Meta): diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 980c57e865b..aa57d4790af 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -855,7 +855,7 @@ class DeviceTypeTest(APITestCase): self.assertEqual( sorted(response.data['results'][0]), - ['id', 'manufacturer', 'model', 'slug', 'url'] + ['display_name', 'id', 'manufacturer', 'model', 'slug', 'url'] ) def test_create_devicetype(self): diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index dc1fbbf39c4..21d620af15e 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -215,6 +215,7 @@ urlpatterns = [ # Front ports # url(r'^devices/front-ports/add/$', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), url(r'^devices/(?P\d+)/front-ports/add/$', views.FrontPortCreateView.as_view(), name='frontport_add'), + url(r'^devices/(?P\d+)/front-ports/edit/$', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'), url(r'^devices/(?P\d+)/front-ports/delete/$', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'), url(r'^front-ports/(?P\d+)/connect/$', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), url(r'^front-ports/(?P\d+)/edit/$', views.FrontPortEditView.as_view(), name='frontport_edit'), @@ -226,6 +227,7 @@ urlpatterns = [ # Rear ports # url(r'^devices/rear-ports/add/$', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), url(r'^devices/(?P\d+)/rear-ports/add/$', views.RearPortCreateView.as_view(), name='rearport_add'), + url(r'^devices/(?P\d+)/rear-ports/edit/$', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'), url(r'^devices/(?P\d+)/rear-ports/delete/$', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'), url(r'^rear-ports/(?P\d+)/connect/$', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), url(r'^rear-ports/(?P\d+)/edit/$', views.RearPortEditView.as_view(), name='rearport_edit'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 350ed15420c..9aa4b235485 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1360,6 +1360,14 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): model = FrontPort +class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_frontport' + queryset = FrontPort.objects.all() + parent_model = Device + table = tables.FrontPortTable + form = forms.FrontPortBulkEditForm + + class FrontPortBulkRenameView(PermissionRequiredMixin, BulkRenameView): permission_required = 'dcim.change_frontport' queryset = FrontPort.objects.all() @@ -1404,6 +1412,14 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): model = RearPort +class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_rearport' + queryset = RearPort.objects.all() + parent_model = Device + table = tables.RearPortTable + form = forms.RearPortBulkEditForm + + class RearPortBulkRenameView(PermissionRequiredMixin, BulkRenameView): permission_required = 'dcim.change_rearport' queryset = RearPort.objects.all() diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 0864223d135..d0e25f580c4 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -531,7 +531,7 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): null_label='-- Global --', widget=APISelectMultiple( api_url="/api/ipam/vrfs/", - value_field="slug", + value_field="rd", null_option=True, ) ) @@ -980,7 +980,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): null_label='-- Global --', widget=APISelectMultiple( api_url="/api/ipam/vrfs/", - value_field="slug", + value_field="rd", null_option=True, ) ) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 7c879595fd7..181852ad33a 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -81,9 +81,9 @@ class VRF(ChangeLoggedModel, CustomFieldModel): @property def display_name(self): - if self.name and self.rd: + if self.rd: return "{} ({})".format(self.name, self.rd) - return None + return self.name class RIR(ChangeLoggedModel): diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index f2692776afb..a0f9b267fb0 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ except ImportError: ) -VERSION = '2.5.5' +VERSION = '2.5.6' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/netbox/templates/dcim/cable_connect.html b/netbox/templates/dcim/cable_connect.html index 965a94ac7cb..cad396966b2 100644 --- a/netbox/templates/dcim/cable_connect.html +++ b/netbox/templates/dcim/cable_connect.html @@ -4,7 +4,7 @@ {% load form_helpers %} {% block content %} -
+ {% csrf_token %} {% for field in form.hidden_fields %} {{ field }} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 8ee50c4234f..716752c57b7 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -163,7 +163,7 @@ Device Type - {{ device.device_type.full_name }} ({{ device.device_type.u_height }}U) + {{ device.device_type.display_name }} ({{ device.device_type.u_height }}U) @@ -416,7 +416,7 @@ {% endif %} - {{ rd.device_type.full_name }} + {{ rd.device_type.display_name }} {% endfor %} @@ -698,6 +698,9 @@ + @@ -752,6 +755,9 @@ + diff --git a/netbox/templates/dcim/device_inventory.html b/netbox/templates/dcim/device_inventory.html index d9f8c549506..106e5a6e256 100644 --- a/netbox/templates/dcim/device_inventory.html +++ b/netbox/templates/dcim/device_inventory.html @@ -13,7 +13,7 @@ - + diff --git a/netbox/templates/dcim/inc/devicebay.html b/netbox/templates/dcim/inc/devicebay.html index 656116c89e1..dc2977855ec 100644 --- a/netbox/templates/dcim/inc/devicebay.html +++ b/netbox/templates/dcim/inc/devicebay.html @@ -15,7 +15,7 @@ {{ devicebay.installed_device }} {% else %} diff --git a/netbox/templates/dcim/inc/frontport.html b/netbox/templates/dcim/inc/frontport.html index 3ac87e09086..6ec5857ac30 100644 --- a/netbox/templates/dcim/inc/frontport.html +++ b/netbox/templates/dcim/inc/frontport.html @@ -27,9 +27,24 @@ {% if frontport.cable %} {% with far_end=frontport.get_cable_peer %} - + {% endwith %} {% else %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 3d805720857..7c1e9f26779 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -1,5 +1,5 @@ {% load helpers %} - + {# Checkbox #} {% if perms.dcim.change_interface or perms.dcim.delete_interface %} diff --git a/netbox/templates/dcim/inc/rack_elevation.html b/netbox/templates/dcim/inc/rack_elevation.html index e7beeb9ba3a..ced6eb92902 100644 --- a/netbox/templates/dcim/inc/rack_elevation.html +++ b/netbox/templates/dcim/inc/rack_elevation.html @@ -26,7 +26,7 @@
  • {% ifequal u.device.face face_id %} + data-content="{{ u.device.device_role }}
    {{ u.device.device_type.display_name }} ({{ u.device.device_type.u_height }}U){% if u.device.asset_tag %}
    {{ u.device.asset_tag }}{% endif %}{% if u.device.serial %}
    {{ u.device.serial }}{% endif %}"> {{ u.device }} {% if u.device.devicebay_count %} ({{ u.device.get_children.count }}/{{ u.device.devicebay_count }}) diff --git a/netbox/templates/dcim/inc/rearport.html b/netbox/templates/dcim/inc/rearport.html index fc6b5c1df15..e16cc82c5f1 100644 --- a/netbox/templates/dcim/inc/rearport.html +++ b/netbox/templates/dcim/inc/rearport.html @@ -26,9 +26,24 @@ {% if rearport.cable %}
  • {% with far_end=rearport.get_cable_peer %} - + {% endwith %} {% else %} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 1d92aac2240..fe9e2ab7c0c 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -208,7 +208,7 @@ {{ device }} - +
    Model{{ device.device_type.full_name }}{{ device.device_type.display_name }}
    Serial Number - {{ devicebay.installed_device.device_type.full_name }} + {{ devicebay.installed_device.device_type.display_name }} {{ frontport.cable }} + + + {{ far_end.parent }} + {% if far_end.parent.provider %} + + + {{ far_end.parent.provider }} + {{ far_end.parent }} + + {% else %} + + {{ far_end.parent }} + + {% endif %} + {{ far_end }}
    {{ rearport.cable }} + + + {{ far_end.parent }} + {% if far_end.parent.provider %} + + + {{ far_end.parent.provider }} + {{ far_end.parent }} + + {% else %} + + {{ far_end.parent }} + + {% endif %} + {{ far_end }} {{ device.device_role }}{{ device.device_type.full_name }}{{ device.device_type.display_name }} {% if device.parent_bay %} {{ device.parent_bay }} diff --git a/netbox/users/views.py b/netbox/users/views.py index 171d444b98d..6ec98493655 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.http import is_safe_url +from django.views.decorators.debug import sensitive_post_parameters from django.views.generic import View from secrets.forms import UserKeyForm @@ -23,6 +24,10 @@ from .models import Token class LoginView(View): template_name = 'login.html' + @method_decorator(sensitive_post_parameters('password')) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + def get(self, request): form = LoginForm(request)