diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md
index c36344912..5e538890c 100644
--- a/docs/release-notes/version-3.2.md
+++ b/docs/release-notes/version-3.2.md
@@ -2,6 +2,23 @@
## v3.2.8 (FUTURE)
+### Enhancements
+
+* [#9062](https://github.com/netbox-community/netbox/issues/9062) - Add/edit {module} substitution to help text for component template name
+* [#9637](https://github.com/netbox-community/netbox/issues/9637) - Add site group field to rack reservation form
+* [#9762](https://github.com/netbox-community/netbox/issues/9762) - Add `nat_outside` column to the IPAddress table
+* [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table
+* [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values
+* [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table
+* [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table
+
+### Bug Fixes
+
+* [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments
+* [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init
+* [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk
+* [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization
+
---
## v3.2.7 (2022-07-20)
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index c5474a2b1..16ff6fee2 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -291,7 +291,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'tag')),
('User', ('user_id',)),
- ('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id')),
+ ('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
region_id = DynamicModelMultipleChoiceField(
@@ -299,25 +299,38 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
required=False,
label=_('Region')
)
- site_id = DynamicModelMultipleChoiceField(
- queryset=Site.objects.all(),
- required=False,
- query_params={
- 'region_id': '$region_id'
- },
- label=_('Site')
- )
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
- location_id = DynamicModelMultipleChoiceField(
- queryset=Location.objects.prefetch_related('site'),
+ site_id = DynamicModelMultipleChoiceField(
+ queryset=Site.objects.all(),
required=False,
+ query_params={
+ 'region_id': '$region_id',
+ 'group_id': '$site_group_id',
+ },
+ label=_('Site')
+ )
+ location_id = DynamicModelMultipleChoiceField(
+ queryset=Location.objects.all(),
+ required=False,
+ query_params={
+ 'site_id': '$site_id',
+ },
label=_('Location'),
null_option='None'
)
+ rack_id = DynamicModelMultipleChoiceField(
+ queryset=Rack.objects.all(),
+ required=False,
+ query_params={
+ 'site_id': '$site_id',
+ 'location_id': '$location_id',
+ },
+ label=_('Rack')
+ )
user_id = DynamicModelMultipleChoiceField(
queryset=User.objects.all(),
required=False,
diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py
index f3ab6f3a9..edf25cf2c 100644
--- a/netbox/dcim/forms/models.py
+++ b/netbox/dcim/forms/models.py
@@ -325,7 +325,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
)
fieldsets = (
- ('Reservation', ('region', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
+ ('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
)
diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py
index 8c9ddab19..d2c941b34 100644
--- a/netbox/dcim/forms/object_create.py
+++ b/netbox/dcim/forms/object_create.py
@@ -64,6 +64,14 @@ class ModularComponentTemplateCreateForm(ComponentCreateForm):
"""
Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType.
"""
+ name_pattern = ExpandableNameField(
+ label='Name',
+ help_text="""
+ Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
+ are not supported. Example: [ge,xe]-0/0/[0-9]
. {module} is accepted as a substitution for
+ the module bay position.
+ """
+ )
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(),
required=False
diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py
index 4a66bc457..3fc1d4e61 100644
--- a/netbox/dcim/models/device_component_templates.py
+++ b/netbox/dcim/models/device_component_templates.py
@@ -39,7 +39,10 @@ class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel):
related_name='%(class)ss'
)
name = models.CharField(
- max_length=64
+ max_length=64,
+ help_text="""
+ {module} is accepted as a substitution for the module bay position when attached to a module type.
+ """
)
_name = NaturalOrderingField(
target_field='name',
@@ -157,6 +160,14 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
**kwargs
)
+ def to_yaml(self):
+ return {
+ 'name': self.name,
+ 'type': self.type,
+ 'label': self.label,
+ 'description': self.description,
+ }
+
class ConsoleServerPortTemplate(ModularComponentTemplateModel):
"""
@@ -185,6 +196,14 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
**kwargs
)
+ def to_yaml(self):
+ return {
+ 'name': self.name,
+ 'type': self.type,
+ 'label': self.label,
+ 'description': self.description,
+ }
+
class PowerPortTemplate(ModularComponentTemplateModel):
"""
@@ -236,6 +255,16 @@ class PowerPortTemplate(ModularComponentTemplateModel):
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
})
+ def to_yaml(self):
+ return {
+ 'name': self.name,
+ 'type': self.type,
+ 'maximum_draw': self.maximum_draw,
+ 'allocated_draw': self.allocated_draw,
+ 'label': self.label,
+ 'description': self.description,
+ }
+
class PowerOutletTemplate(ModularComponentTemplateModel):
"""
@@ -298,6 +327,16 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
**kwargs
)
+ def to_yaml(self):
+ return {
+ 'name': self.name,
+ 'type': self.type,
+ 'power_port': self.power_port.name if self.power_port else None,
+ 'feed_leg': self.feed_leg,
+ 'label': self.label,
+ 'description': self.description,
+ }
+
class InterfaceTemplate(ModularComponentTemplateModel):
"""
@@ -351,6 +390,17 @@ class InterfaceTemplate(ModularComponentTemplateModel):
**kwargs
)
+ def to_yaml(self):
+ return {
+ 'name': self.name,
+ 'type': self.type,
+ 'mgmt_only': self.mgmt_only,
+ 'label': self.label,
+ 'description': self.description,
+ 'poe_mode': self.poe_mode,
+ 'poe_type': self.poe_type,
+ }
+
class FrontPortTemplate(ModularComponentTemplateModel):
"""
@@ -424,6 +474,16 @@ class FrontPortTemplate(ModularComponentTemplateModel):
**kwargs
)
+ def to_yaml(self):
+ return {
+ 'name': self.name,
+ 'type': self.type,
+ 'rear_port': self.rear_port.name,
+ 'rear_port_position': self.rear_port_position,
+ 'label': self.label,
+ 'description': self.description,
+ }
+
class RearPortTemplate(ModularComponentTemplateModel):
"""
@@ -463,6 +523,15 @@ class RearPortTemplate(ModularComponentTemplateModel):
**kwargs
)
+ def to_yaml(self):
+ return {
+ 'name': self.name,
+ 'type': self.type,
+ 'positions': self.positions,
+ 'label': self.label,
+ 'description': self.description,
+ }
+
class ModuleBayTemplate(ComponentTemplateModel):
"""
@@ -488,6 +557,14 @@ class ModuleBayTemplate(ComponentTemplateModel):
position=self.position
)
+ def to_yaml(self):
+ return {
+ 'name': self.name,
+ 'label': self.label,
+ 'position': self.position,
+ 'description': self.description,
+ }
+
class DeviceBayTemplate(ComponentTemplateModel):
"""
@@ -512,6 +589,13 @@ class DeviceBayTemplate(ComponentTemplateModel):
f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
)
+ def to_yaml(self):
+ return {
+ 'name': self.name,
+ 'label': self.label,
+ 'description': self.description,
+ }
+
class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
"""
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index f8a28eb58..f21176d8d 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -1,5 +1,4 @@
import decimal
-from collections import OrderedDict
import yaml
from django.contrib.contenttypes.fields import GenericRelation
@@ -164,117 +163,54 @@ class DeviceType(NetBoxModel):
return reverse('dcim:devicetype', args=[self.pk])
def to_yaml(self):
- data = OrderedDict((
- ('manufacturer', self.manufacturer.name),
- ('model', self.model),
- ('slug', self.slug),
- ('part_number', self.part_number),
- ('u_height', float(self.u_height)),
- ('is_full_depth', self.is_full_depth),
- ('subdevice_role', self.subdevice_role),
- ('airflow', self.airflow),
- ('comments', self.comments),
- ))
+ data = {
+ 'manufacturer': self.manufacturer.name,
+ 'model': self.model,
+ 'slug': self.slug,
+ 'part_number': self.part_number,
+ 'u_height': float(self.u_height),
+ 'is_full_depth': self.is_full_depth,
+ 'subdevice_role': self.subdevice_role,
+ 'airflow': self.airflow,
+ 'comments': self.comments,
+ }
# Component templates
if self.consoleporttemplates.exists():
data['console-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.consoleporttemplates.all()
+ c.to_yaml() for c in self.consoleporttemplates.all()
]
if self.consoleserverporttemplates.exists():
data['console-server-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.consoleserverporttemplates.all()
+ c.to_yaml() for c in self.consoleserverporttemplates.all()
]
if self.powerporttemplates.exists():
data['power-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'maximum_draw': c.maximum_draw,
- 'allocated_draw': c.allocated_draw,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.powerporttemplates.all()
+ c.to_yaml() for c in self.powerporttemplates.all()
]
if self.poweroutlettemplates.exists():
data['power-outlets'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'power_port': c.power_port.name if c.power_port else None,
- 'feed_leg': c.feed_leg,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.poweroutlettemplates.all()
+ c.to_yaml() for c in self.poweroutlettemplates.all()
]
if self.interfacetemplates.exists():
data['interfaces'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'mgmt_only': c.mgmt_only,
- 'label': c.label,
- 'description': c.description,
- 'poe_mode': c.poe_mode,
- 'poe_type': c.poe_type,
- }
- for c in self.interfacetemplates.all()
+ c.to_yaml() for c in self.interfacetemplates.all()
]
if self.frontporttemplates.exists():
data['front-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'rear_port': c.rear_port.name,
- 'rear_port_position': c.rear_port_position,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.frontporttemplates.all()
+ c.to_yaml() for c in self.frontporttemplates.all()
]
if self.rearporttemplates.exists():
data['rear-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'positions': c.positions,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.rearporttemplates.all()
+ c.to_yaml() for c in self.rearporttemplates.all()
]
if self.modulebaytemplates.exists():
data['module-bays'] = [
- {
- 'name': c.name,
- 'label': c.label,
- 'position': c.position,
- 'description': c.description,
- }
- for c in self.modulebaytemplates.all()
+ c.to_yaml() for c in self.modulebaytemplates.all()
]
if self.devicebaytemplates.exists():
data['device-bays'] = [
- {
- 'name': c.name,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.devicebaytemplates.all()
+ c.to_yaml() for c in self.devicebaytemplates.all()
]
return yaml.dump(dict(data), sort_keys=False)
@@ -406,91 +342,41 @@ class ModuleType(NetBoxModel):
return reverse('dcim:moduletype', args=[self.pk])
def to_yaml(self):
- data = OrderedDict((
- ('manufacturer', self.manufacturer.name),
- ('model', self.model),
- ('part_number', self.part_number),
- ('comments', self.comments),
- ))
+ data = {
+ 'manufacturer': self.manufacturer.name,
+ 'model': self.model,
+ 'part_number': self.part_number,
+ 'comments': self.comments,
+ }
# Component templates
if self.consoleporttemplates.exists():
data['console-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.consoleporttemplates.all()
+ c.to_yaml() for c in self.consoleporttemplates.all()
]
if self.consoleserverporttemplates.exists():
data['console-server-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.consoleserverporttemplates.all()
+ c.to_yaml() for c in self.consoleserverporttemplates.all()
]
if self.powerporttemplates.exists():
data['power-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'maximum_draw': c.maximum_draw,
- 'allocated_draw': c.allocated_draw,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.powerporttemplates.all()
+ c.to_yaml() for c in self.powerporttemplates.all()
]
if self.poweroutlettemplates.exists():
data['power-outlets'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'power_port': c.power_port.name if c.power_port else None,
- 'feed_leg': c.feed_leg,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.poweroutlettemplates.all()
+ c.to_yaml() for c in self.poweroutlettemplates.all()
]
if self.interfacetemplates.exists():
data['interfaces'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'mgmt_only': c.mgmt_only,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.interfacetemplates.all()
+ c.to_yaml() for c in self.interfacetemplates.all()
]
if self.frontporttemplates.exists():
data['front-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'rear_port': c.rear_port.name,
- 'rear_port_position': c.rear_port_position,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.frontporttemplates.all()
+ c.to_yaml() for c in self.frontporttemplates.all()
]
if self.rearporttemplates.exists():
data['rear-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'positions': c.positions,
- 'label': c.label,
- 'description': c.description,
- }
- for c in self.rearporttemplates.all()
+ c.to_yaml() for c in self.rearporttemplates.all()
]
return yaml.dump(dict(data), sort_keys=False)
diff --git a/netbox/dcim/tables/modules.py b/netbox/dcim/tables/modules.py
index 5b009e42e..e40d7bd80 100644
--- a/netbox/dcim/tables/modules.py
+++ b/netbox/dcim/tables/modules.py
@@ -14,6 +14,9 @@ class ModuleTypeTable(NetBoxTable):
linkify=True,
verbose_name='Module Type'
)
+ manufacturer = tables.Column(
+ linkify=True
+ )
instance_count = columns.LinkedCountColumn(
viewname='dcim:module_list',
url_params={'module_type_id': 'pk'},
@@ -41,6 +44,10 @@ class ModuleTable(NetBoxTable):
module_bay = tables.Column(
linkify=True
)
+ manufacturer = tables.Column(
+ accessor=tables.A('module_type__manufacturer'),
+ linkify=True
+ )
module_type = tables.Column(
linkify=True
)
@@ -52,8 +59,9 @@ class ModuleTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Module
fields = (
- 'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags',
+ 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'comments',
+ 'tags',
)
default_columns = (
- 'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag',
+ 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag',
)
diff --git a/netbox/dcim/tables/power.py b/netbox/dcim/tables/power.py
index 92c4bb0aa..6696d516a 100644
--- a/netbox/dcim/tables/power.py
+++ b/netbox/dcim/tables/power.py
@@ -21,6 +21,9 @@ class PowerPanelTable(NetBoxTable):
site = tables.Column(
linkify=True
)
+ location = tables.Column(
+ linkify=True
+ )
powerfeed_count = columns.LinkedCountColumn(
viewname='dcim:powerfeed_list',
url_params={'power_panel_id': 'pk'},
@@ -35,7 +38,9 @@ class PowerPanelTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = PowerPanel
- fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',)
+ fields = (
+ 'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',
+ )
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py
index 5412e2297..d83f25a5f 100644
--- a/netbox/dcim/tables/racks.py
+++ b/netbox/dcim/tables/racks.py
@@ -109,6 +109,10 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
accessor=Accessor('rack__site'),
linkify=True
)
+ location = tables.Column(
+ accessor=Accessor('rack__location'),
+ linkify=True
+ )
rack = tables.Column(
linkify=True
)
@@ -123,7 +127,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = RackReservation
fields = (
- 'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
+ 'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py
index d97823e7c..0e02b0de5 100644
--- a/netbox/dcim/tests/test_models.py
+++ b/netbox/dcim/tests/test_models.py
@@ -163,8 +163,8 @@ class RackTestCase(TestCase):
}
self.assertEqual(rack1_inventory_front[10.0]['device'], device1)
self.assertEqual(rack1_inventory_front[10.5]['device'], device1)
- del(rack1_inventory_front[10.0])
- del(rack1_inventory_front[10.5])
+ del rack1_inventory_front[10.0]
+ del rack1_inventory_front[10.5]
for u in rack1_inventory_front.values():
self.assertIsNone(u['device'])
@@ -174,8 +174,8 @@ class RackTestCase(TestCase):
}
self.assertEqual(rack1_inventory_rear[10.0]['device'], device1)
self.assertEqual(rack1_inventory_rear[10.5]['device'], device1)
- del(rack1_inventory_rear[10.0])
- del(rack1_inventory_rear[10.5])
+ del rack1_inventory_rear[10.0]
+ del rack1_inventory_rear[10.5]
for u in rack1_inventory_rear.values():
self.assertIsNone(u['device'])
diff --git a/netbox/extras/forms/models.py b/netbox/extras/forms/models.py
index 1ef723e93..bea1fbcc1 100644
--- a/netbox/extras/forms/models.py
+++ b/netbox/extras/forms/models.py
@@ -136,6 +136,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
'http_method': StaticSelect(),
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
+ 'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
}
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index bbc66f279..156e02f74 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -181,7 +181,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
model = ct.model_class()
instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False})
for instance in instances:
- del(instance.custom_field_data[self.name])
+ del instance.custom_field_data[self.name]
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
def rename_object_data(self, old_name, new_name):
diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py
index 8dcb53b09..946999bc2 100644
--- a/netbox/extras/tests/test_customfields.py
+++ b/netbox/extras/tests/test_customfields.py
@@ -992,7 +992,7 @@ class CustomFieldModelTest(TestCase):
with self.assertRaises(ValidationError):
site.clean()
- del(site.cf['bar'])
+ del site.cf['bar']
site.clean()
def test_missing_required_field(self):
diff --git a/netbox/extras/tests/test_registry.py b/netbox/extras/tests/test_registry.py
index 53ba6584a..38a6b9f83 100644
--- a/netbox/extras/tests/test_registry.py
+++ b/netbox/extras/tests/test_registry.py
@@ -30,4 +30,4 @@ class RegistryTest(TestCase):
reg['foo'] = 123
with self.assertRaises(TypeError):
- del(reg['foo'])
+ del reg['foo']
diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py
index 0a22cbc21..34bf739f4 100644
--- a/netbox/ipam/forms/models.py
+++ b/netbox/ipam/forms/models.py
@@ -851,7 +851,7 @@ class ServiceCreateForm(ServiceForm):
# Fields which may be populated from a ServiceTemplate are not required
for field in ('name', 'protocol', 'ports'):
self.fields[field].required = False
- del(self.fields[field].widget.attrs['required'])
+ del self.fields[field].widget.attrs['required']
def clean(self):
if self.cleaned_data['service_template']:
diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py
index ee5de8cf4..9ad763920 100644
--- a/netbox/ipam/models/ip.py
+++ b/netbox/ipam/models/ip.py
@@ -373,7 +373,7 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
# Cache the original prefix and VRF so we can check if they have changed on post_save
self._prefix = self.prefix
- self._vrf = self.vrf
+ self._vrf_id = self.vrf_id
def __str__(self):
return str(self.prefix)
diff --git a/netbox/ipam/signals.py b/netbox/ipam/signals.py
index 3e8b86050..8555f5e67 100644
--- a/netbox/ipam/signals.py
+++ b/netbox/ipam/signals.py
@@ -30,14 +30,14 @@ def update_children_depth(prefix):
def handle_prefix_saved(instance, created, **kwargs):
# Prefix has changed (or new instance has been created)
- if created or instance.vrf != instance._vrf or instance.prefix != instance._prefix:
+ if created or instance.vrf_id != instance._vrf_id or instance.prefix != instance._prefix:
update_parents_children(instance)
update_children_depth(instance)
# If this is not a new prefix, clean up parent/children of previous prefix
if not created:
- old_prefix = Prefix(vrf=instance._vrf, prefix=instance._prefix)
+ old_prefix = Prefix(vrf_id=instance._vrf_id, prefix=instance._prefix)
update_parents_children(old_prefix)
update_children_depth(old_prefix)
diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py
index bec05eeff..087d0de73 100644
--- a/netbox/ipam/tables/ip.py
+++ b/netbox/ipam/tables/ip.py
@@ -369,6 +369,11 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
orderable=False,
verbose_name='NAT (Inside)'
)
+ nat_outside = tables.Column(
+ linkify=True,
+ orderable=False,
+ verbose_name='NAT (Outside)'
+ )
assigned = columns.BooleanColumn(
accessor='assigned_object_id',
linkify=True,
@@ -381,7 +386,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = IPAddress
fields = (
- 'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'assigned', 'dns_name', 'description',
+ 'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside', 'assigned', 'dns_name', 'description',
'tags', 'created', 'last_updated',
)
default_columns = (
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index 880ddb83b..a086ab66d 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -334,14 +334,18 @@ class AggregateBulkImportView(generic.BulkImportView):
class AggregateBulkEditView(generic.BulkEditView):
- queryset = Aggregate.objects.all()
+ queryset = Aggregate.objects.annotate(
+ child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
+ )
filterset = filtersets.AggregateFilterSet
table = tables.AggregateTable
form = forms.AggregateBulkEditForm
class AggregateBulkDeleteView(generic.BulkDeleteView):
- queryset = Aggregate.objects.all()
+ queryset = Aggregate.objects.annotate(
+ child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
+ )
filterset = filtersets.AggregateFilterSet
table = tables.AggregateTable
diff --git a/netbox/utilities/templates/helpers/utilization_graph.html b/netbox/utilities/templates/helpers/utilization_graph.html
index e6829befc..967ac8a87 100644
--- a/netbox/utilities/templates/helpers/utilization_graph.html
+++ b/netbox/utilities/templates/helpers/utilization_graph.html
@@ -1,21 +1,15 @@
-{% if utilization == 0 %}
-