Compare commits

..

35 Commits

Author SHA1 Message Date
Jeremy Stretch
77954a3796 Merge pull request #2886 from digitalocean/develop
Release v2.5.6
2019-02-13 17:11:26 -05:00
Jeremy Stretch
8152dc4b04 Release v2.5.6 2019-02-13 17:07:15 -05:00
Jeremy Stretch
109b233e14 Closes #2758: Add cable trace button to pass-through ports 2019-02-13 17:05:02 -05:00
Jeremy Stretch
cc3b26998b Fixes #2880: Sanitize user password if an exception is raised during login 2019-02-13 11:34:16 -05:00
Jeremy Stretch
95dea1faaa Closes #2866: Add cellular interface types (GSM/CDMA/LTE) 2019-02-13 10:45:42 -05:00
Jeremy Stretch
57fecdbf17 Closes #2851: Include circuit provider in pass-through port connection details 2019-02-13 10:26:54 -05:00
Jeremy Stretch
3b4bcc881f Fix broken test 2019-02-13 10:09:33 -05:00
Jeremy Stretch
dfa4dfa4a4 Fixes #2877: Fixed device role label display on light background color 2019-02-13 10:06:56 -05:00
Jeremy Stretch
5da9d6b46b Closes #2839: Add "110 punch" type for pass-through ports 2019-02-08 09:57:04 -05:00
Jeremy Stretch
100809f11a Closes #2854: Enable bulk editing of pass-through ports 2019-02-08 09:31:10 -05:00
Jeremy Stretch
5256077a3c Fixes #2862: Follow return URL when connecting a cable 2019-02-08 09:10:31 -05:00
Jeremy Stretch
375e66047d Fixes #2864: Correct display of VRF name when no RD is assigned 2019-02-08 09:04:43 -05:00
Jeremy Stretch
42d1d6e1b0 Fixes #2841: Fix filtering by VRF for prefix and IP address lists 2019-02-06 10:48:14 -05:00
Jeremy Stretch
ca51fab4d8 Fixes #2845: Enable filtering of rack unit list by unit ID 2019-02-06 10:44:05 -05:00
Jeremy Stretch
73c983516d Fixes #2856: Fix navigation links between LAG interfaces and their members on device view 2019-02-06 10:28:25 -05:00
Jeremy Stretch
f733d5a4da Fixes #2857: Add display_name to DeviceType API serializer; fix DeviceType list for bulk device edit 2019-02-06 10:23:30 -05:00
Jeremy Stretch
3d2948daf3 Merge pull request #2793 from candlerb/candlerb/doc-inventory
Clarify how chassis-based switches/routers are supposed to be modelled
2019-02-06 10:02:05 -05:00
Jeremy Stretch
69a5d3644a Closes #2844: Correct display of far cable end for pass-through ports 2019-02-01 09:12:48 -05:00
Jeremy Stretch
2f1018c742 Post-release version bump 2019-01-31 16:12:00 -05:00
Jeremy Stretch
d5fc37282f Merge pull request #2838 from digitalocean/develop
Release v2.5.5
2019-01-31 16:10:32 -05:00
Jeremy Stretch
525ed359cd Release v2.5.5 2019-01-31 16:04:14 -05:00
John Anderson
fe00db62d6 fixes #2837 - select2 nullable filter fields add multiple null_option elements when paging 2019-01-31 13:56:36 -05:00
Jeremy Stretch
bcfa760cf9 Closes #2805: Allow null route distinguisher for VRFs 2019-01-31 13:47:24 -05:00
John Anderson
613e8f05c2 fixes #2835 - certain model filters did not support the q query param 2019-01-31 13:36:30 -05:00
Jeremy Stretch
59f8f0c7ea Closes #2825: Include directly connected device for front/rear ports 2019-01-31 12:21:43 -05:00
Jeremy Stretch
ae0c8deec2 Closes #2809: Remove VRF child prefixes table; link to main prefixes view 2019-01-31 10:06:08 -05:00
Jeremy Stretch
b508415983 Fixes #2833: Fix form widget for front port template creation 2019-01-31 09:19:53 -05:00
Jeremy Stretch
a98d014763 Post-release version bump 2019-01-31 09:06:49 -05:00
John Anderson
51e5e49d3b Merge pull request #2832 from cimnine/patch-1
Updated link to netbox-docker, again
2019-01-31 03:06:29 -05:00
Christian Mäder
0eced489da Updated link to netbox-docker, again
After some feedback, that `netbox-community/docker` is not an ideal name, I've renamed the repo back to `netbox-docker`. Hence one more PR to update that link.
2019-01-31 09:02:12 +01:00
TheTrafficNetwork
5138d12942 Typo (#2822) 2019-01-30 14:20:40 -05:00
Jeremy Stretch
fe30276db2 Merge pull request #2828 from cimnine/patch-1
Updated link after the move of `netbox-docker`
2019-01-30 14:19:57 -05:00
Christian Mäder
e51e8b5c8a Updated link after the move of netbox-docker 2019-01-30 16:17:37 +01:00
Jeremy Stretch
170900e80f Fixes #2824: Fix template exception when viewing rack elevations list 2019-01-30 09:59:19 -05:00
Brian Candler
9991985170 Clarify how chassis-based switches and routers are supposed to be modelled 2019-01-24 13:35:37 +00:00
42 changed files with 356 additions and 115 deletions

View File

@@ -1,3 +1,43 @@
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
* [#2805](https://github.com/digitalocean/netbox/issues/2805) - Allow null route distinguisher for VRFs
* [#2809](https://github.com/digitalocean/netbox/issues/2809) - Remove VRF child prefixes table; link to main prefixes view
* [#2825](https://github.com/digitalocean/netbox/issues/2825) - Include directly connected device for front/rear ports
## Bug Fixes
* [#2824](https://github.com/digitalocean/netbox/issues/2824) - Fix template exception when viewing rack elevations list
* [#2833](https://github.com/digitalocean/netbox/issues/2833) - Fix form widget for front port template creation
* [#2835](https://github.com/digitalocean/netbox/issues/2835) - Fix certain model filters did not support the `q` query param
* [#2837](https://github.com/digitalocean/netbox/issues/2837) - Fix select2 nullable filter fields add multiple null_option elements when paging
---
v2.5.4 (2019-01-29)
## Enhancements

View File

@@ -37,7 +37,7 @@ and run `upgrade.sh`.
## Alternative Installations
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Docker container](https://github.com/netbox-community/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))

View File

@@ -1,6 +1,6 @@
# Tags
Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object 9for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object (for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
Each tag has a label and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters.

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

@@ -83,7 +83,7 @@ An IP address can be designated as the network address translation (NAT) inside
A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space).
Each VRF is assigned a unique name and route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
Each VRF is assigned a unique name and an optional route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
Each prefix and IP address may be assigned to one (and only one) VRF. If you have a prefix or IP address which exists in multiple VRFs, you will need to create a separate instance of it in NetBox for each VRF. Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.

View File

@@ -4,7 +4,7 @@ from django.db.models import Q
from dcim.models import Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Provider, Circuit, CircuitTermination, CircuitType
@@ -47,7 +47,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
)
class CircuitTypeFilter(django_filters.FilterSet):
class CircuitTypeFilter(NameSlugSearchFilterSet):
class Meta:
model = CircuitType

View File

@@ -3,7 +3,7 @@ from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.constants import CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, STATUS_CLASSES
from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
from dcim.fields import ASNField
from dcim.models import CableTermination
from extras.models import CustomFieldModel, ObjectChange
@@ -283,6 +283,10 @@ class CircuitTermination(CableTermination):
object_data=serialize_object(self)
).save()
@property
def parent(self):
return self.circuit
def get_peer_termination(self):
peer_side = 'Z' if self.term_side == 'A' else 'A'
try:

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

@@ -8,7 +8,7 @@ from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES
from utilities.filters import NullableCharFieldFilter, NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter
from virtualization.models import Cluster
from .constants import *
from .models import (
@@ -19,11 +19,7 @@ from .models import (
)
class RegionFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
class RegionFilter(NameSlugSearchFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Parent region (ID)',
@@ -39,15 +35,6 @@ class RegionFilter(django_filters.FilterSet):
model = Region
fields = ['name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(slug__icontains=value)
)
return queryset.filter(qs_filter)
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(
@@ -119,11 +106,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
)
class RackGroupFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
class RackGroupFilter(NameSlugSearchFilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@@ -139,17 +122,8 @@ class RackGroupFilter(django_filters.FilterSet):
model = RackGroup
fields = ['site_id', 'name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(slug__icontains=value)
)
return queryset.filter(qs_filter)
class RackRoleFilter(django_filters.FilterSet):
class RackRoleFilter(NameSlugSearchFilterSet):
class Meta:
model = RackRole
@@ -303,7 +277,7 @@ class RackReservationFilter(django_filters.FilterSet):
)
class ManufacturerFilter(django_filters.FilterSet):
class ManufacturerFilter(NameSlugSearchFilterSet):
class Meta:
model = Manufacturer
@@ -393,7 +367,7 @@ class DeviceTypeFilter(CustomFieldFilterSet):
)
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
field_name='device_type_id',
@@ -457,14 +431,14 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
fields = ['name']
class DeviceRoleFilter(django_filters.FilterSet):
class DeviceRoleFilter(NameSlugSearchFilterSet):
class Meta:
model = DeviceRole
fields = ['name', 'slug', 'color', 'vm_role']
class PlatformFilter(django_filters.FilterSet):
class PlatformFilter(NameSlugSearchFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer',
queryset=Manufacturer.objects.all(),
@@ -696,6 +670,10 @@ class DeviceFilter(CustomFieldFilterSet):
class DeviceComponentFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
device_id = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
@@ -707,6 +685,13 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
)
tag = TagFilter()
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value)
)
class ConsolePortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(

View File

@@ -1066,7 +1066,6 @@ class FrontPortTemplateCreateForm(ComponentForm):
choices=[],
label='Rear ports',
help_text='Select one rear port assignment for each front port being created.',
widget=StaticSelect2(),
)
def __init__(self, *args, **kwargs):
@@ -1600,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(
@@ -2360,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(),
@@ -2413,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

@@ -68,6 +68,10 @@ class ComponentModel(models.Model):
object_data=serialize_object(self)
).save()
@property
def parent(self):
return getattr(self, 'device', None)
class CableTermination(models.Model):
cable = models.ForeignKey(
@@ -162,6 +166,14 @@ class CableTermination(models.Model):
return path + next_segment
def get_cable_peer(self):
if self.cable is None:
return None
if self._cabled_as_a.exists():
return self.cable.termination_b
if self._cabled_as_b.exists():
return self.cable.termination_a
#
# Regions
@@ -968,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

@@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from virtualization.models import VirtualMachine
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
@@ -48,7 +48,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
fields = ['name', 'rd', 'enforce_unique']
class RIRFilter(django_filters.FilterSet):
class RIRFilter(NameSlugSearchFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@@ -96,7 +96,11 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter(qs_filter)
class RoleFilter(django_filters.FilterSet):
class RoleFilter(NameSlugSearchFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
class Meta:
model = Role
@@ -373,7 +377,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.none()
class VLANGroupFilter(django_filters.FilterSet):
class VLANGroupFilter(NameSlugSearchFilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',

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

@@ -0,0 +1,18 @@
# Generated by Django 2.1.5 on 2019-01-31 18:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0023_change_logging'),
]
operations = [
migrations.AlterField(
model_name='vrf',
name='rd',
field=models.CharField(blank=True, max_length=21, null=True, unique=True),
),
]

View File

@@ -29,6 +29,8 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
rd = models.CharField(
max_length=21,
unique=True,
blank=True,
null=True,
verbose_name='Route distinguisher'
)
tenant = models.ForeignKey(
@@ -79,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

@@ -16,7 +16,7 @@ class VRFTest(APITestCase):
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
self.vrf2 = VRF.objects.create(name='Test VRF 2', rd='65000:2')
self.vrf3 = VRF.objects.create(name='Test VRF 3', rd='65000:3')
self.vrf3 = VRF.objects.create(name='Test VRF 3') # No RD
def test_get_vrf(self):
@@ -44,19 +44,26 @@ class VRFTest(APITestCase):
def test_create_vrf(self):
data = {
'name': 'Test VRF 4',
'rd': '65000:4',
}
data_list = [
# VRF with RD
{
'name': 'Test VRF 4',
'rd': '65000:4',
},
# VRF without RD
{
'name': 'Test VRF 5',
}
]
url = reverse('ipam-api:vrf-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(VRF.objects.count(), 4)
vrf4 = VRF.objects.get(pk=response.data['id'])
self.assertEqual(vrf4.name, data['name'])
self.assertEqual(vrf4.rd, data['rd'])
for data in data_list:
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
vrf = VRF.objects.get(pk=response.data['id'])
self.assertEqual(vrf.name, data['name'])
self.assertEqual(vrf.rd, data['rd'] if 'rd' in data else None)
def test_create_vrf_bulk(self):

View File

@@ -126,14 +126,11 @@ class VRFView(View):
def get(self, request, pk):
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
prefix_table = tables.PrefixTable(
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False
)
prefix_table.exclude = ('vrf',)
prefix_count = Prefix.objects.filter(vrf=vrf).count()
return render(request, 'ipam/vrf.html', {
'vrf': vrf,
'prefix_table': prefix_table,
'prefix_count': prefix_count,
})

View File

@@ -22,7 +22,7 @@ except ImportError:
)
VERSION = '2.5.4'
VERSION = '2.5.6'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

View File

@@ -197,8 +197,8 @@ $(document).ready(function() {
return obj;
});
// Handle the null option
if (element.getAttribute('data-null-option')) {
// Handle the null option, but only add it once
if (element.getAttribute('data-null-option') && data.previous === null) {
var null_option = $(element).children()[0]
results.unshift({
id: null_option.value,

View File

@@ -3,11 +3,11 @@ from django.db.models import Q
from dcim.models import Device
from extras.filters import CustomFieldFilterSet
from utilities.filters import NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .models import Secret, SecretRole
class SecretRoleFilter(django_filters.FilterSet):
class SecretRoleFilter(NameSlugSearchFilterSet):
class Meta:
model = SecretRole

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>
@@ -682,7 +682,8 @@
<th>Rear Port</th>
<th>Position</th>
<th>Description</th>
<th>Connected Cable</th>
<th>Cable</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
</thead>
@@ -697,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>
@@ -735,7 +739,8 @@
<th>Type</th>
<th>Positions</th>
<th>Description</th>
<th>Connected Cable</th>
<th>Cable</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
</thead>
@@ -750,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

@@ -23,14 +23,35 @@
{# Description #}
<td>{{ frontport.description|placeholder }}</td>
{# Cable #}
<td>
{% if frontport.cable %}
{# Cable/connection #}
{% if frontport.cable %}
<td>
<a href="{{ frontport.cable.get_absolute_url }}">{{ frontport.cable }}</a>
{% else %}
<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>
{% 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 %}
<td colspan="3">
<span class="text-muted">Not connected</span>
{% endif %}
</td>
</td>
{% endif %}
{# Actions #}
<td class="text-right">

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

@@ -22,14 +22,35 @@
{# Description #}
<td>{{ rearport.description|placeholder }}</td>
{# Cable #}
<td>
{% if rearport.cable %}
{# Cable/connection #}
{% if rearport.cable %}
<td>
<a href="{{ rearport.cable.get_absolute_url }}">{{ rearport.cable }}</a>
{% else %}
<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>
{% 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 %}
<td colspan="3">
<span class="text-muted">Not connected</span>
{% endif %}
</td>
</td>
{% endif %}
{# Actions #}
<td class="text-right">

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

@@ -45,7 +45,6 @@
{% endblock %}
{% block javascript %}
{% include 'dcim/inc/filter_rack_group.html' %}
<script type="text/javascript">
$(function() {
$('[data-toggle="popover"]').popover()

View File

@@ -83,19 +83,19 @@
<tr>
<td>Description</td>
<td>{{ vrf.description|placeholder }}</td>
</tr>
<tr>
<td>Prefixes</td>
<td>
<a href="{% url 'ipam:prefix_list' %}?vrf={{ vrf.rd }}">{{ prefix_count }}</a>
</td>
</tr>
</table>
</div>
{% include 'inc/custom_fields_panel.html' with obj=vrf %}
{% include 'extras/inc/tags_panel.html' with tags=vrf.tags.all url='ipam:vrf_list' %}
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Prefixes</strong>
</div>
{% include 'responsive_table.html' with table=prefix_table %}
</div>
</div>
{% include 'inc/custom_fields_panel.html' with obj=vrf %}
</div>
</div>
{% endblock %}

View File

@@ -2,11 +2,11 @@ import django_filters
from django.db.models import Q
from extras.filters import CustomFieldFilterSet
from utilities.filters import NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .models import Tenant, TenantGroup
class TenantGroupFilter(django_filters.FilterSet):
class TenantGroupFilter(NameSlugSearchFilterSet):
class Meta:
model = TenantGroup

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)

View File

@@ -1,4 +1,5 @@
import django_filters
from django.db.models import Q
from taggit.models import Tag
@@ -35,3 +36,21 @@ class TagFilter(django_filters.ModelMultipleChoiceFilter):
kwargs.setdefault('queryset', Tag.objects.all())
super().__init__(*args, **kwargs)
class NameSlugSearchFilterSet(django_filters.FilterSet):
"""
A base class for adding the search method to models which only expose the `name` and `slug` fields
"""
q = django_filters.CharFilter(
method='search',
label='Search',
)
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(slug__icontains=value)
)

View File

@@ -7,19 +7,19 @@ from netaddr.core import AddrFormatError
from dcim.models import DeviceRole, Interface, Platform, Region, Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .constants import VM_STATUS_CHOICES
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
class ClusterTypeFilter(django_filters.FilterSet):
class ClusterTypeFilter(NameSlugSearchFilterSet):
class Meta:
model = ClusterType
fields = ['name', 'slug']
class ClusterGroupFilter(django_filters.FilterSet):
class ClusterGroupFilter(NameSlugSearchFilterSet):
class Meta:
model = ClusterGroup
@@ -196,6 +196,10 @@ class VirtualMachineFilter(CustomFieldFilterSet):
class InterfaceFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_machine',
queryset=VirtualMachine.objects.all(),
@@ -225,3 +229,10 @@ class InterfaceFilter(django_filters.FilterSet):
return queryset.filter(mac_address=mac)
except AddrFormatError:
return queryset.none()
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value)
)