diff --git a/CHANGELOG.md b/CHANGELOG.md index cf51633f8..b03164bfb 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 e51bf541c..176a70676 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 4d7478595..e53259e94 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 765ed83dd..c17400a35 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 d01358447..4e14d8163 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 27f8b6f79..22d4468fc 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 4d02506fe..bf774dfcb 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 8c6ed9650..524689ecb 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 0ab1d594c..3cbd9378d 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 980c57e86..aa57d4790 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 dc1fbbf39..21d620af1 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 350ed1542..9aa4b2354 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 0864223d1..d0e25f580 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 7c879595f..181852ad3 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 f2692776a..a0f9b267f 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 965a94ac7..cad396966 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 8ee50c423..716752c57 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 d9f8c5495..106e5a6e2 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 656116c89..dc2977855 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 3ac87e090..6ec5857ac 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 3d8057208..7c1e9f267 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 e7beeb9ba..ced6eb929 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 fc6b5c1df..e16cc82c5 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 1d92aac22..fe9e2ab7c 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 171d444b9..6ec984936 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)