diff --git a/CHANGELOG.md b/CHANGELOG.md index 77a8bdcbd..cf51633f8 100644 --- a/CHANGELOG.md +++ b/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) ## Enhancements diff --git a/README.md b/README.md index 04e61029a..39ab1afc4 100644 --- a/README.md +++ b/README.md @@ -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)) diff --git a/docs/additional-features/tags.md b/docs/additional-features/tags.md index 18edcad12..f94957616 100644 --- a/docs/additional-features/tags.md +++ b/docs/additional-features/tags.md @@ -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. diff --git a/docs/core-functionality/ipam.md b/docs/core-functionality/ipam.md index 05b613da2..cd95c40e6 100644 --- a/docs/core-functionality/ipam.md +++ b/docs/core-functionality/ipam.md @@ -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. diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 0982624d5..12955eeca 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -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 diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index f10221b0b..b558d5007 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -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: diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index e10cfe337..388426e06 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -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( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 5233895d0..4d02506fe 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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): diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 89e786a1b..8c6ed9650 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -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: + return self.cable.termination_b + if self._cabled_as_b: + return self.cable.termination_a + # # Regions diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 22df1b1c9..f7125ceb0 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -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)', diff --git a/netbox/ipam/migrations/0024_vrf_allow_null_rd.py b/netbox/ipam/migrations/0024_vrf_allow_null_rd.py new file mode 100644 index 000000000..611644f6c --- /dev/null +++ b/netbox/ipam/migrations/0024_vrf_allow_null_rd.py @@ -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), + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index a14e1c7ed..7c879595f 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -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( diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index d57cb728f..3f4555b55 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -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): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 3a4c36173..2f76089a2 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -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, }) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index ba7cca57b..f2692776a 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -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__))) diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 235705763..438882805 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -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, diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index 5880fb9f9..6548708b5 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -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 diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 860a3aa44..8ee50c423 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -682,7 +682,8 @@ Rear Port Position Description - Connected Cable + Cable + Connection @@ -735,7 +736,8 @@ Type Positions Description - Connected Cable + Cable + Connection diff --git a/netbox/templates/dcim/inc/frontport.html b/netbox/templates/dcim/inc/frontport.html index 568eaa38e..3ac87e090 100644 --- a/netbox/templates/dcim/inc/frontport.html +++ b/netbox/templates/dcim/inc/frontport.html @@ -23,14 +23,20 @@ {# Description #} {{ frontport.description|placeholder }} - {# Cable #} - - {% if frontport.cable %} + {# Cable/connection #} + {% if frontport.cable %} + {{ frontport.cable }} - {% else %} + + {% with far_end=frontport.get_cable_peer %} + {{ far_end.parent }} + {{ far_end }} + {% endwith %} + {% else %} + Not connected - {% endif %} - + + {% endif %} {# Actions #} diff --git a/netbox/templates/dcim/inc/rearport.html b/netbox/templates/dcim/inc/rearport.html index 77ddbb78a..fc6b5c1df 100644 --- a/netbox/templates/dcim/inc/rearport.html +++ b/netbox/templates/dcim/inc/rearport.html @@ -22,14 +22,20 @@ {# Description #} {{ rearport.description|placeholder }} - {# Cable #} - - {% if rearport.cable %} + {# Cable/connection #} + {% if rearport.cable %} + {{ rearport.cable }} - {% else %} + + {% with far_end=rearport.get_cable_peer %} + {{ far_end.parent }} + {{ far_end }} + {% endwith %} + {% else %} + Not connected - {% endif %} - + + {% endif %} {# Actions #} diff --git a/netbox/templates/dcim/rack_elevation_list.html b/netbox/templates/dcim/rack_elevation_list.html index 38a821750..3019eb288 100644 --- a/netbox/templates/dcim/rack_elevation_list.html +++ b/netbox/templates/dcim/rack_elevation_list.html @@ -45,7 +45,6 @@ {% endblock %} {% block javascript %} - {% include 'dcim/inc/filter_rack_group.html' %}