Standardize model types based on function

This commit is contained in:
Jeremy Stretch 2021-02-24 21:01:16 -05:00
parent 0a6ebdee48
commit bec7ea7072
31 changed files with 1569 additions and 579 deletions

View File

@ -0,0 +1,37 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0024_standardize_name_length'),
]
operations = [
migrations.AddField(
model_name='circuittype',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='circuit',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='circuittermination',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='circuittype',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='provider',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@ -4,8 +4,9 @@ from taggit.managers import TaggableManager
from dcim.fields import ASNField
from dcim.models import CableTermination, PathEndpoint
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.models import ObjectChange, TaggedItem
from extras.utils import extras_features
from netbox.models import BigIDModel, OrganizationalModel, PrimaryModel
from utilities.querysets import RestrictedQuerySet
from utilities.utils import serialize_object
from .choices import *
@ -21,7 +22,7 @@ __all__ = (
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Provider(ChangeLoggedModel, CustomFieldModel):
class Provider(PrimaryModel):
"""
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider.
@ -93,7 +94,8 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
)
class CircuitType(ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class CircuitType(OrganizationalModel):
"""
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band".
@ -133,7 +135,7 @@ class CircuitType(ChangeLoggedModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Circuit(ChangeLoggedModel, CustomFieldModel):
class Circuit(PrimaryModel):
"""
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
@ -233,7 +235,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
return self._get_termination('Z')
class CircuitTermination(PathEndpoint, CableTermination):
class CircuitTermination(BigIDModel, PathEndpoint, CableTermination):
circuit = models.ForeignKey(
to='circuits.Circuit',
on_delete=models.CASCADE,

View File

@ -0,0 +1,462 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0122_standardize_name_length'),
]
operations = [
migrations.AddField(
model_name='consoleport',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='consoleport',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='consoleport',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='consoleporttemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='consoleporttemplate',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='consoleporttemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='consoleserverport',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='consoleserverport',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='consoleserverport',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='consoleserverporttemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='consoleserverporttemplate',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='consoleserverporttemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='devicebay',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='devicebay',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='devicebay',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='devicebaytemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='devicebaytemplate',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='devicebaytemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='devicerole',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='frontport',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='frontport',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='frontport',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='frontporttemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='frontporttemplate',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='frontporttemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='interface',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='interface',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='interface',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='interfacetemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='interfacetemplate',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='interfacetemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='inventoryitem',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='inventoryitem',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='inventoryitem',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='manufacturer',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='platform',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='poweroutlet',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='poweroutlet',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='poweroutlet',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='poweroutlettemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='poweroutlettemplate',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='poweroutlettemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='powerport',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='powerport',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='powerport',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='powerporttemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='powerporttemplate',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='powerporttemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='rackgroup',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='rackrole',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='rearport',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='rearport',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='rearport',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='rearporttemplate',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='rearporttemplate',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='rearporttemplate',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='region',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='cable',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='cablepath',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='consoleport',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='consoleporttemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='consoleserverport',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='consoleserverporttemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='device',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='devicebay',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='devicebaytemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='devicerole',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='devicetype',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='frontport',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='frontporttemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='interface',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='interfacetemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='inventoryitem',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='manufacturer',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='platform',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='powerfeed',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='poweroutlet',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='powerpanel',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='powerport',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='powerporttemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rack',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rackgroup',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rackreservation',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rackrole',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rearport',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rearporttemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='region',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='site',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='virtualchassis',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@ -12,8 +12,9 @@ from dcim.choices import *
from dcim.constants import *
from dcim.fields import PathField
from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
from extras.models import TaggedItem
from extras.utils import extras_features
from netbox.models import BigIDModel, PrimaryModel
from utilities.fields import ColorField
from utilities.querysets import RestrictedQuerySet
from utilities.utils import to_meters
@ -32,7 +33,7 @@ __all__ = (
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Cable(ChangeLoggedModel, CustomFieldModel):
class Cable(PrimaryModel):
"""
A physical connection between two endpoints.
"""
@ -305,7 +306,7 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
class CablePath(models.Model):
class CablePath(BigIDModel):
"""
A CablePath instance represents the physical path from an origin to a destination, including all intermediate
elements in the path. Every instance must specify an `origin`, whereas `destination` may be null (for paths which do

View File

@ -5,6 +5,8 @@ from django.db import models
from dcim.choices import *
from dcim.constants import *
from extras.models import ObjectChange
from extras.utils import extras_features
from netbox.models import PrimaryModel
from utilities.fields import NaturalOrderingField
from utilities.querysets import RestrictedQuerySet
from utilities.ordering import naturalize_interface
@ -26,7 +28,7 @@ __all__ = (
)
class ComponentTemplateModel(models.Model):
class ComponentTemplateModel(PrimaryModel):
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
@ -82,6 +84,7 @@ class ComponentTemplateModel(models.Model):
)
@extras_features('custom_fields', 'export_templates', 'webhooks')
class ConsolePortTemplate(ComponentTemplateModel):
"""
A template for a ConsolePort to be created for a new Device.
@ -105,6 +108,7 @@ class ConsolePortTemplate(ComponentTemplateModel):
)
@extras_features('custom_fields', 'export_templates', 'webhooks')
class ConsoleServerPortTemplate(ComponentTemplateModel):
"""
A template for a ConsoleServerPort to be created for a new Device.
@ -128,6 +132,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
)
@extras_features('custom_fields', 'export_templates', 'webhooks')
class PowerPortTemplate(ComponentTemplateModel):
"""
A template for a PowerPort to be created for a new Device.
@ -174,6 +179,7 @@ class PowerPortTemplate(ComponentTemplateModel):
})
@extras_features('custom_fields', 'export_templates', 'webhooks')
class PowerOutletTemplate(ComponentTemplateModel):
"""
A template for a PowerOutlet to be created for a new Device.
@ -225,6 +231,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
)
@extras_features('custom_fields', 'export_templates', 'webhooks')
class InterfaceTemplate(ComponentTemplateModel):
"""
A template for a physical data interface on a new Device.
@ -259,6 +266,7 @@ class InterfaceTemplate(ComponentTemplateModel):
)
@extras_features('custom_fields', 'export_templates', 'webhooks')
class FrontPortTemplate(ComponentTemplateModel):
"""
Template for a pass-through port on the front of a new Device.
@ -319,6 +327,7 @@ class FrontPortTemplate(ComponentTemplateModel):
)
@extras_features('custom_fields', 'export_templates', 'webhooks')
class RearPortTemplate(ComponentTemplateModel):
"""
Template for a pass-through port on the rear of a new Device.
@ -349,6 +358,7 @@ class RearPortTemplate(ComponentTemplateModel):
)
@extras_features('custom_fields', 'export_templates', 'webhooks')
class DeviceBayTemplate(ComponentTemplateModel):
"""
A template for a DeviceBay to be created for a new parent Device.

View File

@ -13,6 +13,7 @@ from dcim.constants import *
from dcim.fields import MACAddressField
from extras.models import ObjectChange, TaggedItem
from extras.utils import extras_features
from netbox.models import PrimaryModel
from utilities.fields import NaturalOrderingField
from utilities.mptt import TreeManager
from utilities.ordering import naturalize_interface
@ -37,7 +38,7 @@ __all__ = (
)
class ComponentModel(models.Model):
class ComponentModel(PrimaryModel):
"""
An abstract model inherited by any model which has a parent Device.
"""
@ -198,7 +199,7 @@ class PathEndpoint(models.Model):
# Console ports
#
@extras_features('export_templates', 'webhooks')
@extras_features('custom_fields', 'export_templates', 'webhooks')
class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
"""
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@ -234,7 +235,7 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
# Console server ports
#
@extras_features('webhooks')
@extras_features('custom_fields', 'export_templates', 'webhooks')
class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
"""
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
@ -270,7 +271,7 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
# Power ports
#
@extras_features('export_templates', 'webhooks')
@extras_features('custom_fields', 'export_templates', 'webhooks')
class PowerPort(CableTermination, PathEndpoint, ComponentModel):
"""
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@ -379,7 +380,7 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel):
# Power outlets
#
@extras_features('webhooks')
@extras_features('custom_fields', 'export_templates', 'webhooks')
class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
"""
A physical power outlet (output) within a Device which provides power to a PowerPort.
@ -479,7 +480,7 @@ class BaseInterface(models.Model):
return super().save(*args, **kwargs)
@extras_features('export_templates', 'webhooks')
@extras_features('custom_fields', 'export_templates', 'webhooks')
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
"""
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
@ -624,7 +625,7 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
# Pass-through ports
#
@extras_features('webhooks')
@extras_features('custom_fields', 'export_templates', 'webhooks')
class FrontPort(CableTermination, ComponentModel):
"""
A pass-through port on the front of a Device.
@ -687,7 +688,7 @@ class FrontPort(CableTermination, ComponentModel):
})
@extras_features('webhooks')
@extras_features('custom_fields', 'export_templates', 'webhooks')
class RearPort(CableTermination, ComponentModel):
"""
A pass-through port on the rear of a Device.
@ -740,7 +741,7 @@ class RearPort(CableTermination, ComponentModel):
# Device bays
#
@extras_features('webhooks')
@extras_features('custom_fields', 'export_templates', 'webhooks')
class DeviceBay(ComponentModel):
"""
An empty space within a Device which can house a child device
@ -800,7 +801,7 @@ class DeviceBay(ComponentModel):
# Inventory items
#
@extras_features('export_templates', 'webhooks')
@extras_features('custom_fields', 'export_templates', 'webhooks')
class InventoryItem(MPTTModel, ComponentModel):
"""
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.

View File

@ -13,9 +13,10 @@ from taggit.managers import TaggableManager
from dcim.choices import *
from dcim.constants import *
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
from extras.models import ConfigContextModel, TaggedItem
from extras.querysets import ConfigContextModelQuerySet
from extras.utils import extras_features
from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField
from utilities.querysets import RestrictedQuerySet
@ -36,8 +37,8 @@ __all__ = (
# Device Types
#
@extras_features('export_templates', 'webhooks')
class Manufacturer(ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class Manufacturer(OrganizationalModel):
"""
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
"""
@ -76,7 +77,7 @@ class Manufacturer(ChangeLoggedModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class DeviceType(ChangeLoggedModel, CustomFieldModel):
class DeviceType(PrimaryModel):
"""
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
well as high-level functional role(s).
@ -338,7 +339,8 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
# Devices
#
class DeviceRole(ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class DeviceRole(OrganizationalModel):
"""
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
@ -385,7 +387,8 @@ class DeviceRole(ChangeLoggedModel):
)
class Platform(ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class Platform(OrganizationalModel):
"""
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
@ -449,7 +452,7 @@ class Platform(ChangeLoggedModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
class Device(PrimaryModel, ConfigContextModel):
"""
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
@ -882,7 +885,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class VirtualChassis(ChangeLoggedModel, CustomFieldModel):
class VirtualChassis(PrimaryModel):
"""
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
"""

View File

@ -6,8 +6,9 @@ from taggit.managers import TaggableManager
from dcim.choices import *
from dcim.constants import *
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
from extras.models import TaggedItem
from extras.utils import extras_features
from netbox.models import PrimaryModel
from utilities.querysets import RestrictedQuerySet
from utilities.validators import ExclusionValidator
from .device_components import CableTermination, PathEndpoint
@ -23,7 +24,7 @@ __all__ = (
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class PowerPanel(ChangeLoggedModel, CustomFieldModel):
class PowerPanel(PrimaryModel):
"""
A distribution point for electrical power; e.g. a data center RPP.
"""
@ -74,7 +75,7 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldModel):
class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
"""
An electrical circuit delivered from a PowerPanel.
"""

View File

@ -10,14 +10,15 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Count, Sum
from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
from mptt.models import TreeForeignKey
from taggit.managers import TaggableManager
from dcim.choices import *
from dcim.constants import *
from dcim.elevations import RackElevationSVG
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.models import ObjectChange, TaggedItem
from extras.utils import extras_features
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField
from utilities.querysets import RestrictedQuerySet
@ -39,8 +40,8 @@ __all__ = (
# Racks
#
@extras_features('export_templates')
class RackGroup(MPTTModel, ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class RackGroup(NestedGroupModel):
"""
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
@ -70,8 +71,6 @@ class RackGroup(MPTTModel, ChangeLoggedModel):
blank=True
)
objects = TreeManager()
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
class Meta:
@ -81,12 +80,6 @@ class RackGroup(MPTTModel, ChangeLoggedModel):
['site', 'slug'],
]
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
@ -99,15 +92,6 @@ class RackGroup(MPTTModel, ChangeLoggedModel):
self.description,
)
def to_objectchange(self, action):
# Remove MPTT-internal fields
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
)
def clean(self):
super().clean()
@ -116,7 +100,8 @@ class RackGroup(MPTTModel, ChangeLoggedModel):
raise ValidationError(f"Parent rack group ({self.parent}) must belong to the same site ({self.site})")
class RackRole(ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class RackRole(OrganizationalModel):
"""
Racks can be organized by functional role, similar to Devices.
"""
@ -159,7 +144,7 @@ class RackRole(ChangeLoggedModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Rack(ChangeLoggedModel, CustomFieldModel):
class Rack(PrimaryModel):
"""
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
Each Rack is assigned to a Site and (optionally) a RackGroup.
@ -550,7 +535,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class RackReservation(ChangeLoggedModel, CustomFieldModel):
class RackReservation(PrimaryModel):
"""
One or more reserved units within a Rack.
"""

View File

@ -1,15 +1,16 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
from mptt.models import TreeForeignKey
from taggit.managers import TaggableManager
from timezone_field import TimeZoneField
from dcim.choices import *
from dcim.constants import *
from dcim.fields import ASNField
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.models import ObjectChange, TaggedItem
from extras.utils import extras_features
from netbox.models import NestedGroupModel, PrimaryModel
from utilities.fields import NaturalOrderingField
from utilities.querysets import RestrictedQuerySet
from utilities.mptt import TreeManager
@ -25,8 +26,8 @@ __all__ = (
# Regions
#
@extras_features('export_templates', 'webhooks')
class Region(MPTTModel, ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class Region(NestedGroupModel):
"""
Sites can be grouped within geographic Regions.
"""
@ -51,16 +52,8 @@ class Region(MPTTModel, ChangeLoggedModel):
blank=True
)
objects = TreeManager()
csv_headers = ['name', 'slug', 'parent', 'description']
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
@ -78,22 +71,13 @@ class Region(MPTTModel, ChangeLoggedModel):
Q(region__in=self.get_descendants())
).count()
def to_objectchange(self, action):
# Remove MPTT-internal fields
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
)
#
# Sites
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Site(ChangeLoggedModel, CustomFieldModel):
class Site(PrimaryModel):
"""
A Site represents a geographic location within a network; typically a building or campus. The optional facility
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).

View File

@ -0,0 +1,67 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0053_rename_webhook_obj_type'),
]
operations = [
migrations.AddField(
model_name='configcontext',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='configcontext',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='customfield',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='customlink',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='exporttemplate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='imageattachment',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='jobresult',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='objectchange',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='tag',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='taggeditem',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='webhook',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@ -1,4 +1,4 @@
from .change_logging import ChangeLoggedModel, ObjectChange
from .change_logging import ObjectChange
from .customfields import CustomField, CustomFieldModel
from .models import (
ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, ImageAttachment, JobResult, Report, Script,
@ -7,7 +7,6 @@ from .models import (
from .tags import Tag, TaggedItem
__all__ = (
'ChangeLoggedModel',
'ConfigContext',
'ConfigContextModel',
'CustomField',

View File

@ -4,48 +4,12 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.urls import reverse
from utilities.querysets import RestrictedQuerySet
from utilities.utils import serialize_object
from extras.choices import *
from netbox.models import BigIDModel
from utilities.querysets import RestrictedQuerySet
#
# Change logging
#
class ChangeLoggedModel(models.Model):
"""
An abstract model which adds fields to store the creation and last-updated times for an object. Both fields can be
null to facilitate adding these fields to existing instances via a database migration.
"""
created = models.DateField(
auto_now_add=True,
blank=True,
null=True
)
last_updated = models.DateTimeField(
auto_now=True,
blank=True,
null=True
)
class Meta:
abstract = True
def to_objectchange(self, action):
"""
Return a new ObjectChange representing a change made to this object. This will typically be called automatically
by ChangeLoggingMiddleware.
"""
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
object_data=serialize_object(self)
)
class ObjectChange(models.Model):
class ObjectChange(BigIDModel):
"""
Record a change to an object and the user account associated with that change. A change record may optionally
indicate an object related to the one being changed. For example, a change to an interface may also indicate the

View File

@ -12,12 +12,13 @@ from django.utils.safestring import mark_safe
from extras.choices import *
from extras.utils import FeatureQuery
from netbox.models import BigIDModel
from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice
from utilities.querysets import RestrictedQuerySet
from utilities.validators import validate_regex
class CustomFieldModel(models.Model):
class CustomFieldModel(BigIDModel):
"""
Abstract class for any model which may have custom fields associated with it.
"""
@ -77,7 +78,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
return self.get_queryset().filter(content_types=content_type)
class CustomField(models.Model):
class CustomField(BigIDModel):
content_types = models.ManyToManyField(
to=ContentType,
related_name='custom_fields',

View File

@ -14,9 +14,9 @@ from rest_framework.utils.encoders import JSONEncoder
from extras.choices import *
from extras.constants import *
from extras.models import ChangeLoggedModel
from extras.querysets import ConfigContextQuerySet
from extras.utils import extras_features, FeatureQuery, image_upload
from netbox.models import BigIDModel, PrimaryModel
from utilities.querysets import RestrictedQuerySet
from utilities.utils import deepmerge, render_jinja2
@ -25,7 +25,7 @@ from utilities.utils import deepmerge, render_jinja2
# Webhooks
#
class Webhook(models.Model):
class Webhook(BigIDModel):
"""
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
@ -158,7 +158,7 @@ class Webhook(models.Model):
# Custom links
#
class CustomLink(models.Model):
class CustomLink(BigIDModel):
"""
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
code to be rendered with an object as context.
@ -210,7 +210,7 @@ class CustomLink(models.Model):
# Export templates
#
class ExportTemplate(models.Model):
class ExportTemplate(BigIDModel):
content_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
@ -285,7 +285,7 @@ class ExportTemplate(models.Model):
# Image attachments
#
class ImageAttachment(models.Model):
class ImageAttachment(BigIDModel):
"""
An uploaded image which is associated with an object.
"""
@ -361,7 +361,7 @@ class ImageAttachment(models.Model):
# Config contexts
#
class ConfigContext(ChangeLoggedModel):
class ConfigContext(PrimaryModel):
"""
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
@ -526,7 +526,7 @@ class Report(models.Model):
# Job results
#
class JobResult(models.Model):
class JobResult(BigIDModel):
"""
This model stores the results from running a user-defined report.
"""

View File

@ -2,7 +2,7 @@ from django.db import models
from django.utils.text import slugify
from taggit.models import TagBase, GenericTaggedItemBase
from extras.models import ChangeLoggedModel
from netbox.models import BigIDModel, CoreModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField
from utilities.querysets import RestrictedQuerySet
@ -12,7 +12,7 @@ from utilities.querysets import RestrictedQuerySet
# Tags
#
class Tag(TagBase, ChangeLoggedModel):
class Tag(TagBase, CoreModel):
color = ColorField(
default=ColorChoices.COLOR_GREY
)
@ -44,7 +44,7 @@ class Tag(TagBase, ChangeLoggedModel):
)
class TaggedItem(GenericTaggedItemBase):
class TaggedItem(BigIDModel, GenericTaggedItemBase):
tag = models.ForeignKey(
to=Tag,
related_name="%(app_label)s_%(class)s_items",

View File

@ -0,0 +1,77 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0043_add_tenancy_to_aggregates'),
]
operations = [
migrations.AddField(
model_name='rir',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='role',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='vlangroup',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='aggregate',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='ipaddress',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='prefix',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='rir',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='role',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='routetarget',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='service',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='vlan',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='vlangroup',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='vrf',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@ -0,0 +1,17 @@
from .ip import *
from .services import *
from .vlans import *
from .vrfs import *
__all__ = (
'Aggregate',
'IPAddress',
'Prefix',
'RIR',
'Role',
'RouteTarget',
'Service',
'VLAN',
'VLANGroup',
'VRF',
)

View File

@ -2,26 +2,25 @@ import netaddr
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import F
from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.models import Device, Interface
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
from dcim.models import Device
from extras.models import ObjectChange, TaggedItem
from extras.utils import extras_features
from netbox.models import OrganizationalModel, PrimaryModel
from ipam.choices import *
from ipam.constants import *
from ipam.fields import IPNetworkField, IPAddressField
from ipam.managers import IPAddressManager
from ipam.querysets import PrefixQuerySet
from ipam.validators import DNSValidator
from utilities.querysets import RestrictedQuerySet
from utilities.utils import array_to_string, serialize_object
from virtualization.models import VirtualMachine, VMInterface
from .choices import *
from .constants import *
from .fields import IPNetworkField, IPAddressField
from .managers import IPAddressManager
from .querysets import PrefixQuerySet
from .validators import DNSValidator
from utilities.utils import serialize_object
from virtualization.models import VirtualMachine
__all__ = (
@ -30,139 +29,11 @@ __all__ = (
'Prefix',
'RIR',
'Role',
'RouteTarget',
'Service',
'VLAN',
'VLANGroup',
'VRF',
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class VRF(ChangeLoggedModel, CustomFieldModel):
"""
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
table). Prefixes and IPAddresses can optionally be assigned to VRFs. (Prefixes and IPAddresses not assigned to a VRF
are said to exist in the "global" table.)
"""
name = models.CharField(
max_length=100
)
rd = models.CharField(
max_length=VRF_RD_MAX_LENGTH,
unique=True,
blank=True,
null=True,
verbose_name='Route distinguisher',
help_text='Unique route distinguisher (as defined in RFC 4364)'
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='vrfs',
blank=True,
null=True
)
enforce_unique = models.BooleanField(
default=True,
verbose_name='Enforce unique space',
help_text='Prevent duplicate prefixes/IP addresses within this VRF'
)
description = models.CharField(
max_length=200,
blank=True
)
import_targets = models.ManyToManyField(
to='ipam.RouteTarget',
related_name='importing_vrfs',
blank=True
)
export_targets = models.ManyToManyField(
to='ipam.RouteTarget',
related_name='exporting_vrfs',
blank=True
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
clone_fields = [
'tenant', 'enforce_unique', 'description',
]
class Meta:
ordering = ('name', 'rd', 'pk') # (name, rd) may be non-unique
verbose_name = 'VRF'
verbose_name_plural = 'VRFs'
def __str__(self):
return self.display_name or super().__str__()
def get_absolute_url(self):
return reverse('ipam:vrf', args=[self.pk])
def to_csv(self):
return (
self.name,
self.rd,
self.tenant.name if self.tenant else None,
self.enforce_unique,
self.description,
)
@property
def display_name(self):
if self.rd:
return f'{self.name} ({self.rd})'
return self.name
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class RouteTarget(ChangeLoggedModel, CustomFieldModel):
"""
A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.
"""
name = models.CharField(
max_length=VRF_RD_MAX_LENGTH, # Same format options as VRF RD (RFC 4360 section 4)
unique=True,
help_text='Route target value (formatted in accordance with RFC 4360)'
)
description = models.CharField(
max_length=200,
blank=True
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='route_targets',
blank=True,
null=True
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
csv_headers = ['name', 'description', 'tenant']
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('ipam:routetarget', args=[self.pk])
def to_csv(self):
return (
self.name,
self.description,
self.tenant.name if self.tenant else None,
)
class RIR(ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class RIR(OrganizationalModel):
"""
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
space. This can be an organization like ARIN or RIPE, or a governing standard such as RFC 1918.
@ -210,7 +81,7 @@ class RIR(ChangeLoggedModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Aggregate(ChangeLoggedModel, CustomFieldModel):
class Aggregate(PrimaryModel):
"""
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
the hierarchy and track the overall utilization of available address space. Each Aggregate is assigned to a RIR.
@ -317,7 +188,8 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
return int(float(child_prefixes.size) / self.prefix.size * 100)
class Role(ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class Role(OrganizationalModel):
"""
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
"Management."
@ -358,7 +230,7 @@ class Role(ChangeLoggedModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Prefix(ChangeLoggedModel, CustomFieldModel):
class Prefix(PrimaryModel):
"""
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be
@ -616,7 +488,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class IPAddress(ChangeLoggedModel, CustomFieldModel):
class IPAddress(PrimaryModel):
"""
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
@ -831,274 +703,3 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
def get_role_class(self):
return IPAddressRoleChoices.CSS_CLASSES.get(self.role)
class VLANGroup(ChangeLoggedModel):
"""
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
"""
name = models.CharField(
max_length=100
)
slug = models.SlugField(
max_length=100
)
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.PROTECT,
related_name='vlan_groups',
blank=True,
null=True
)
description = models.CharField(
max_length=200,
blank=True
)
objects = RestrictedQuerySet.as_manager()
csv_headers = ['name', 'slug', 'site', 'description']
class Meta:
ordering = ('site', 'name', 'pk') # (site, name) may be non-unique
unique_together = [
['site', 'name'],
['site', 'slug'],
]
verbose_name = 'VLAN group'
verbose_name_plural = 'VLAN groups'
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('ipam:vlangroup_vlans', args=[self.pk])
def to_csv(self):
return (
self.name,
self.slug,
self.site.name if self.site else None,
self.description,
)
def get_next_available_vid(self):
"""
Return the first available VLAN ID (1-4094) in the group.
"""
vlan_ids = VLAN.objects.filter(group=self).values_list('vid', flat=True)
for i in range(1, 4095):
if i not in vlan_ids:
return i
return None
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class VLAN(ChangeLoggedModel, CustomFieldModel):
"""
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
within which all VLAN IDs and names but be unique.
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
or more Prefixes assigned to it.
"""
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.PROTECT,
related_name='vlans',
blank=True,
null=True
)
group = models.ForeignKey(
to='ipam.VLANGroup',
on_delete=models.PROTECT,
related_name='vlans',
blank=True,
null=True
)
vid = models.PositiveSmallIntegerField(
verbose_name='ID',
validators=[MinValueValidator(1), MaxValueValidator(4094)]
)
name = models.CharField(
max_length=64
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='vlans',
blank=True,
null=True
)
status = models.CharField(
max_length=50,
choices=VLANStatusChoices,
default=VLANStatusChoices.STATUS_ACTIVE
)
role = models.ForeignKey(
to='ipam.Role',
on_delete=models.SET_NULL,
related_name='vlans',
blank=True,
null=True
)
description = models.CharField(
max_length=200,
blank=True
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
csv_headers = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
clone_fields = [
'site', 'group', 'tenant', 'status', 'role', 'description',
]
class Meta:
ordering = ('site', 'group', 'vid', 'pk') # (site, group, vid) may be non-unique
unique_together = [
['group', 'vid'],
['group', 'name'],
]
verbose_name = 'VLAN'
verbose_name_plural = 'VLANs'
def __str__(self):
return self.display_name or super().__str__()
def get_absolute_url(self):
return reverse('ipam:vlan', args=[self.pk])
def clean(self):
super().clean()
# Validate VLAN group
if self.group and self.group.site != self.site:
raise ValidationError({
'group': "VLAN group must belong to the assigned site ({}).".format(self.site)
})
def to_csv(self):
return (
self.site.name if self.site else None,
self.group.name if self.group else None,
self.vid,
self.name,
self.tenant.name if self.tenant else None,
self.get_status_display(),
self.role.name if self.role else None,
self.description,
)
@property
def display_name(self):
return f'{self.name} ({self.vid})'
def get_status_class(self):
return VLANStatusChoices.CSS_CLASSES.get(self.status)
def get_interfaces(self):
# Return all device interfaces assigned to this VLAN
return Interface.objects.filter(
Q(untagged_vlan_id=self.pk) |
Q(tagged_vlans=self.pk)
).distinct()
def get_vminterfaces(self):
# Return all VM interfaces assigned to this VLAN
return VMInterface.objects.filter(
Q(untagged_vlan_id=self.pk) |
Q(tagged_vlans=self.pk)
).distinct()
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Service(ChangeLoggedModel, CustomFieldModel):
"""
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
optionally be tied to one or more specific IPAddresses belonging to its parent.
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='services',
verbose_name='device',
null=True,
blank=True
)
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='services',
null=True,
blank=True
)
name = models.CharField(
max_length=100
)
protocol = models.CharField(
max_length=50,
choices=ServiceProtocolChoices
)
ports = ArrayField(
base_field=models.PositiveIntegerField(
validators=[
MinValueValidator(SERVICE_PORT_MIN),
MaxValueValidator(SERVICE_PORT_MAX)
]
),
verbose_name='Port numbers'
)
ipaddresses = models.ManyToManyField(
to='ipam.IPAddress',
related_name='services',
blank=True,
verbose_name='IP addresses'
)
description = models.CharField(
max_length=200,
blank=True
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'ports', 'description']
class Meta:
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique
def __str__(self):
return f'{self.name} ({self.get_protocol_display()}/{self.port_list})'
def get_absolute_url(self):
return reverse('ipam:service', args=[self.pk])
@property
def parent(self):
return self.device or self.virtual_machine
def clean(self):
super().clean()
# A Service must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine:
raise ValidationError("A service cannot be associated with both a device and a virtual machine.")
if not self.device and not self.virtual_machine:
raise ValidationError("A service must be associated with either a device or a virtual machine.")
def to_csv(self):
return (
self.device.name if self.device else None,
self.virtual_machine.name if self.virtual_machine else None,
self.name,
self.get_protocol_display(),
self.ports,
self.description,
)
@property
def port_list(self):
return array_to_string(self.ports)

View File

@ -0,0 +1,109 @@
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
from extras.models import TaggedItem
from extras.utils import extras_features
from ipam.choices import *
from ipam.constants import *
from netbox.models import PrimaryModel
from utilities.querysets import RestrictedQuerySet
from utilities.utils import array_to_string
__all__ = (
'Service',
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Service(PrimaryModel):
"""
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
optionally be tied to one or more specific IPAddresses belonging to its parent.
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='services',
verbose_name='device',
null=True,
blank=True
)
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='services',
null=True,
blank=True
)
name = models.CharField(
max_length=100
)
protocol = models.CharField(
max_length=50,
choices=ServiceProtocolChoices
)
ports = ArrayField(
base_field=models.PositiveIntegerField(
validators=[
MinValueValidator(SERVICE_PORT_MIN),
MaxValueValidator(SERVICE_PORT_MAX)
]
),
verbose_name='Port numbers'
)
ipaddresses = models.ManyToManyField(
to='ipam.IPAddress',
related_name='services',
blank=True,
verbose_name='IP addresses'
)
description = models.CharField(
max_length=200,
blank=True
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'ports', 'description']
class Meta:
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique
def __str__(self):
return f'{self.name} ({self.get_protocol_display()}/{self.port_list})'
def get_absolute_url(self):
return reverse('ipam:service', args=[self.pk])
@property
def parent(self):
return self.device or self.virtual_machine
def clean(self):
super().clean()
# A Service must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine:
raise ValidationError("A service cannot be associated with both a device and a virtual machine.")
if not self.device and not self.virtual_machine:
raise ValidationError("A service must be associated with either a device or a virtual machine.")
def to_csv(self):
return (
self.device.name if self.device else None,
self.virtual_machine.name if self.virtual_machine else None,
self.name,
self.get_protocol_display(),
self.ports,
self.description,
)
@property
def port_list(self):
return array_to_string(self.ports)

202
netbox/ipam/models/vlans.py Normal file
View File

@ -0,0 +1,202 @@
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.models import Interface
from extras.models import TaggedItem
from extras.utils import extras_features
from ipam.choices import *
from ipam.constants import *
from netbox.models import OrganizationalModel, PrimaryModel
from utilities.querysets import RestrictedQuerySet
from virtualization.models import VMInterface
__all__ = (
'VLAN',
'VLANGroup',
)
@extras_features('custom_fields', 'export_templates', 'webhooks')
class VLANGroup(OrganizationalModel):
"""
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
"""
name = models.CharField(
max_length=100
)
slug = models.SlugField(
max_length=100
)
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.PROTECT,
related_name='vlan_groups',
blank=True,
null=True
)
description = models.CharField(
max_length=200,
blank=True
)
objects = RestrictedQuerySet.as_manager()
csv_headers = ['name', 'slug', 'site', 'description']
class Meta:
ordering = ('site', 'name', 'pk') # (site, name) may be non-unique
unique_together = [
['site', 'name'],
['site', 'slug'],
]
verbose_name = 'VLAN group'
verbose_name_plural = 'VLAN groups'
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('ipam:vlangroup_vlans', args=[self.pk])
def to_csv(self):
return (
self.name,
self.slug,
self.site.name if self.site else None,
self.description,
)
def get_next_available_vid(self):
"""
Return the first available VLAN ID (1-4094) in the group.
"""
vlan_ids = VLAN.objects.filter(group=self).values_list('vid', flat=True)
for i in range(1, 4095):
if i not in vlan_ids:
return i
return None
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class VLAN(PrimaryModel):
"""
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
within which all VLAN IDs and names but be unique.
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
or more Prefixes assigned to it.
"""
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.PROTECT,
related_name='vlans',
blank=True,
null=True
)
group = models.ForeignKey(
to='ipam.VLANGroup',
on_delete=models.PROTECT,
related_name='vlans',
blank=True,
null=True
)
vid = models.PositiveSmallIntegerField(
verbose_name='ID',
validators=[MinValueValidator(1), MaxValueValidator(4094)]
)
name = models.CharField(
max_length=64
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='vlans',
blank=True,
null=True
)
status = models.CharField(
max_length=50,
choices=VLANStatusChoices,
default=VLANStatusChoices.STATUS_ACTIVE
)
role = models.ForeignKey(
to='ipam.Role',
on_delete=models.SET_NULL,
related_name='vlans',
blank=True,
null=True
)
description = models.CharField(
max_length=200,
blank=True
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
csv_headers = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
clone_fields = [
'site', 'group', 'tenant', 'status', 'role', 'description',
]
class Meta:
ordering = ('site', 'group', 'vid', 'pk') # (site, group, vid) may be non-unique
unique_together = [
['group', 'vid'],
['group', 'name'],
]
verbose_name = 'VLAN'
verbose_name_plural = 'VLANs'
def __str__(self):
return self.display_name or super().__str__()
def get_absolute_url(self):
return reverse('ipam:vlan', args=[self.pk])
def clean(self):
super().clean()
# Validate VLAN group
if self.group and self.group.site != self.site:
raise ValidationError({
'group': "VLAN group must belong to the assigned site ({}).".format(self.site)
})
def to_csv(self):
return (
self.site.name if self.site else None,
self.group.name if self.group else None,
self.vid,
self.name,
self.tenant.name if self.tenant else None,
self.get_status_display(),
self.role.name if self.role else None,
self.description,
)
@property
def display_name(self):
return f'{self.name} ({self.vid})'
def get_status_class(self):
return VLANStatusChoices.CSS_CLASSES.get(self.status)
def get_interfaces(self):
# Return all device interfaces assigned to this VLAN
return Interface.objects.filter(
Q(untagged_vlan_id=self.pk) |
Q(tagged_vlans=self.pk)
).distinct()
def get_vminterfaces(self):
# Return all VM interfaces assigned to this VLAN
return VMInterface.objects.filter(
Q(untagged_vlan_id=self.pk) |
Q(tagged_vlans=self.pk)
).distinct()

139
netbox/ipam/models/vrfs.py Normal file
View File

@ -0,0 +1,139 @@
from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
from extras.models import TaggedItem
from extras.utils import extras_features
from ipam.constants import *
from netbox.models import PrimaryModel
from utilities.querysets import RestrictedQuerySet
__all__ = (
'RouteTarget',
'VRF',
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class VRF(PrimaryModel):
"""
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
table). Prefixes and IPAddresses can optionally be assigned to VRFs. (Prefixes and IPAddresses not assigned to a VRF
are said to exist in the "global" table.)
"""
name = models.CharField(
max_length=100
)
rd = models.CharField(
max_length=VRF_RD_MAX_LENGTH,
unique=True,
blank=True,
null=True,
verbose_name='Route distinguisher',
help_text='Unique route distinguisher (as defined in RFC 4364)'
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='vrfs',
blank=True,
null=True
)
enforce_unique = models.BooleanField(
default=True,
verbose_name='Enforce unique space',
help_text='Prevent duplicate prefixes/IP addresses within this VRF'
)
description = models.CharField(
max_length=200,
blank=True
)
import_targets = models.ManyToManyField(
to='ipam.RouteTarget',
related_name='importing_vrfs',
blank=True
)
export_targets = models.ManyToManyField(
to='ipam.RouteTarget',
related_name='exporting_vrfs',
blank=True
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
clone_fields = [
'tenant', 'enforce_unique', 'description',
]
class Meta:
ordering = ('name', 'rd', 'pk') # (name, rd) may be non-unique
verbose_name = 'VRF'
verbose_name_plural = 'VRFs'
def __str__(self):
return self.display_name or super().__str__()
def get_absolute_url(self):
return reverse('ipam:vrf', args=[self.pk])
def to_csv(self):
return (
self.name,
self.rd,
self.tenant.name if self.tenant else None,
self.enforce_unique,
self.description,
)
@property
def display_name(self):
if self.rd:
return f'{self.name} ({self.rd})'
return self.name
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class RouteTarget(PrimaryModel):
"""
A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.
"""
name = models.CharField(
max_length=VRF_RD_MAX_LENGTH, # Same format options as VRF RD (RFC 4360 section 4)
unique=True,
help_text='Route target value (formatted in accordance with RFC 4360)'
)
description = models.CharField(
max_length=200,
blank=True
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='route_targets',
blank=True,
null=True
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
csv_headers = ['name', 'description', 'tenant']
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('ipam:routetarget', args=[self.pk])
def to_csv(self):
return (
self.name,
self.description,
self.tenant.name if self.tenant else None,
)

184
netbox/netbox/models.py Normal file
View File

@ -0,0 +1,184 @@
from collections import OrderedDict
from django.core.serializers.json import DjangoJSONEncoder
from django.core.validators import ValidationError
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from utilities.mptt import TreeManager
from utilities.utils import serialize_object
__all__ = (
'BigIDModel',
'NestedGroupModel',
'OrganizationalModel',
'PrimaryModel',
)
class BigIDModel(models.Model):
"""
Abstract base model for all Schematic data objects. Ensures the use of a 64-bit PK.
"""
id = models.BigAutoField(
primary_key=True
)
class Meta:
abstract = True
class CoreModel(BigIDModel):
"""
Base class for all core objects. Provides the following:
- Change logging
- Custom field support
"""
created = models.DateField(
auto_now_add=True,
blank=True,
null=True
)
last_updated = models.DateTimeField(
auto_now=True,
blank=True,
null=True
)
class Meta:
abstract = True
def to_objectchange(self, action):
"""
Return a new ObjectChange representing a change made to this object. This will typically be called automatically
by ChangeLoggingMiddleware.
"""
from extras.models import ObjectChange
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
object_data=serialize_object(self)
)
class PrimaryModel(CoreModel):
"""
Primary models represent real objects within the infrastructure being modeled.
"""
custom_field_data = models.JSONField(
encoder=DjangoJSONEncoder,
blank=True,
default=dict
)
class Meta:
abstract = True
@property
def cf(self):
"""
Convenience wrapper for custom field data.
"""
return self.custom_field_data
def get_custom_fields(self):
"""
Return a dictionary of custom fields for a single object in the form {<field>: value}.
"""
from extras.models import CustomField
fields = CustomField.objects.get_for_model(self)
return OrderedDict([
(field, self.custom_field_data.get(field.name)) for field in fields
])
def clean(self):
super().clean()
from extras.models import CustomField
custom_fields = {cf.name: cf for cf in CustomField.objects.get_for_model(self)}
# Validate all field values
for field_name, value in self.custom_field_data.items():
if field_name not in custom_fields:
raise ValidationError(f"Unknown field name '{field_name}' in custom field data.")
try:
custom_fields[field_name].validate(value)
except ValidationError as e:
raise ValidationError(f"Invalid value for custom field '{field_name}': {e.message}")
# Check for missing required values
for cf in custom_fields.values():
if cf.required and cf.name not in self.custom_field_data:
raise ValidationError(f"Missing required custom field '{cf.name}'.")
class NestedGroupModel(PrimaryModel, MPTTModel):
"""
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
recursively using MPTT. Within each parent, each child instance must have a unique name.
"""
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)
name = models.CharField(
max_length=100
)
description = models.CharField(
max_length=200,
blank=True
)
objects = TreeManager()
class Meta:
abstract = True
class MPTTMeta:
order_insertion_by = ('name',)
def __str__(self):
return self.name
def to_objectchange(self, action):
# Remove MPTT-internal fields
from extras.models import ObjectChange
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
)
class OrganizationalModel(PrimaryModel):
"""
Organizational models are those which are used solely to categorize and qualify other objects, and do not convey
any real information about the infrastructure being modeled (for example, functional device roles). Organizational
models provide the following standard attributes:
- Unique name
- Unique slug (automatically derived from name)
- Optional description
"""
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
description = models.CharField(
max_length=200,
blank=True
)
class Meta:
abstract = True
ordering = ('name',)

View File

@ -0,0 +1,37 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('secrets', '0012_standardize_name_length'),
]
operations = [
migrations.AddField(
model_name='secretrole',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='secret',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='secretrole',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='sessionkey',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='userkey',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@ -14,8 +14,9 @@ from django.urls import reverse
from django.utils.encoding import force_bytes
from taggit.managers import TaggableManager
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
from extras.models import TaggedItem
from extras.utils import extras_features
from netbox.models import BigIDModel, OrganizationalModel, PrimaryModel
from utilities.querysets import RestrictedQuerySet
from .exceptions import InvalidKey
from .hashers import SecretValidationHasher
@ -31,7 +32,7 @@ __all__ = (
)
class UserKey(models.Model):
class UserKey(BigIDModel):
"""
A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted
copy of the master encryption key. The encrypted instance of the master key can be decrypted only with the user's
@ -164,7 +165,7 @@ class UserKey(models.Model):
self.save()
class SessionKey(models.Model):
class SessionKey(BigIDModel):
"""
A SessionKey stores a User's temporary key to be used for the encryption and decryption of secrets.
"""
@ -234,7 +235,8 @@ class SessionKey(models.Model):
return session_key
class SecretRole(ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class SecretRole(OrganizationalModel):
"""
A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
such as "Login Credentials" or "SNMP Communities."
@ -274,7 +276,7 @@ class SecretRole(ChangeLoggedModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Secret(ChangeLoggedModel, CustomFieldModel):
class Secret(PrimaryModel):
"""
A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
SHA-256 hash is stored along with the ciphertext for validation upon decryption. Each Secret is assigned to exactly

View File

@ -0,0 +1,27 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0011_standardize_name_length'),
]
operations = [
migrations.AddField(
model_name='tenantgroup',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='tenant',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='tenantgroup',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@ -3,11 +3,11 @@ from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
from taggit.managers import TaggableManager
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.models import ObjectChange, TaggedItem
from extras.utils import extras_features
from netbox.models import NestedGroupModel, PrimaryModel
from utilities.mptt import TreeManager
from utilities.querysets import RestrictedQuerySet
from utilities.utils import serialize_object
__all__ = (
@ -16,7 +16,8 @@ __all__ = (
)
class TenantGroup(MPTTModel, ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class TenantGroup(NestedGroupModel):
"""
An arbitrary collection of Tenants.
"""
@ -48,12 +49,6 @@ class TenantGroup(MPTTModel, ChangeLoggedModel):
class Meta:
ordering = ['name']
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug)
@ -65,18 +60,9 @@ class TenantGroup(MPTTModel, ChangeLoggedModel):
self.description,
)
def to_objectchange(self, action):
# Remove MPTT-internal fields
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Tenant(ChangeLoggedModel, CustomFieldModel):
class Tenant(PrimaryModel):
"""
A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
department.

View File

@ -0,0 +1,26 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0010_update_jsonfield'),
]
operations = [
migrations.AlterField(
model_name='objectpermission',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='token',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='userconfig',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@ -11,6 +11,7 @@ from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from netbox.models import BigIDModel
from utilities.querysets import RestrictedQuerySet
from utilities.utils import flatten_dict
@ -50,7 +51,7 @@ class AdminUser(User):
# User preferences
#
class UserConfig(models.Model):
class UserConfig(BigIDModel):
"""
This model stores arbitrary user-specific preferences in a JSON data structure.
"""
@ -175,7 +176,7 @@ def create_userconfig(instance, created, **kwargs):
# REST API
#
class Token(models.Model):
class Token(BigIDModel):
"""
An API token used for user authentication. This extends the stock model to allow each user to have multiple tokens.
It also supports setting an expiration time and toggling write ability.
@ -233,7 +234,7 @@ class Token(models.Model):
# Permissions
#
class ObjectPermission(models.Model):
class ObjectPermission(BigIDModel):
"""
A mapping of view, add, change, and/or delete permission for users and/or groups to an arbitrary set of objects
identified by ORM query parameters.

View File

@ -0,0 +1,62 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0019_standardize_name_length'),
]
operations = [
migrations.AddField(
model_name='clustergroup',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='clustertype',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='vminterface',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='vminterface',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AddField(
model_name='vminterface',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='cluster',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='clustergroup',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='clustertype',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='virtualmachine',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='vminterface',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@ -6,9 +6,10 @@ from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.models import BaseInterface, Device
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.models import ConfigContextModel, ObjectChange, TaggedItem
from extras.querysets import ConfigContextModelQuerySet
from extras.utils import extras_features
from netbox.models import OrganizationalModel, PrimaryModel
from utilities.fields import NaturalOrderingField
from utilities.ordering import naturalize_interface
from utilities.query_functions import CollateAsChar
@ -30,7 +31,8 @@ __all__ = (
# Cluster types
#
class ClusterType(ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class ClusterType(OrganizationalModel):
"""
A type of Cluster.
"""
@ -72,7 +74,8 @@ class ClusterType(ChangeLoggedModel):
# Cluster groups
#
class ClusterGroup(ChangeLoggedModel):
@extras_features('custom_fields', 'export_templates', 'webhooks')
class ClusterGroup(OrganizationalModel):
"""
An organizational group of Clusters.
"""
@ -115,7 +118,7 @@ class ClusterGroup(ChangeLoggedModel):
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Cluster(ChangeLoggedModel, CustomFieldModel):
class Cluster(PrimaryModel):
"""
A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
"""
@ -199,7 +202,7 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
class VirtualMachine(PrimaryModel, ConfigContextModel):
"""
A virtual machine which runs inside a Cluster.
"""
@ -371,7 +374,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
#
@extras_features('export_templates', 'webhooks')
class VMInterface(BaseInterface):
class VMInterface(PrimaryModel, BaseInterface):
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,