mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Merge branch 'feature' into 12237-django-4.2
This commit is contained in:
commit
4d996dc833
@ -61,6 +61,10 @@ If installed in a rack, this field indicates the base rack unit in which the dev
|
|||||||
!!! tip
|
!!! tip
|
||||||
Devices with a height of more than one rack unit should be set to the lowest-numbered rack unit that they occupy.
|
Devices with a height of more than one rack unit should be set to the lowest-numbered rack unit that they occupy.
|
||||||
|
|
||||||
|
### Latitude & Longitude
|
||||||
|
|
||||||
|
GPS coordinates of the device for geolocation.
|
||||||
|
|
||||||
### Status
|
### Status
|
||||||
|
|
||||||
The device's operational status.
|
The device's operational status.
|
||||||
|
@ -165,19 +165,6 @@ In addition to the [form fields provided by Django](https://docs.djangoproject.c
|
|||||||
options:
|
options:
|
||||||
members: false
|
members: false
|
||||||
|
|
||||||
## Choice Fields
|
|
||||||
|
|
||||||
!!! warning "Obsolete Fields"
|
|
||||||
NetBox's custom `ChoiceField` and `MultipleChoiceField` classes are no longer necessary thanks to improvements made to the user interface. Django's native form fields can be used instead. These custom field classes will be removed in NetBox v3.6.
|
|
||||||
|
|
||||||
::: utilities.forms.fields.ChoiceField
|
|
||||||
options:
|
|
||||||
members: false
|
|
||||||
|
|
||||||
::: utilities.forms.fields.MultipleChoiceField
|
|
||||||
options:
|
|
||||||
members: false
|
|
||||||
|
|
||||||
## Dynamic Object Fields
|
## Dynamic Object Fields
|
||||||
|
|
||||||
::: utilities.forms.fields.DynamicModelChoiceField
|
::: utilities.forms.fields.DynamicModelChoiceField
|
||||||
|
12
docs/release-notes/version-3.6.md
Normal file
12
docs/release-notes/version-3.6.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# NetBox v3.6
|
||||||
|
|
||||||
|
## v3.6.0 (FUTURE)
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
* The `napalm_driver` and `napalm_args` fields (which were deprecated in v3.5) have been removed from the platform model.
|
||||||
|
|
||||||
|
### Other Changes
|
||||||
|
|
||||||
|
* [#11766](https://github.com/netbox-community/netbox/issues/11766) - Remove obsolete custom `ChoiceField` and `MultipleChoiceField` classes
|
||||||
|
* [#12320](https://github.com/netbox-community/netbox/issues/12320) - Remove obsolete fields `napalm_driver` and `napalm_args` from Platform
|
@ -273,6 +273,7 @@ nav:
|
|||||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||||
- Release Notes:
|
- Release Notes:
|
||||||
- Summary: 'release-notes/index.md'
|
- Summary: 'release-notes/index.md'
|
||||||
|
- Version 3.6: 'release-notes/version-3.6.md'
|
||||||
- Version 3.5: 'release-notes/version-3.5.md'
|
- Version 3.5: 'release-notes/version-3.5.md'
|
||||||
- Version 3.4: 'release-notes/version-3.4.md'
|
- Version 3.4: 'release-notes/version-3.4.md'
|
||||||
- Version 3.3: 'release-notes/version-3.3.md'
|
- Version 3.3: 'release-notes/version-3.3.md'
|
||||||
|
@ -200,6 +200,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
|||||||
|
|
||||||
# Emit the post_sync signal
|
# Emit the post_sync signal
|
||||||
post_sync.send(sender=self.__class__, instance=self)
|
post_sync.send(sender=self.__class__, instance=self)
|
||||||
|
sync.alters_data = True
|
||||||
|
|
||||||
def _walk(self, root):
|
def _walk(self, root):
|
||||||
"""
|
"""
|
||||||
|
@ -635,8 +635,8 @@ class PlatformSerializer(NetBoxModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args',
|
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
|
||||||
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -673,9 +673,10 @@ class DeviceSerializer(NetBoxModelSerializer):
|
|||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||||
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
|
'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
|
||||||
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
|
'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
|
||||||
'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
|
'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@extend_schema_field(NestedDeviceSerializer)
|
@extend_schema_field(NestedDeviceSerializer)
|
||||||
|
@ -811,7 +811,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
|
fields = ['id', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
||||||
@ -999,7 +999,7 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['id', 'asset_tag', 'face', 'position', 'airflow', 'vc_position', 'vc_priority']
|
fields = ['id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -472,10 +472,6 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
napalm_driver = forms.CharField(
|
|
||||||
max_length=50,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
config_template = DynamicModelChoiceField(
|
config_template = DynamicModelChoiceField(
|
||||||
queryset=ConfigTemplate.objects.all(),
|
queryset=ConfigTemplate.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
@ -487,9 +483,9 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
model = Platform
|
model = Platform
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('manufacturer', 'config_template', 'napalm_driver', 'description')),
|
(None, ('manufacturer', 'config_template', 'description')),
|
||||||
)
|
)
|
||||||
nullable_fields = ('manufacturer', 'config_template', 'napalm_driver', 'description')
|
nullable_fields = ('manufacturer', 'config_template', 'description')
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
|
@ -365,7 +365,7 @@ class PlatformImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -478,8 +478,9 @@ class DeviceImportForm(BaseDeviceImportForm):
|
|||||||
class Meta(BaseDeviceImportForm.Meta):
|
class Meta(BaseDeviceImportForm.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
'site', 'location', 'rack', 'position', 'face', 'parent', 'device_bay', 'airflow', 'virtual_chassis',
|
'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent', 'device_bay', 'airflow',
|
||||||
'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'comments', 'tags',
|
'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'comments',
|
||||||
|
'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
|
@ -360,19 +360,14 @@ class PlatformForm(NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Platform', (
|
('Platform', ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')),
|
||||||
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
|
||||||
'napalm_args': forms.Textarea(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceForm(TenancyForm, NetBoxModelForm):
|
class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||||
@ -454,9 +449,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
|||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
|
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
|
||||||
'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster', 'tenant_group', 'tenant',
|
'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster',
|
||||||
'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'tags',
|
'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
|
||||||
'local_context_data'
|
'comments', 'tags', 'local_context_data'
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
19
netbox/dcim/migrations/0173_remove_napalm_fields.py
Normal file
19
netbox/dcim/migrations/0173_remove_napalm_fields.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0172_larger_power_draw_values'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='platform',
|
||||||
|
name='napalm_args',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='platform',
|
||||||
|
name='napalm_driver',
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 4.1.9 on 2023-05-31 22:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0173_remove_napalm_fields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='latitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='longitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -359,6 +359,7 @@ class CableTermination(ChangeLoggedModel):
|
|||||||
# Circuit terminations
|
# Circuit terminations
|
||||||
elif getattr(self.termination, 'site', None):
|
elif getattr(self.termination, 'site', None):
|
||||||
self._site = self.termination.site
|
self._site = self.termination.site
|
||||||
|
cache_related_objects.alters_data = True
|
||||||
|
|
||||||
def to_objectchange(self, action):
|
def to_objectchange(self, action):
|
||||||
objectchange = super().to_objectchange(action)
|
objectchange = super().to_objectchange(action)
|
||||||
@ -637,6 +638,7 @@ class CablePath(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
else:
|
else:
|
||||||
self.delete()
|
self.delete()
|
||||||
|
retrace.alters_data = True
|
||||||
|
|
||||||
def _get_path(self):
|
def _get_path(self):
|
||||||
"""
|
"""
|
||||||
|
@ -213,6 +213,7 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
|||||||
type=self.type,
|
type=self.type,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
instantiate.do_not_call_in_templates = True
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
return {
|
return {
|
||||||
@ -256,6 +257,7 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
|||||||
allocated_draw=self.allocated_draw,
|
allocated_draw=self.allocated_draw,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
instantiate.do_not_call_in_templates = True
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
@ -330,6 +332,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
|||||||
feed_leg=self.feed_leg,
|
feed_leg=self.feed_leg,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
instantiate.do_not_call_in_templates = True
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
return {
|
return {
|
||||||
@ -413,6 +416,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
|||||||
poe_type=self.poe_type,
|
poe_type=self.poe_type,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
instantiate.do_not_call_in_templates = True
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
return {
|
return {
|
||||||
@ -507,6 +511,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
rear_port_position=self.rear_port_position,
|
rear_port_position=self.rear_port_position,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
instantiate.do_not_call_in_templates = True
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
return {
|
return {
|
||||||
@ -550,6 +555,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
positions=self.positions,
|
positions=self.positions,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
instantiate.do_not_call_in_templates = True
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
return {
|
return {
|
||||||
@ -581,6 +587,7 @@ class ModuleBayTemplate(ComponentTemplateModel):
|
|||||||
label=self.label,
|
label=self.label,
|
||||||
position=self.position
|
position=self.position
|
||||||
)
|
)
|
||||||
|
instantiate.do_not_call_in_templates = True
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
return {
|
return {
|
||||||
@ -603,6 +610,7 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
|||||||
name=self.name,
|
name=self.name,
|
||||||
label=self.label
|
label=self.label
|
||||||
)
|
)
|
||||||
|
instantiate.do_not_call_in_templates = True
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT:
|
if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT:
|
||||||
@ -696,3 +704,4 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
|||||||
part_id=self.part_id,
|
part_id=self.part_id,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
instantiate.do_not_call_in_templates = True
|
||||||
|
@ -432,9 +432,8 @@ class DeviceRole(OrganizationalModel):
|
|||||||
|
|
||||||
class Platform(OrganizationalModel):
|
class Platform(OrganizationalModel):
|
||||||
"""
|
"""
|
||||||
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos". A
|
||||||
NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
|
Platform may optionally be associated with a particular Manufacturer.
|
||||||
specifying a NAPALM driver.
|
|
||||||
"""
|
"""
|
||||||
manufacturer = models.ForeignKey(
|
manufacturer = models.ForeignKey(
|
||||||
to='dcim.Manufacturer',
|
to='dcim.Manufacturer',
|
||||||
@ -451,18 +450,6 @@ class Platform(OrganizationalModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
napalm_driver = models.CharField(
|
|
||||||
max_length=50,
|
|
||||||
blank=True,
|
|
||||||
verbose_name='NAPALM driver',
|
|
||||||
help_text=_('The name of the NAPALM driver to use when interacting with devices')
|
|
||||||
)
|
|
||||||
napalm_args = models.JSONField(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name='NAPALM arguments',
|
|
||||||
help_text=_('Additional arguments to pass when initiating the NAPALM driver (JSON format)')
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:platform', args=[self.pk])
|
return reverse('dcim:platform', args=[self.pk])
|
||||||
@ -637,6 +624,20 @@ class Device(PrimaryModel, ConfigContextModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
latitude = models.DecimalField(
|
||||||
|
max_digits=8,
|
||||||
|
decimal_places=6,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
|
||||||
|
)
|
||||||
|
longitude = models.DecimalField(
|
||||||
|
max_digits=9,
|
||||||
|
decimal_places=6,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
|
||||||
|
)
|
||||||
|
|
||||||
# Generic relations
|
# Generic relations
|
||||||
contacts = GenericRelation(
|
contacts = GenericRelation(
|
||||||
|
@ -172,7 +172,6 @@ class PlatformIndex(SearchIndex):
|
|||||||
fields = (
|
fields = (
|
||||||
('name', 100),
|
('name', 100),
|
||||||
('slug', 110),
|
('slug', 110),
|
||||||
('napalm_driver', 300),
|
|
||||||
('description', 500),
|
('description', 500),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -137,11 +137,11 @@ class PlatformTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = models.Platform
|
model = models.Platform
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'napalm_driver',
|
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'description',
|
||||||
'napalm_args', 'description', 'tags', 'actions', 'created', 'last_updated',
|
'tags', 'actions', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description',
|
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'description',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -236,9 +236,9 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
|
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
|
||||||
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
|
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
|
||||||
'device_bay_position', 'position', 'face', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster',
|
'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'primary_ip', 'primary_ip4',
|
||||||
'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'contacts',
|
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
|
||||||
'tags', 'created', 'last_updated',
|
'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
|
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
|
||||||
|
@ -1515,9 +1515,9 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Manufacturer.objects.bulk_create(manufacturers)
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
platforms = (
|
platforms = (
|
||||||
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], napalm_driver='driver-1', description='A'),
|
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], description='A'),
|
||||||
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], napalm_driver='driver-2', description='B'),
|
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], description='B'),
|
||||||
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], napalm_driver='driver-3', description='C'),
|
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='C'),
|
||||||
)
|
)
|
||||||
Platform.objects.bulk_create(platforms)
|
Platform.objects.bulk_create(platforms)
|
||||||
|
|
||||||
@ -1533,10 +1533,6 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'description': ['A', 'B']}
|
params = {'description': ['A', 'B']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_napalm_driver(self):
|
|
||||||
params = {'napalm_driver': ['driver-1', 'driver-2']}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
|
|
||||||
def test_manufacturer(self):
|
def test_manufacturer(self):
|
||||||
manufacturers = Manufacturer.objects.all()[:2]
|
manufacturers = Manufacturer.objects.all()[:2]
|
||||||
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
||||||
@ -1642,9 +1638,9 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], location=locations[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}),
|
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], location=locations[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, latitude=10, longitude=10, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}),
|
||||||
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], location=locations[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, cluster=clusters[1]),
|
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], location=locations[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, latitude=20, longitude=20, status=DeviceStatusChoices.STATUS_STAGED, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, cluster=clusters[1]),
|
||||||
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], location=locations[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT, cluster=clusters[2]),
|
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], location=locations[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, latitude=30, longitude=30, status=DeviceStatusChoices.STATUS_FAILED, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT, cluster=clusters[2]),
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -1725,6 +1721,14 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'position': [1, 2]}
|
params = {'position': [1, 2]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_latitude(self):
|
||||||
|
params = {'latitude': [10, 20]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_longitude(self):
|
||||||
|
params = {'longitude': [10, 20]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_vc_position(self):
|
def test_vc_position(self):
|
||||||
params = {'vc_position': [1, 2]}
|
params = {'vc_position': [1, 2]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -1609,8 +1609,6 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
'name': 'Platform X',
|
'name': 'Platform X',
|
||||||
'slug': 'platform-x',
|
'slug': 'platform-x',
|
||||||
'manufacturer': manufacturer.pk,
|
'manufacturer': manufacturer.pk,
|
||||||
'napalm_driver': 'junos',
|
|
||||||
'napalm_args': None,
|
|
||||||
'description': 'A new platform',
|
'description': 'A new platform',
|
||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
@ -1630,7 +1628,6 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'napalm_driver': 'ios',
|
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1699,6 +1696,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'rack': racks[1].pk,
|
'rack': racks[1].pk,
|
||||||
'position': 1,
|
'position': 1,
|
||||||
'face': DeviceFaceChoices.FACE_FRONT,
|
'face': DeviceFaceChoices.FACE_FRONT,
|
||||||
|
'latitude': Decimal('35.780000'),
|
||||||
|
'longitude': Decimal('-78.642000'),
|
||||||
'status': DeviceStatusChoices.STATUS_PLANNED,
|
'status': DeviceStatusChoices.STATUS_PLANNED,
|
||||||
'primary_ip4': None,
|
'primary_ip4': None,
|
||||||
'primary_ip6': None,
|
'primary_ip6': None,
|
||||||
|
@ -146,6 +146,7 @@ class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
|
|||||||
Synchronize context data from the designated DataFile (if any).
|
Synchronize context data from the designated DataFile (if any).
|
||||||
"""
|
"""
|
||||||
self.data = self.data_file.get_data()
|
self.data = self.data_file.get_data()
|
||||||
|
sync_data.alters_data = True
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextModel(models.Model):
|
class ConfigContextModel(models.Model):
|
||||||
@ -236,6 +237,7 @@ class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLog
|
|||||||
Synchronize template content from the designated DataFile (if any).
|
Synchronize template content from the designated DataFile (if any).
|
||||||
"""
|
"""
|
||||||
self.template_code = self.data_file.data_as_string
|
self.template_code = self.data_file.data_as_string
|
||||||
|
sync_data.alters_data = True
|
||||||
|
|
||||||
def render(self, context=None):
|
def render(self, context=None):
|
||||||
"""
|
"""
|
||||||
|
@ -362,6 +362,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
|
|||||||
Synchronize template content from the designated DataFile (if any).
|
Synchronize template content from the designated DataFile (if any).
|
||||||
"""
|
"""
|
||||||
self.template_code = self.data_file.data_as_string
|
self.template_code = self.data_file.data_as_string
|
||||||
|
sync_data.alters_data = True
|
||||||
|
|
||||||
def render(self, queryset):
|
def render(self, queryset):
|
||||||
"""
|
"""
|
||||||
@ -625,6 +626,7 @@ class ConfigRevision(models.Model):
|
|||||||
"""
|
"""
|
||||||
cache.set('config', self.data, None)
|
cache.set('config', self.data, None)
|
||||||
cache.set('config_version', self.pk, None)
|
cache.set('config_version', self.pk, None)
|
||||||
|
activate.alters_data = True
|
||||||
|
|
||||||
@admin.display(boolean=True)
|
@admin.display(boolean=True)
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
|
@ -112,3 +112,4 @@ class StagedChange(ChangeLoggedModel):
|
|||||||
instance = self.model.objects.get(pk=self.object_id)
|
instance = self.model.objects.get(pk=self.object_id)
|
||||||
logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')
|
logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')
|
||||||
instance.delete()
|
instance.delete()
|
||||||
|
apply.alters_data = True
|
||||||
|
@ -71,6 +71,7 @@ class ChangeLoggingMixin(models.Model):
|
|||||||
`_prechange_snapshot` on the instance.
|
`_prechange_snapshot` on the instance.
|
||||||
"""
|
"""
|
||||||
self._prechange_snapshot = self.serialize_object()
|
self._prechange_snapshot = self.serialize_object()
|
||||||
|
snapshot.alters_data = True
|
||||||
|
|
||||||
def to_objectchange(self, action):
|
def to_objectchange(self, action):
|
||||||
"""
|
"""
|
||||||
@ -244,6 +245,7 @@ class CustomFieldsMixin(models.Model):
|
|||||||
"""
|
"""
|
||||||
for cf in self.custom_fields:
|
for cf in self.custom_fields:
|
||||||
self.custom_field_data[cf.name] = cf.default
|
self.custom_field_data[cf.name] = cf.default
|
||||||
|
populate_custom_field_defaults.alters_data = True
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
@ -419,6 +421,7 @@ class SyncedDataMixin(models.Model):
|
|||||||
self.data_synced = None
|
self.data_synced = None
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
clean.alters_data = True
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
from core.models import AutoSyncRecord
|
from core.models import AutoSyncRecord
|
||||||
@ -466,6 +469,7 @@ class SyncedDataMixin(models.Model):
|
|||||||
self.data_synced = timezone.now()
|
self.data_synced = timezone.now()
|
||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
sync.alters_data = True
|
||||||
|
|
||||||
def sync_data(self):
|
def sync_data(self):
|
||||||
"""
|
"""
|
||||||
|
@ -76,6 +76,23 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">GPS Coordinates</th>
|
||||||
|
<td class="position-relative">
|
||||||
|
{% if object.latitude and object.longitude %}
|
||||||
|
{% if config.MAPS_URL %}
|
||||||
|
<div class="position-absolute top-50 end-0 translate-middle-y noprint">
|
||||||
|
<a href="{{ config.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-sm">
|
||||||
|
<i class="mdi mdi-map-marker"></i> Map It
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<span>{{ object.latitude }}, {{ object.longitude }}</span>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Tenant</th>
|
<th scope="row">Tenant</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -53,6 +53,8 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% render_field form.face %}
|
{% render_field form.face %}
|
||||||
{% render_field form.position %}
|
{% render_field form.position %}
|
||||||
|
{% render_field form.latitude %}
|
||||||
|
{% render_field form.longitude %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -53,26 +53,11 @@
|
|||||||
title="This field has been deprecated, and will be removed in NetBox v3.6."
|
title="This field has been deprecated, and will be removed in NetBox v3.6."
|
||||||
></i>
|
></i>
|
||||||
</th>
|
</th>
|
||||||
<td>{{ object.napalm_driver|placeholder }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
<div class="card">
|
|
||||||
<h5 class="card-header">
|
|
||||||
NAPALM Arguments
|
|
||||||
<i
|
|
||||||
class="mdi mdi-alert-box text-warning"
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-placement="right"
|
|
||||||
title="This field has been deprecated, and will be removed in NetBox v3.6."
|
|
||||||
></i>
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
<pre>{{ object.napalm_args|json }}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
|
@ -11,13 +11,11 @@ from utilities.forms import widgets
|
|||||||
from utilities.validators import EnhancedURLValidator
|
from utilities.validators import EnhancedURLValidator
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ChoiceField',
|
|
||||||
'ColorField',
|
'ColorField',
|
||||||
'CommentField',
|
'CommentField',
|
||||||
'JSONField',
|
'JSONField',
|
||||||
'LaxURLField',
|
'LaxURLField',
|
||||||
'MACAddressField',
|
'MACAddressField',
|
||||||
'MultipleChoiceField',
|
|
||||||
'SlugField',
|
'SlugField',
|
||||||
'TagFilterField',
|
'TagFilterField',
|
||||||
)
|
)
|
||||||
@ -128,24 +126,3 @@ class MACAddressField(forms.Field):
|
|||||||
raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
|
raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Choice fields
|
|
||||||
#
|
|
||||||
|
|
||||||
class ChoiceField(forms.ChoiceField):
|
|
||||||
"""
|
|
||||||
Previously used to override Django's built-in `ChoiceField` to use NetBox's now-obsolete `StaticSelect` widget.
|
|
||||||
"""
|
|
||||||
# TODO: Remove in v3.6
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MultipleChoiceField(forms.MultipleChoiceField):
|
|
||||||
"""
|
|
||||||
Previously used to override Django's built-in `MultipleChoiceField` to use NetBox's now-obsolete
|
|
||||||
`StaticSelectMultiple` widget.
|
|
||||||
"""
|
|
||||||
# TODO: Remove in v3.6
|
|
||||||
pass
|
|
||||||
|
Loading…
Reference in New Issue
Block a user