diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index aabc726f0..e1840319e 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -1,5 +1,20 @@ # NetBox v2.11 +## v2.11.8 (FUTURE) + +### Enhancements + +* [#6620](https://github.com/netbox-community/netbox/issues/6620) - Show assigned VMs count under device role view + +### Bug Fixes + +* [#6626](https://github.com/netbox-community/netbox/issues/6626) - Fix site field on VM search form; add site group +* [#6637](https://github.com/netbox-community/netbox/issues/6637) - Fix group assignment in "available VLANs" link under VLAN group view +* [#6640](https://github.com/netbox-community/netbox/issues/6640) - Disallow numeric values in custom text fields +* [#6652](https://github.com/netbox-community/netbox/issues/6652) - Fix exception when adding components in bulk to multiple devices + +--- + ## v2.11.7 (2021-06-16) ### Enhancements diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 93e7a84a6..79e42b777 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1171,6 +1171,8 @@ class DeviceRoleView(generic.ObjectView): return { 'devices_table': devices_table, + 'device_count': Device.objects.filter(device_role=instance).count(), + 'virtualmachine_count': VirtualMachine.objects.filter(role=instance).count(), } diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index b88c73531..60c6adce9 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -280,8 +280,10 @@ class CustomField(BigIDModel): if value not in [None, '']: # Validate text field - if self.type == CustomFieldTypeChoices.TYPE_TEXT and self.validation_regex: - if not re.match(self.validation_regex, value): + if self.type == CustomFieldTypeChoices.TYPE_TEXT: + if type(value) is not str: + raise ValidationError(f"Value must be a string.") + if self.validation_regex and not re.match(self.validation_regex, value): raise ValidationError(f"Value must match regex '{self.validation_regex}'") # Validate integer diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 1ace3a052..0ddc8697e 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -65,7 +65,7 @@ VLAN_LINK = """ {% if record.pk %} {{ record.vid }} {% elif perms.ipam.add_vlan %} - {{ record.available }} VLAN{{ record.available|pluralize }} available + {{ record.available }} VLAN{{ record.available|pluralize }} available {% else %} {{ record.available }} VLAN{{ record.available|pluralize }} available {% endif %} diff --git a/netbox/ipam/utils.py b/netbox/ipam/utils.py index 18a470253..0e79e7f78 100644 --- a/netbox/ipam/utils.py +++ b/netbox/ipam/utils.py @@ -68,24 +68,40 @@ def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False): return output -def add_available_vlans(vlan_group, vlans): +def add_available_vlans(vlans, vlan_group=None): """ Create fake records for all gaps between used VLANs """ if not vlans: - return [{'vid': VLAN_VID_MIN, 'available': VLAN_VID_MAX - VLAN_VID_MIN + 1}] + return [{ + 'vid': VLAN_VID_MIN, + 'vlan_group': vlan_group, + 'available': VLAN_VID_MAX - VLAN_VID_MIN + 1 + }] prev_vid = VLAN_VID_MAX new_vlans = [] for vlan in vlans: if vlan.vid - prev_vid > 1: - new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1}) + new_vlans.append({ + 'vid': prev_vid + 1, + 'vlan_group': vlan_group, + 'available': vlan.vid - prev_vid - 1, + }) prev_vid = vlan.vid if vlans[0].vid > VLAN_VID_MIN: - new_vlans.append({'vid': VLAN_VID_MIN, 'available': vlans[0].vid - VLAN_VID_MIN}) + new_vlans.append({ + 'vid': VLAN_VID_MIN, + 'vlan_group': vlan_group, + 'available': vlans[0].vid - VLAN_VID_MIN, + }) if prev_vid < VLAN_VID_MAX: - new_vlans.append({'vid': prev_vid + 1, 'available': VLAN_VID_MAX - prev_vid}) + new_vlans.append({ + 'vid': prev_vid + 1, + 'vlan_group': vlan_group, + 'available': VLAN_VID_MAX - prev_vid, + }) vlans = list(vlans) + new_vlans vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid']) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 919b09b79..95546fcc6 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -145,7 +145,6 @@ class RIRListView(generic.ObjectListView): filterset = filtersets.RIRFilterSet filterset_form = forms.RIRFilterForm table = tables.RIRTable - template_name = 'ipam/rir_list.html' class RIRView(generic.ObjectView): @@ -676,7 +675,7 @@ class VLANGroupView(generic.ObjectView): Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)) ).order_by('vid') vlans_count = vlans.count() - vlans = add_available_vlans(instance, vlans) + vlans = add_available_vlans(vlans, vlan_group=instance) vlans_table = tables.VLANDetailTable(vlans) if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'): diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index 23b7f9c8d..b00d07ec2 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -1218,7 +1218,7 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, component_form = self.model_form(component_data) if component_form.is_valid(): instance = component_form.save() - logger.debug(f"Created {instance} on {instance.parent}") + logger.debug(f"Created {instance} on {instance.parent_object}") new_components.append(instance) else: for field, errors in component_form.errors.as_data().items(): diff --git a/netbox/templates/dcim/devicerole.html b/netbox/templates/dcim/devicerole.html index 6811e9036..87b078e59 100644 --- a/netbox/templates/dcim/devicerole.html +++ b/netbox/templates/dcim/devicerole.html @@ -43,7 +43,17 @@ Devices - {{ devices_table.rows|length }} + {{ device_count }} + + + + Virtual Machines + + {% if object.vm_role %} + {{ virtualmachine_count }} + {% else %} + — + {% endif %} diff --git a/netbox/templates/ipam/rir_list.html b/netbox/templates/ipam/rir_list.html deleted file mode 100644 index ed4d0deb4..000000000 --- a/netbox/templates/ipam/rir_list.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'generic/object_list.html' %} - -{% block extra_controls %} - {% if request.GET.family == '6' %} - - - IPv4 Stats - - {% else %} - - - IPv6 Stats - - {% endif %} -{% endblock %} - -{% block sidebar %} - {% if request.GET.family == '6' %} -
- Numbers shown indicate /64 prefixes. -
- {% endif %} -{% endblock %} diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 5600fd525..36b0ecf57 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -531,8 +531,8 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VirtualMachine field_order = [ - 'cluster_group_id', 'cluster_type_id', 'cluster_id', 'status', 'role_id', 'region_id', 'site_id', - 'tenant_group_id', 'tenant_id', 'platform_id', 'mac_address', + 'q', 'cluster_group_id', 'cluster_type_id', 'cluster_id', 'status', 'role_id', 'region_id', 'site_group_id', + 'site_id', 'tenant_group_id', 'tenant_id', 'platform_id', 'mac_address', ] field_groups = [ ['status', 'role_id'], @@ -564,14 +564,20 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil required=False, label=_('Region') ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site group') + ) site_id = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), required=False, null_option='None', query_params={ - 'region_id': '$region_id' + 'region_id': '$region_id', + 'group_id': '$site_group_id', }, - label=_('Cluster') + label=_('Site') ) role_id = DynamicModelMultipleChoiceField( queryset=DeviceRole.objects.all(),