From 88adc5ca86f6dbc22bad766a812c17cb72c83d2d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 22 Mar 2018 15:06:59 -0400 Subject: [PATCH 01/13] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 52cd80169..e40106a21 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.3.2' +VERSION = '2.3.3-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 4ec6e52e7320011300aaa33ad1199845603593d2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Mar 2018 09:45:17 -0400 Subject: [PATCH 02/13] Closes #1990: Improved search function when assigning an IP address to an interface --- netbox/ipam/tables.py | 2 +- netbox/ipam/views.py | 4 ++-- netbox/templates/ipam/ipaddress_assign.html | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index f0b05b3db..08035d549 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -329,7 +329,7 @@ class IPAddressAssignTable(BaseTable): class Meta(BaseTable.Meta): model = IPAddress - fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface') + fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description') orderable = False diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 5c8ce68b6..1d4575e34 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -729,8 +729,8 @@ class IPAddressAssignView(PermissionRequiredMixin, View): 'vrf', 'tenant', 'interface__device', 'interface__virtual_machine' ).filter( vrf=form.cleaned_data['vrf'], - address__net_host=form.cleaned_data['address'], - ) + address__istartswith=form.cleaned_data['address'], + )[:100] # Limit to 100 results table = tables.IPAddressAssignTable(queryset) return render(request, 'ipam/ipaddress_assign.html', { diff --git a/netbox/templates/ipam/ipaddress_assign.html b/netbox/templates/ipam/ipaddress_assign.html index a623a55f3..63b1823f8 100644 --- a/netbox/templates/ipam/ipaddress_assign.html +++ b/netbox/templates/ipam/ipaddress_assign.html @@ -39,7 +39,7 @@ {% if table %}
-
+

Search Results

