diff --git a/docs/data-model/extras.md b/docs/data-model/extras.md index a3fd23aad..f4654c0dd 100644 --- a/docs/data-model/extras.md +++ b/docs/data-model/extras.md @@ -119,7 +119,7 @@ Each line of the **device patterns** field represents a hierarchical layer withi ``` core-switch-[abcd] dist-switch\d -access-switch\d+,oob-switch\d+ +access-switch\d+;oob-switch\d+ ``` Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those. diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index da15ce1aa..5a1878b77 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -49,6 +49,10 @@ class CustomFieldsSerializer(serializers.BaseSerializer): # Validate selected choice if cf.type == CF_TYPE_SELECT: + try: + value = int(value) + except ValueError: + raise ValidationError("{}: Choice selections must be passed as integers.".format(field_name)) valid_choices = [c.pk for c in cf.choices.all()] if value not in valid_choices: raise ValidationError("Invalid choice for field {}: {}".format(field_name, value)) diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 8ee0fa3a3..9d0e636ff 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -332,7 +332,7 @@ def image_upload(instance, filename): path = 'image-attachments/' # Rename the file to the provided name, if any. Attempt to preserve the file extension. - extension = filename.rsplit('.')[-1] + extension = filename.rsplit('.')[-1].lower() if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']: filename = '.'.join([instance.name, extension]) elif instance.name: diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 6469ba76a..06c852d68 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -25,7 +25,7 @@ class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView): class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView): - permission_required = 'dcim.delete_imageattachment' + permission_required = 'extras.delete_imageattachment' model = ImageAttachment def get_return_url(self, request, imageattachment): diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index a822ba6b6..7eb9a9599 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -646,16 +646,23 @@ class IPAddressCSVForm(forms.ModelForm): # Set interface if self.cleaned_data['device'] and self.cleaned_data['interface_name']: - self.instance.interface = Interface.objects.get(device=self.cleaned_data['device'], - name=self.cleaned_data['interface_name']) + self.instance.interface = Interface.objects.get( + device=self.cleaned_data['device'], + name=self.cleaned_data['interface_name'] + ) + + ipaddress = super(IPAddressCSVForm, self).save(*args, **kwargs) + # Set as primary for device if self.cleaned_data['is_primary']: + device = self.cleaned_data['device'] if self.instance.address.version == 4: - self.instance.primary_ip4_for = self.cleaned_data['device'] + device.primary_ip4 = ipaddress elif self.instance.address.version == 6: - self.instance.primary_ip6_for = self.cleaned_data['device'] + device.primary_ip6 = ipaddress + device.save() - return super(IPAddressCSVForm, self).save(*args, **kwargs) + return ipaddress class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index add959862..04853c5da 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -158,13 +158,9 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): """ Determine the prefix utilization of the aggregate and return it as a percentage. """ - child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix)) - # Remove overlapping prefixes from list of children - networks = netaddr.cidr_merge([c.prefix for c in child_prefixes]) - children_size = float(0) - for p in networks: - children_size += p.size - return int(children_size / self.prefix.size * 100) + queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix)) + child_prefixes = netaddr.IPSet([p.prefix for p in queryset]) + return int(float(child_prefixes.size) / self.prefix.size * 100) @python_2_unicode_compatible @@ -345,13 +341,21 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): def get_utilization(self): """ - Determine the utilization of the prefix and return it as a percentage. + Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of + "container", calculate utilization based on child prefixes. For all others, count child IP addresses. """ - child_count = self.get_child_ips().count() - prefix_size = self.prefix.size - if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool: - prefix_size -= 2 - return int(float(child_count) / prefix_size * 100) + if self.status == PREFIX_STATUS_CONTAINER: + queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf) + child_prefixes = netaddr.IPSet([p.prefix for p in queryset]) + return int(float(child_prefixes.size) / self.prefix.size * 100) + else: + child_count = IPAddress.objects.filter( + address__net_contained_or_equal=str(self.prefix), vrf=self.vrf + ).count() + prefix_size = self.prefix.size + if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool: + prefix_size -= 2 + return int(float(child_count) / prefix_size * 100) @property def new_subnet(self): diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index bfdab9319..b648eb9c9 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -241,7 +241,7 @@ class PrefixTable(BaseTable): prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}}) status = tables.TemplateColumn(STATUS_LABEL) vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') - get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='IP Usage') + get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') tenant = tables.TemplateColumn(TENANT_LINK) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 3cc54d252..74432d180 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -665,19 +665,6 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView): table = tables.IPAddressTable default_return_url = 'ipam:ipaddress_list' - def save_obj(self, obj): - obj.save() - - # Update primary IP for device if needed. The Device must be updated directly in the database; otherwise we risk - # overwriting a previous IP assignment from the same import (see #861). - try: - if obj.family == 4 and obj.primary_ip4_for: - Device.objects.filter(pk=obj.primary_ip4_for.pk).update(primary_ip4=obj) - elif obj.family == 6 and obj.primary_ip6_for: - Device.objects.filter(pk=obj.primary_ip6_for.pk).update(primary_ip6=obj) - except Device.DoesNotExist: - pass - class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'ipam.change_ipaddress' diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index a4672f6d0..3bf92fbca 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -204,7 +204,7 @@ None {% endif %} - {% if perms.dcim.add_service %} + {% if perms.ipam.add_service %}