1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into feature

This commit is contained in:
jeremystretch
2021-06-22 14:30:47 -04:00
10 changed files with 66 additions and 39 deletions

View File

@ -1,5 +1,20 @@
# NetBox v2.11 # 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) ## v2.11.7 (2021-06-16)
### Enhancements ### Enhancements

View File

@ -1171,6 +1171,8 @@ class DeviceRoleView(generic.ObjectView):
return { return {
'devices_table': devices_table, 'devices_table': devices_table,
'device_count': Device.objects.filter(device_role=instance).count(),
'virtualmachine_count': VirtualMachine.objects.filter(role=instance).count(),
} }

View File

@ -280,8 +280,10 @@ class CustomField(BigIDModel):
if value not in [None, '']: if value not in [None, '']:
# Validate text field # Validate text field
if self.type == CustomFieldTypeChoices.TYPE_TEXT and self.validation_regex: if self.type == CustomFieldTypeChoices.TYPE_TEXT:
if not re.match(self.validation_regex, value): 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}'") raise ValidationError(f"Value must match regex '{self.validation_regex}'")
# Validate integer # Validate integer

View File

@ -65,7 +65,7 @@ VLAN_LINK = """
{% if record.pk %} {% if record.pk %}
<a href="{{ record.get_absolute_url }}">{{ record.vid }}</a> <a href="{{ record.get_absolute_url }}">{{ record.vid }}</a>
{% elif perms.ipam.add_vlan %} {% elif perms.ipam.add_vlan %}
<a href="{% url 'ipam:vlan_add' %}?vid={{ record.vid }}&group={{ vlan_group.pk }}{% if vlan_group.site %}&site={{ vlan_group.site.pk }}{% endif %}" class="btn btn-sm btn-success">{{ record.available }} VLAN{{ record.available|pluralize }} available</a> <a href="{% url 'ipam:vlan_add' %}?vid={{ record.vid }}{% if record.vlan_group %}&group={{ record.vlan_group.pk }}{% endif %}" class="btn btn-sm btn-success">{{ record.available }} VLAN{{ record.available|pluralize }} available</a>
{% else %} {% else %}
{{ record.available }} VLAN{{ record.available|pluralize }} available {{ record.available }} VLAN{{ record.available|pluralize }} available
{% endif %} {% endif %}

View File

@ -68,24 +68,40 @@ def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
return output 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 Create fake records for all gaps between used VLANs
""" """
if not 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 prev_vid = VLAN_VID_MAX
new_vlans = [] new_vlans = []
for vlan in vlans: for vlan in vlans:
if vlan.vid - prev_vid > 1: 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 prev_vid = vlan.vid
if vlans[0].vid > VLAN_VID_MIN: 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: 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 = list(vlans) + new_vlans
vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid']) vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])

View File

@ -145,7 +145,6 @@ class RIRListView(generic.ObjectListView):
filterset = filtersets.RIRFilterSet filterset = filtersets.RIRFilterSet
filterset_form = forms.RIRFilterForm filterset_form = forms.RIRFilterForm
table = tables.RIRTable table = tables.RIRTable
template_name = 'ipam/rir_list.html'
class RIRView(generic.ObjectView): class RIRView(generic.ObjectView):
@ -676,7 +675,7 @@ class VLANGroupView(generic.ObjectView):
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)) Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user))
).order_by('vid') ).order_by('vid')
vlans_count = vlans.count() vlans_count = vlans.count()
vlans = add_available_vlans(instance, vlans) vlans = add_available_vlans(vlans, vlan_group=instance)
vlans_table = tables.VLANDetailTable(vlans) vlans_table = tables.VLANDetailTable(vlans)
if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'): if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):

View File

@ -1218,7 +1218,7 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
component_form = self.model_form(component_data) component_form = self.model_form(component_data)
if component_form.is_valid(): if component_form.is_valid():
instance = component_form.save() 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) new_components.append(instance)
else: else:
for field, errors in component_form.errors.as_data().items(): for field, errors in component_form.errors.as_data().items():

View File

@ -43,7 +43,17 @@
<tr> <tr>
<th scope="row">Devices</th> <th scope="row">Devices</th>
<td> <td>
<a href="{% url 'dcim:device_list' %}?role_id={{ object.pk }}">{{ devices_table.rows|length }}</a> <a href="{% url 'dcim:device_list' %}?role_id={{ object.pk }}">{{ device_count }}</a>
</td>
</tr>
<tr>
<td>Virtual Machines</td>
<td>
{% if object.vm_role %}
<a href="{% url 'virtualization:virtualmachine_list' %}?role_id={{ object.pk }}">{{ virtualmachine_count }}</a>
{% else %}
&mdash;
{% endif %}
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -1,23 +0,0 @@
{% extends 'generic/object_list.html' %}
{% block extra_controls %}
{% if request.GET.family == '6' %}
<a href="{% url 'ipam:rir_list' %}" class="btn btn-sm btn-outline-secondary m-1">
<span class="mdi mdi-table" aria-hidden="true"></span>
IPv4 Stats
</a>
{% else %}
<a href="{% url 'ipam:rir_list' %}?family=6{% if request.GET %}&{{ request.GET.urlencode }}{% endif %}" class="btn btn-sm btn-outline-secondary m-1">
<span class="mdi mdi-table" aria-hidden="true"></span>
IPv6 Stats
</a>
{% endif %}
{% endblock %}
{% block sidebar %}
{% if request.GET.family == '6' %}
<div class="alert alert-info small">
<i class="mdi mdi-information-outline"></i> Numbers shown indicate /64 prefixes.
</div>
{% endif %}
{% endblock %}

View File

@ -531,8 +531,8 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = VirtualMachine model = VirtualMachine
field_order = [ field_order = [
'cluster_group_id', 'cluster_type_id', 'cluster_id', 'status', 'role_id', 'region_id', 'site_id', 'q', 'cluster_group_id', 'cluster_type_id', 'cluster_id', 'status', 'role_id', 'region_id', 'site_group_id',
'tenant_group_id', 'tenant_id', 'platform_id', 'mac_address', 'site_id', 'tenant_group_id', 'tenant_id', 'platform_id', 'mac_address',
] ]
field_groups = [ field_groups = [
['status', 'role_id'], ['status', 'role_id'],
@ -564,14 +564,20 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
required=False, required=False,
label=_('Region') label=_('Region')
) )
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField( site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
null_option='None', null_option='None',
query_params={ query_params={
'region_id': '$region_id' 'region_id': '$region_id',
'group_id': '$site_group_id',
}, },
label=_('Cluster') label=_('Site')
) )
role_id = DynamicModelMultipleChoiceField( role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),