{% include 'utilities/obj_table.html' with table_template='panel_table.html' %}
From 94b12e506e66025b4711273c972613cec645b59b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Mar 2018 09:50:29 -0400 Subject: [PATCH 03/13] Fixes #1993: Corrected status choices in site CSV import form --- netbox/dcim/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6795726a6..df4d91c66 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -131,7 +131,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): class SiteCSVForm(forms.ModelForm): status = CSVChoiceField( - choices=DEVICE_STATUS_CHOICES, + choices=SITE_STATUS_CHOICES, required=False, help_text='Operational status' ) From 3f3b385de725ddd32f162420c3dcc1c6102d4793 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Mar 2018 13:49:50 -0400 Subject: [PATCH 04/13] Fixes #1999: Added missing description field to site edit form --- netbox/dcim/forms.py | 7 ++++--- netbox/templates/dcim/site_edit.html | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index df4d91c66..b360108bf 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -112,9 +112,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): class Meta: model = Site fields = [ - 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'description', - 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'time_zone', - 'comments', + 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description', + 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', ] widgets = { 'physical_address': SmallTextarea(attrs={'rows': 3}), @@ -124,6 +123,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): 'name': "Full name of the site", 'facility': "Data center provider and facility (e.g. Equinix NY7)", 'asn': "BGP autonomous system number", + 'time_zone': "Local time zone", + 'description': "Short description (will appear in sites list)", 'physical_address': "Physical location of the building (e.g. for GPS)", 'shipping_address': "If different from the physical address" } diff --git a/netbox/templates/dcim/site_edit.html b/netbox/templates/dcim/site_edit.html index 582f93996..399551434 100644 --- a/netbox/templates/dcim/site_edit.html +++ b/netbox/templates/dcim/site_edit.html @@ -12,6 +12,7 @@ {% render_field form.facility %} {% render_field form.asn %} {% render_field form.time_zone %} + {% render_field form.description %}
From 20cb13e1bbdca9b9f59cfeb8e288d8650fc34bb5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Mar 2018 14:47:35 -0400 Subject: [PATCH 05/13] Fixes #1975: Correct filtering logic for custom boolean fields --- netbox/extras/filters.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index f21c228db..4a991471b 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -43,11 +43,18 @@ class CustomFieldFilter(django_filters.Filter): return queryset.none() # Apply the assigned filter logic (exact or loose) - queryset = queryset.filter(custom_field_values__field__name=self.name) if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT: - return queryset.filter(custom_field_values__serialized_value=value) + queryset = queryset.filter( + custom_field_values__field__name=self.name, + custom_field_values__serialized_value=value + ) else: - return queryset.filter(custom_field_values__serialized_value__icontains=value) + queryset = queryset.filter( + custom_field_values__field__name=self.name, + custom_field_values__serialized_value__icontains=value + ) + + return queryset class CustomFieldFilterSet(django_filters.FilterSet): From 07364abf9e9ff193bad49b790e657382cf186f0c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Mar 2018 15:15:13 -0400 Subject: [PATCH 06/13] Fixes #1988: Order interfaces naturally when bulk renaming --- netbox/dcim/views.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 84760348b..924fe67fb 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -41,19 +41,21 @@ class BulkRenameView(View): """ An extendable view for renaming device components in bulk. """ - model = None + queryset = None form = None template_name = 'dcim/bulk_rename.html' def post(self, request): + model = self.queryset.model + return_url = request.GET.get('return_url') if not return_url or not is_safe_url(url=return_url, host=request.get_host()): return_url = 'home' if '_preview' in request.POST or '_apply' in request.POST: form = self.form(request.POST, initial={'pk': request.POST.getlist('pk')}) - selected_objects = self.model.objects.filter(pk__in=form.initial['pk']) + selected_objects = self.queryset.filter(pk__in=form.initial['pk']) if form.is_valid(): for obj in selected_objects: @@ -65,17 +67,17 @@ class BulkRenameView(View): obj.save() messages.success(request, "Renamed {} {}".format( len(selected_objects), - self.model._meta.verbose_name_plural + model._meta.verbose_name_plural )) return redirect(return_url) else: form = self.form(initial={'pk': request.POST.getlist('pk')}) - selected_objects = self.model.objects.filter(pk__in=form.initial['pk']) + selected_objects = self.queryset.filter(pk__in=form.initial['pk']) return render(request, self.template_name, { 'form': form, - 'obj_type_plural': self.model._meta.verbose_name_plural, + 'obj_type_plural': model._meta.verbose_name_plural, 'selected_objects': selected_objects, 'return_url': return_url, }) @@ -1316,7 +1318,7 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): class ConsoleServerPortBulkRenameView(PermissionRequiredMixin, BulkRenameView): permission_required = 'dcim.change_consoleserverport' - model = ConsoleServerPort + queryset = ConsoleServerPort.objects.all() form = forms.ConsoleServerPortBulkRenameForm @@ -1600,7 +1602,7 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView): class PowerOutletBulkRenameView(PermissionRequiredMixin, BulkRenameView): permission_required = 'dcim.change_poweroutlet' - model = PowerOutlet + queryset = PowerOutlet.objects.all() form = forms.PowerOutletBulkRenameForm @@ -1676,7 +1678,7 @@ class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceBulkRenameView(PermissionRequiredMixin, BulkRenameView): permission_required = 'dcim.change_interface' - model = Interface + queryset = Interface.objects.order_naturally() form = forms.InterfaceBulkRenameForm @@ -1783,7 +1785,7 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View): class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView): permission_required = 'dcim.change_devicebay' - model = DeviceBay + queryset = DeviceBay.objects.all() form = forms.DeviceBayBulkRenameForm From 2c8bea1b5974c99777f17f6d7aec2041cd689cdb Mon Sep 17 00:00:00 2001 From: frankfarmer Date: Mon, 9 Apr 2018 17:42:54 -0700 Subject: [PATCH 07/13] avoid illegal casts on large integers A similar fix was applied in e5e32d82d00e454ba5edf25316828c1cdcd7673e --- netbox/dcim/querysets.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/netbox/dcim/querysets.py b/netbox/dcim/querysets.py index 3e977ddc6..32275ce01 100644 --- a/netbox/dcim/querysets.py +++ b/netbox/dcim/querysets.py @@ -43,13 +43,13 @@ class InterfaceQuerySet(QuerySet): }[method] TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')" - ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)" - SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)" - SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)" - POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)" - SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)" - CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)" - VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)" + ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(\d{{1,9}})$') AS integer)" + SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})\/') AS integer)" + SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/)(\d{{1,9}})') AS integer), 0)" + POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{2}}(\d{{1,9}})') AS integer), 0)" + SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{3}}(\d{{1,9}})') AS integer), 0)" + CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)" + VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.(\d{{1,9}})$') AS integer), 0)" fields = { '_type': RawSQL(TYPE_RE.format(sql_col), []), From 81c027e7cffd73238ca108ff3bcf2ae4344a25c4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 12 Apr 2018 12:45:25 -0400 Subject: [PATCH 08/13] Fixes #2023: Manufacturer should not be a required field when importing platforms --- netbox/dcim/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b360108bf..05dc0ea6f 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -706,7 +706,7 @@ class PlatformCSVForm(forms.ModelForm): slug = SlugField() manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), - required=True, + required=False, to_field_name='name', help_text='Manufacturer name', error_messages={ From ef84889a573ea06308ade4af02cdcf5aa7c1798a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 12 Apr 2018 12:54:21 -0400 Subject: [PATCH 09/13] Fixes #2022: Show 0 for zero-value fields on CSV export --- netbox/utilities/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index c08bfef8c..9e96a66fd 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -14,7 +14,7 @@ def csv_format(data): for value in data: # Represent None or False with empty string - if value in [None, False]: + if value is None or value is False: csv.append('') continue From bcb1d9af0be7ae8bd1236b9b7a0e3c98f41e8f4a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 12 Apr 2018 13:03:20 -0400 Subject: [PATCH 10/13] Fixes #2012: Fixed deselection of an IP address as the primary IP for its parent device/VM --- netbox/ipam/forms.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 5b2c6e672..3353d981f 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -508,7 +508,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ipaddress = super(IPAddressForm, self).save(*args, **kwargs) - # Assign this IPAddress as the primary for the associated Device. + # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine. if self.cleaned_data['primary_for_parent']: parent = self.cleaned_data['interface'].parent if ipaddress.address.version == 4: @@ -516,14 +516,12 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) else: parent.primary_ip6 = ipaddress parent.save() - - # Clear assignment as primary for device if set. elif self.cleaned_data['interface']: parent = self.cleaned_data['interface'].parent - if ipaddress.address.version == 4 and parent.primary_ip4 == self: + if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress: parent.primary_ip4 = None parent.save() - elif ipaddress.address.version == 6 and parent.primary_ip6 == self: + elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress: parent.primary_ip6 = None parent.save() From b44aa9d32ea654de242215238d8c9131f4594d2d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Apr 2018 12:37:20 -0400 Subject: [PATCH 11/13] Fixes #2014: Allow assignment of VLANs to VM interfaces via the API --- netbox/virtualization/api/serializers.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 7e2ec1690..c03cdc166 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -3,10 +3,10 @@ from __future__ import unicode_literals from rest_framework import serializers from dcim.api.serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer -from dcim.constants import IFACE_FF_VIRTUAL +from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_CHOICES from dcim.models import Interface from extras.api.customfields import CustomFieldModelSerializer -from ipam.models import IPAddress +from ipam.models import IPAddress, VLAN from tenancy.api.serializers import NestedTenantSerializer from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer from virtualization.constants import VM_STATUS_CHOICES @@ -133,13 +133,26 @@ class WritableVirtualMachineSerializer(CustomFieldModelSerializer): # VM interfaces # +# Cannot import ipam.api.serializers.NestedVLANSerializer due to circular dependency +class InterfaceVLANSerializer(serializers.ModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') + + class Meta: + model = VLAN + fields = ['id', 'url', 'vid', 'name', 'display_name'] + + class InterfaceSerializer(serializers.ModelSerializer): virtual_machine = NestedVirtualMachineSerializer() + mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES) + untagged_vlan = InterfaceVLANSerializer() + tagged_vlans = InterfaceVLANSerializer(many=True) class Meta: model = Interface fields = [ - 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'description', + 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'untagged_vlan', 'tagged_vlans', + 'description', ] @@ -157,5 +170,6 @@ class WritableInterfaceSerializer(ValidatedModelSerializer): class Meta: model = Interface fields = [ - 'id', 'name', 'virtual_machine', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description', + 'id', 'name', 'virtual_machine', 'form_factor', 'enabled', 'mac_address', 'mtu', 'mode', 'untagged_vlan', + 'tagged_vlans', 'description', ] From 9153c71cbf1a9f7273bc7a29e2505687de201a59 Mon Sep 17 00:00:00 2001 From: "Nicholas St. Germain" Date: Wed, 18 Apr 2018 14:02:40 -0500 Subject: [PATCH 12/13] stop force value split w ArrayFieldSelectMultiple --- netbox/utilities/forms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 15fb69f7f..69b102f5c 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -205,7 +205,8 @@ class ArrayFieldSelectMultiple(SelectWithDisabled, forms.SelectMultiple): def optgroups(self, name, value, attrs=None): # Split the delimited string of values into a list - value = value[0].split(self.delimiter) + if value: + value = value[0].split(self.delimiter) return super(ArrayFieldSelectMultiple, self).optgroups(name, value, attrs) def value_from_datadict(self, data, files, name): From a7389de109bd2e30ec4b995d646eea1a1c64f849 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 19 Apr 2018 11:07:19 -0400 Subject: [PATCH 13/13] Release v2.3.3 --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e40106a21..7a8dd1a98 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.3.3-dev' +VERSION = '2.3.3' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))