Merge pull request #2886 from digitalocean/develop

Release v2.5.6
This commit is contained in:
Jeremy Stretch 2019-02-13 17:11:26 -05:00 committed by GitHub
commit 77954a3796
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 179 additions and 26 deletions

View File

@ -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

View File

@ -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.
---

View File

@ -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):

View File

@ -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',
]

View File

@ -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})

View File

@ -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'],
],
],
[

View File

@ -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(),

View File

@ -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

View File

@ -136,7 +136,8 @@ PLATFORM_ACTIONS = """
"""
DEVICE_ROLE = """
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
{% load helpers %}
<label class="label" style="color: {{ record.device_role.color|fgcolor }}; background-color: #{{ record.device_role.color }}">{{ value }}</label>
"""
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):

View File

@ -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):

View File

@ -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<pk>\d+)/front-ports/add/$', views.FrontPortCreateView.as_view(), name='frontport_add'),
url(r'^devices/(?P<pk>\d+)/front-ports/edit/$', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/front-ports/delete/$', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
url(r'^front-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
url(r'^front-ports/(?P<pk>\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<pk>\d+)/rear-ports/add/$', views.RearPortCreateView.as_view(), name='rearport_add'),
url(r'^devices/(?P<pk>\d+)/rear-ports/edit/$', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/rear-ports/delete/$', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
url(r'^rear-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
url(r'^rear-ports/(?P<pk>\d+)/edit/$', views.RearPortEditView.as_view(), name='rearport_edit'),

View File

@ -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()

View File

@ -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,
)
)

View File

@ -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):

View File

@ -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__)))

View File

@ -4,7 +4,7 @@
{% load form_helpers %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
<form method="post" class="form form-horizontal">
{% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}

View File

@ -163,7 +163,7 @@
<tr>
<td>Device Type</td>
<td>
<span><a href="{% url 'dcim:devicetype' pk=device.device_type.pk %}">{{ device.device_type.full_name }}</a> ({{ device.device_type.u_height }}U)</span>
<span><a href="{% url 'dcim:devicetype' pk=device.device_type.pk %}">{{ device.device_type.display_name }}</a> ({{ device.device_type.u_height }}U)</span>
</td>
</tr>
<tr>
@ -416,7 +416,7 @@
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
<td>{{ rd.device_type.full_name }}</td>
<td>{{ rd.device_type.display_name }}</td>
</tr>
{% endfor %}
</table>
@ -698,6 +698,9 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_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:frontport_bulk_edit' pk=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:frontport_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>
@ -752,6 +755,9 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_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:rearport_bulk_edit' pk=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:rearport_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>

View File

@ -13,7 +13,7 @@
<table class="table table-hover panel-body attr-table">
<tr>
<td>Model</td>
<td>{{ device.device_type.full_name }}</td>
<td>{{ device.device_type.display_name }}</td>
</tr>
<tr>
<td>Serial Number</td>

View File

@ -15,7 +15,7 @@
<a href="{% url 'dcim:device' pk=devicebay.installed_device.pk %}">{{ devicebay.installed_device }}</a>
</td>
<td>
<span>{{ devicebay.installed_device.device_type.full_name }}</span>
<span>{{ devicebay.installed_device.device_type.display_name }}</span>
</td>
{% else %}
<td></td>

View File

@ -27,9 +27,24 @@
{% if frontport.cable %}
<td>
<a href="{{ frontport.cable.get_absolute_url }}">{{ frontport.cable }}</a>
<a href="{% url 'dcim:frontport_trace' pk=frontport.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
{% with far_end=frontport.get_cable_peer %}
<td><a href="{{ far_end.parent.get_absolute_url }}">{{ far_end.parent }}</a></td>
<td>
{% if far_end.parent.provider %}
<i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent.provider }}
{{ far_end.parent }}
</a>
{% else %}
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent }}
</a>
{% endif %}
</td>
<td>{{ far_end }}</td>
{% endwith %}
{% else %}

View File

@ -1,5 +1,5 @@
{% load helpers %}
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable.status %} success{% elif iface.cable %} info{% elif iface.is_virtual %} warning{% endif %}" id="iface_{{ iface.name }}">
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable.status %} success{% elif iface.cable %} info{% elif iface.is_virtual %} warning{% endif %}" id="interface_{{ iface.name }}">
{# Checkbox #}
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}

View File

@ -26,7 +26,7 @@
<li class="occupied h{{ u.device.device_type.u_height }}u"{% ifequal u.device.face face_id %} style="background-color: #{{ u.device.device_role.color }}"{% endifequal %}>
{% ifequal u.device.face face_id %}
<a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.full_name }} ({{ u.device.device_type.u_height }}U){% if u.device.asset_tag %}<br />{{ u.device.asset_tag }}{% endif %}{% if u.device.serial %}<br />{{ u.device.serial }}{% endif %}">
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.display_name }} ({{ u.device.device_type.u_height }}U){% if u.device.asset_tag %}<br />{{ u.device.asset_tag }}{% endif %}{% if u.device.serial %}<br />{{ u.device.serial }}{% endif %}">
{{ u.device }}
{% if u.device.devicebay_count %}
({{ u.device.get_children.count }}/{{ u.device.devicebay_count }})

View File

@ -26,9 +26,24 @@
{% if rearport.cable %}
<td>
<a href="{{ rearport.cable.get_absolute_url }}">{{ rearport.cable }}</a>
<a href="{% url 'dcim:rearport_trace' pk=rearport.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
{% with far_end=rearport.get_cable_peer %}
<td><a href="{{ far_end.parent.get_absolute_url }}">{{ far_end.parent }}</a></td>
<td>
{% if far_end.parent.provider %}
<i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent.provider }}
{{ far_end.parent }}
</a>
{% else %}
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent }}
</a>
{% endif %}
</td>
<td>{{ far_end }}</td>
{% endwith %}
{% else %}

View File

@ -208,7 +208,7 @@
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
</td>
<td>{{ device.device_role }}</td>
<td>{{ device.device_type.full_name }}</td>
<td>{{ device.device_type.display_name }}</td>
<td>
{% if device.parent_bay %}
<a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay }}</a>

View File

@ -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)