mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-24 02:48:06 +01:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77954a3796 | ||
|
|
8152dc4b04 | ||
|
|
109b233e14 | ||
|
|
cc3b26998b | ||
|
|
95dea1faaa | ||
|
|
57fecdbf17 | ||
|
|
3b4bcc881f | ||
|
|
dfa4dfa4a4 | ||
|
|
5da9d6b46b | ||
|
|
100809f11a | ||
|
|
5256077a3c | ||
|
|
375e66047d | ||
|
|
42d1d6e1b0 | ||
|
|
ca51fab4d8 | ||
|
|
73c983516d | ||
|
|
f733d5a4da | ||
|
|
3d2948daf3 | ||
|
|
69a5d3644a | ||
|
|
2f1018c742 | ||
|
|
d5fc37282f | ||
|
|
525ed359cd | ||
|
|
fe00db62d6 | ||
|
|
bcfa760cf9 | ||
|
|
613e8f05c2 | ||
|
|
59f8f0c7ea | ||
|
|
ae0c8deec2 | ||
|
|
b508415983 | ||
|
|
a98d014763 | ||
|
|
51e5e49d3b | ||
|
|
0eced489da | ||
|
|
5138d12942 | ||
|
|
fe30276db2 | ||
|
|
e51e8b5c8a | ||
|
|
170900e80f | ||
|
|
9991985170 |
40
CHANGELOG.md
40
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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'],
|
||||
],
|
||||
],
|
||||
[
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
18
netbox/ipam/migrations/0024_vrf_allow_null_rd.py
Normal file
18
netbox/ipam/migrations/0024_vrf_allow_null_rd.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -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__)))
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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">—</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 }})
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
{% include 'dcim/inc/filter_rack_group.html' %}
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$('[data-toggle="popover"]').popover()
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user