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:
@ -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
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 %}
|
||||||
|
@ -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'])
|
||||||
|
@ -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'):
|
||||||
|
@ -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():
|
||||||
|
@ -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 %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -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 %}
|
|
@ -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(),
|
||||||
|
Reference in New Issue
Block a user