mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
commit
d5fc37282f
17
CHANGELOG.md
17
CHANGELOG.md
@ -1,3 +1,20 @@
|
|||||||
|
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)
|
v2.5.4 (2019-01-29)
|
||||||
|
|
||||||
## Enhancements
|
## Enhancements
|
||||||
|
@ -37,7 +37,7 @@ and run `upgrade.sh`.
|
|||||||
|
|
||||||
## Alternative Installations
|
## 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))
|
* [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))
|
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Tags
|
# 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.
|
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.
|
||||||
|
|
||||||
|
@ -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).
|
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.
|
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 dcim.models import Site
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
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 .constants import CIRCUIT_STATUS_CHOICES
|
||||||
from .models import Provider, Circuit, CircuitTermination, CircuitType
|
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:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
|
@ -3,7 +3,7 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from taggit.managers import TaggableManager
|
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.fields import ASNField
|
||||||
from dcim.models import CableTermination
|
from dcim.models import CableTermination
|
||||||
from extras.models import CustomFieldModel, ObjectChange
|
from extras.models import CustomFieldModel, ObjectChange
|
||||||
@ -283,6 +283,10 @@ class CircuitTermination(CableTermination):
|
|||||||
object_data=serialize_object(self)
|
object_data=serialize_object(self)
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
return self.circuit
|
||||||
|
|
||||||
def get_peer_termination(self):
|
def get_peer_termination(self):
|
||||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||||
try:
|
try:
|
||||||
|
@ -8,7 +8,7 @@ from netaddr.core import AddrFormatError
|
|||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.constants import COLOR_CHOICES
|
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 virtualization.models import Cluster
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import (
|
from .models import (
|
||||||
@ -19,11 +19,7 @@ from .models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RegionFilter(django_filters.FilterSet):
|
class RegionFilter(NameSlugSearchFilterSet):
|
||||||
q = django_filters.CharFilter(
|
|
||||||
method='search',
|
|
||||||
label='Search',
|
|
||||||
)
|
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
label='Parent region (ID)',
|
label='Parent region (ID)',
|
||||||
@ -39,15 +35,6 @@ class RegionFilter(django_filters.FilterSet):
|
|||||||
model = Region
|
model = Region
|
||||||
fields = ['name', 'slug']
|
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):
|
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
@ -119,11 +106,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RackGroupFilter(django_filters.FilterSet):
|
class RackGroupFilter(NameSlugSearchFilterSet):
|
||||||
q = django_filters.CharFilter(
|
|
||||||
method='search',
|
|
||||||
label='Search',
|
|
||||||
)
|
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
@ -139,17 +122,8 @@ class RackGroupFilter(django_filters.FilterSet):
|
|||||||
model = RackGroup
|
model = RackGroup
|
||||||
fields = ['site_id', 'name', 'slug']
|
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(NameSlugSearchFilterSet):
|
||||||
class RackRoleFilter(django_filters.FilterSet):
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
@ -303,7 +277,7 @@ class RackReservationFilter(django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerFilter(django_filters.FilterSet):
|
class ManufacturerFilter(NameSlugSearchFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
@ -393,7 +367,7 @@ class DeviceTypeFilter(CustomFieldFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
|
||||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
field_name='device_type_id',
|
field_name='device_type_id',
|
||||||
@ -457,14 +431,14 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
|||||||
fields = ['name']
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleFilter(django_filters.FilterSet):
|
class DeviceRoleFilter(NameSlugSearchFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ['name', 'slug', 'color', 'vm_role']
|
fields = ['name', 'slug', 'color', 'vm_role']
|
||||||
|
|
||||||
|
|
||||||
class PlatformFilter(django_filters.FilterSet):
|
class PlatformFilter(NameSlugSearchFilterSet):
|
||||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='manufacturer',
|
field_name='manufacturer',
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
@ -696,6 +670,10 @@ class DeviceFilter(CustomFieldFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceComponentFilterSet(django_filters.FilterSet):
|
class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
device_id = django_filters.ModelChoiceFilter(
|
device_id = django_filters.ModelChoiceFilter(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
@ -707,6 +685,13 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
tag = TagFilter()
|
tag = TagFilter()
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortFilter(DeviceComponentFilterSet):
|
class ConsolePortFilter(DeviceComponentFilterSet):
|
||||||
cabled = django_filters.BooleanFilter(
|
cabled = django_filters.BooleanFilter(
|
||||||
|
@ -1066,7 +1066,6 @@ class FrontPortTemplateCreateForm(ComponentForm):
|
|||||||
choices=[],
|
choices=[],
|
||||||
label='Rear ports',
|
label='Rear ports',
|
||||||
help_text='Select one rear port assignment for each front port being created.',
|
help_text='Select one rear port assignment for each front port being created.',
|
||||||
widget=StaticSelect2(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -68,6 +68,10 @@ class ComponentModel(models.Model):
|
|||||||
object_data=serialize_object(self)
|
object_data=serialize_object(self)
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
return getattr(self, 'device', None)
|
||||||
|
|
||||||
|
|
||||||
class CableTermination(models.Model):
|
class CableTermination(models.Model):
|
||||||
cable = models.ForeignKey(
|
cable = models.ForeignKey(
|
||||||
@ -162,6 +166,14 @@ class CableTermination(models.Model):
|
|||||||
|
|
||||||
return path + next_segment
|
return path + next_segment
|
||||||
|
|
||||||
|
def get_cable_peer(self):
|
||||||
|
if self.cable is None:
|
||||||
|
return None
|
||||||
|
if self._cabled_as_a:
|
||||||
|
return self.cable.termination_b
|
||||||
|
if self._cabled_as_b:
|
||||||
|
return self.cable.termination_a
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Regions
|
# Regions
|
||||||
|
@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError
|
|||||||
from dcim.models import Site, Device, Interface
|
from dcim.models import Site, Device, Interface
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NumericInFilter, TagFilter
|
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
|
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
|
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']
|
fields = ['name', 'rd', 'enforce_unique']
|
||||||
|
|
||||||
|
|
||||||
class RIRFilter(django_filters.FilterSet):
|
class RIRFilter(NameSlugSearchFilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
field_name='id',
|
field_name='id',
|
||||||
lookup_expr='in'
|
lookup_expr='in'
|
||||||
@ -96,7 +96,11 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class RoleFilter(django_filters.FilterSet):
|
class RoleFilter(NameSlugSearchFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
@ -373,7 +377,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupFilter(django_filters.FilterSet):
|
class VLANGroupFilter(NameSlugSearchFilterSet):
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
|
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(
|
rd = models.CharField(
|
||||||
max_length=21,
|
max_length=21,
|
||||||
unique=True,
|
unique=True,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
verbose_name='Route distinguisher'
|
verbose_name='Route distinguisher'
|
||||||
)
|
)
|
||||||
tenant = models.ForeignKey(
|
tenant = models.ForeignKey(
|
||||||
|
@ -16,7 +16,7 @@ class VRFTest(APITestCase):
|
|||||||
|
|
||||||
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
|
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.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):
|
def test_get_vrf(self):
|
||||||
|
|
||||||
@ -44,19 +44,26 @@ class VRFTest(APITestCase):
|
|||||||
|
|
||||||
def test_create_vrf(self):
|
def test_create_vrf(self):
|
||||||
|
|
||||||
data = {
|
data_list = [
|
||||||
'name': 'Test VRF 4',
|
# VRF with RD
|
||||||
'rd': '65000:4',
|
{
|
||||||
}
|
'name': 'Test VRF 4',
|
||||||
|
'rd': '65000:4',
|
||||||
|
},
|
||||||
|
# VRF without RD
|
||||||
|
{
|
||||||
|
'name': 'Test VRF 5',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
url = reverse('ipam-api:vrf-list')
|
url = reverse('ipam-api:vrf-list')
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
for data in data_list:
|
||||||
self.assertEqual(VRF.objects.count(), 4)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
vrf4 = VRF.objects.get(pk=response.data['id'])
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(vrf4.name, data['name'])
|
vrf = VRF.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(vrf4.rd, data['rd'])
|
self.assertEqual(vrf.name, data['name'])
|
||||||
|
self.assertEqual(vrf.rd, data['rd'] if 'rd' in data else None)
|
||||||
|
|
||||||
def test_create_vrf_bulk(self):
|
def test_create_vrf_bulk(self):
|
||||||
|
|
||||||
|
@ -126,14 +126,11 @@ class VRFView(View):
|
|||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
|
|
||||||
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
||||||
prefix_table = tables.PrefixTable(
|
prefix_count = Prefix.objects.filter(vrf=vrf).count()
|
||||||
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False
|
|
||||||
)
|
|
||||||
prefix_table.exclude = ('vrf',)
|
|
||||||
|
|
||||||
return render(request, 'ipam/vrf.html', {
|
return render(request, 'ipam/vrf.html', {
|
||||||
'vrf': vrf,
|
'vrf': vrf,
|
||||||
'prefix_table': prefix_table,
|
'prefix_count': prefix_count,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ except ImportError:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
VERSION = '2.5.4'
|
VERSION = '2.5.5'
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
@ -197,8 +197,8 @@ $(document).ready(function() {
|
|||||||
return obj;
|
return obj;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle the null option
|
// Handle the null option, but only add it once
|
||||||
if (element.getAttribute('data-null-option')) {
|
if (element.getAttribute('data-null-option') && data.previous === null) {
|
||||||
var null_option = $(element).children()[0]
|
var null_option = $(element).children()[0]
|
||||||
results.unshift({
|
results.unshift({
|
||||||
id: null_option.value,
|
id: null_option.value,
|
||||||
|
@ -3,11 +3,11 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from utilities.filters import NumericInFilter, TagFilter
|
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||||
from .models import Secret, SecretRole
|
from .models import Secret, SecretRole
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleFilter(django_filters.FilterSet):
|
class SecretRoleFilter(NameSlugSearchFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SecretRole
|
model = SecretRole
|
||||||
|
@ -682,7 +682,8 @@
|
|||||||
<th>Rear Port</th>
|
<th>Rear Port</th>
|
||||||
<th>Position</th>
|
<th>Position</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Connected Cable</th>
|
<th>Cable</th>
|
||||||
|
<th colspan="2">Connection</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -735,7 +736,8 @@
|
|||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Positions</th>
|
<th>Positions</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Connected Cable</th>
|
<th>Cable</th>
|
||||||
|
<th colspan="2">Connection</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -23,14 +23,20 @@
|
|||||||
{# Description #}
|
{# Description #}
|
||||||
<td>{{ frontport.description|placeholder }}</td>
|
<td>{{ frontport.description|placeholder }}</td>
|
||||||
|
|
||||||
{# Cable #}
|
{# Cable/connection #}
|
||||||
<td>
|
{% if frontport.cable %}
|
||||||
{% if frontport.cable %}
|
<td>
|
||||||
<a href="{{ frontport.cable.get_absolute_url }}">{{ frontport.cable }}</a>
|
<a href="{{ frontport.cable.get_absolute_url }}">{{ frontport.cable }}</a>
|
||||||
{% else %}
|
</td>
|
||||||
|
{% with far_end=frontport.get_cable_peer %}
|
||||||
|
<td><a href="{{ far_end.parent.get_absolute_url }}">{{ far_end.parent }}</a></td>
|
||||||
|
<td>{{ far_end }}</td>
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
<td colspan="3">
|
||||||
<span class="text-muted">Not connected</span>
|
<span class="text-muted">Not connected</span>
|
||||||
{% endif %}
|
</td>
|
||||||
</td>
|
{% endif %}
|
||||||
|
|
||||||
{# Actions #}
|
{# Actions #}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
|
@ -22,14 +22,20 @@
|
|||||||
{# Description #}
|
{# Description #}
|
||||||
<td>{{ rearport.description|placeholder }}</td>
|
<td>{{ rearport.description|placeholder }}</td>
|
||||||
|
|
||||||
{# Cable #}
|
{# Cable/connection #}
|
||||||
<td>
|
{% if rearport.cable %}
|
||||||
{% if rearport.cable %}
|
<td>
|
||||||
<a href="{{ rearport.cable.get_absolute_url }}">{{ rearport.cable }}</a>
|
<a href="{{ rearport.cable.get_absolute_url }}">{{ rearport.cable }}</a>
|
||||||
{% else %}
|
</td>
|
||||||
|
{% with far_end=rearport.get_cable_peer %}
|
||||||
|
<td><a href="{{ far_end.parent.get_absolute_url }}">{{ far_end.parent }}</a></td>
|
||||||
|
<td>{{ far_end }}</td>
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
<td colspan="3">
|
||||||
<span class="text-muted">Not connected</span>
|
<span class="text-muted">Not connected</span>
|
||||||
{% endif %}
|
</td>
|
||||||
</td>
|
{% endif %}
|
||||||
|
|
||||||
{# Actions #}
|
{# Actions #}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
|
@ -45,7 +45,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
{% include 'dcim/inc/filter_rack_group.html' %}
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function() {
|
$(function() {
|
||||||
$('[data-toggle="popover"]').popover()
|
$('[data-toggle="popover"]').popover()
|
||||||
|
@ -83,19 +83,19 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Description</td>
|
<td>Description</td>
|
||||||
<td>{{ vrf.description|placeholder }}</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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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' %}
|
{% include 'extras/inc/tags_panel.html' with tags=vrf.tags.all url='ipam:vrf_list' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
{% include 'inc/custom_fields_panel.html' with obj=vrf %}
|
||||||
<div class="panel-heading">
|
</div>
|
||||||
<strong>Prefixes</strong>
|
|
||||||
</div>
|
|
||||||
{% include 'responsive_table.html' with table=prefix_table %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -2,11 +2,11 @@ import django_filters
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from utilities.filters import NumericInFilter, TagFilter
|
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||||
from .models import Tenant, TenantGroup
|
from .models import Tenant, TenantGroup
|
||||||
|
|
||||||
|
|
||||||
class TenantGroupFilter(django_filters.FilterSet):
|
class TenantGroupFilter(NameSlugSearchFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TenantGroup
|
model = TenantGroup
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
|
from django.db.models import Q
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
|
||||||
|
|
||||||
@ -35,3 +36,21 @@ class TagFilter(django_filters.ModelMultipleChoiceFilter):
|
|||||||
kwargs.setdefault('queryset', Tag.objects.all())
|
kwargs.setdefault('queryset', Tag.objects.all())
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
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 dcim.models import DeviceRole, Interface, Platform, Region, Site
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
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 .constants import VM_STATUS_CHOICES
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
class ClusterTypeFilter(django_filters.FilterSet):
|
class ClusterTypeFilter(NameSlugSearchFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ClusterType
|
model = ClusterType
|
||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class ClusterGroupFilter(django_filters.FilterSet):
|
class ClusterGroupFilter(NameSlugSearchFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ClusterGroup
|
model = ClusterGroup
|
||||||
@ -196,6 +196,10 @@ class VirtualMachineFilter(CustomFieldFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceFilter(django_filters.FilterSet):
|
class InterfaceFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='virtual_machine',
|
field_name='virtual_machine',
|
||||||
queryset=VirtualMachine.objects.all(),
|
queryset=VirtualMachine.objects.all(),
|
||||||
@ -225,3 +229,10 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
return queryset.filter(mac_address=mac)
|
return queryset.filter(mac_address=mac)
|
||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value)
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user