diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index a65fe3063..4df845bd8 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -7,8 +7,7 @@ from django.utils.encoding import python_2_unicode_compatible from dcim.constants import STATUS_CLASSES from dcim.fields import ASNField -from extras.models import CustomFieldModel, CustomFieldValue -from tenancy.models import Tenant +from extras.models import CustomFieldModel from utilities.models import CreatedUpdatedModel from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES @@ -19,15 +18,43 @@ class Provider(CreatedUpdatedModel, CustomFieldModel): 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. """ - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) - asn = ASNField(blank=True, null=True, verbose_name='ASN') - account = models.CharField(max_length=30, blank=True, verbose_name='Account number') - portal_url = models.URLField(blank=True, verbose_name='Portal') - noc_contact = models.TextField(blank=True, verbose_name='NOC contact') - admin_contact = models.TextField(blank=True, verbose_name='Admin contact') - comments = models.TextField(blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) + asn = ASNField( + blank=True, + null=True, + verbose_name='ASN' + ) + account = models.CharField( + max_length=30, + blank=True, + verbose_name='Account number' + ) + portal_url = models.URLField( + blank=True, + verbose_name='Portal' + ) + noc_contact = models.TextField( + blank=True, + verbose_name='NOC contact' + ) + admin_contact = models.TextField( + blank=True, + verbose_name='Admin contact' + ) + comments = models.TextField( + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments'] @@ -59,8 +86,13 @@ class CircuitType(models.Model): 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". """ - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) csv_headers = ['name', 'slug'] @@ -87,16 +119,52 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device interface, but this is not required. Circuit port speed and commit rate are measured in Kbps. """ - cid = models.CharField(max_length=50, verbose_name='Circuit ID') - provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT) - type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT) - status = models.PositiveSmallIntegerField(choices=CIRCUIT_STATUS_CHOICES, default=CIRCUIT_STATUS_ACTIVE) - tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT) - install_date = models.DateField(blank=True, null=True, verbose_name='Date installed') - commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)') - description = models.CharField(max_length=100, blank=True) - comments = models.TextField(blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') + cid = models.CharField( + max_length=50, + verbose_name='Circuit ID' + ) + provider = models.ForeignKey( + to='circuits.Provider', + on_delete=models.PROTECT, + related_name='circuits' + ) + type = models.ForeignKey( + to='CircuitType', + on_delete=models.PROTECT, + related_name='circuits' + ) + status = models.PositiveSmallIntegerField( + choices=CIRCUIT_STATUS_CHOICES, + default=CIRCUIT_STATUS_ACTIVE + ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='circuits', + blank=True, + null=True + ) + install_date = models.DateField( + blank=True, + null=True, + verbose_name='Date installed' + ) + commit_rate = models.PositiveIntegerField( + blank=True, + null=True, + verbose_name='Commit rate (Kbps)') + description = models.CharField( + max_length=100, + blank=True + ) + comments = models.TextField( + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) csv_headers = [ 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', @@ -145,19 +213,47 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): @python_2_unicode_compatible class CircuitTermination(models.Model): - circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE) - term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination') - site = models.ForeignKey('dcim.Site', related_name='circuit_terminations', on_delete=models.PROTECT) - interface = models.OneToOneField( - 'dcim.Interface', related_name='circuit_termination', blank=True, null=True, on_delete=models.PROTECT + circuit = models.ForeignKey( + to='circuits.Circuit', + on_delete=models.CASCADE, + related_name='terminations' + ) + term_side = models.CharField( + max_length=1, + choices=TERM_SIDE_CHOICES, + verbose_name='Termination' + ) + site = models.ForeignKey( + to='dcim.Site', + on_delete=models.PROTECT, + related_name='circuit_terminations' + ) + interface = models.OneToOneField( + to='dcim.Interface', + on_delete=models.PROTECT, + related_name='circuit_termination', + blank=True, + null=True + ) + port_speed = models.PositiveIntegerField( + verbose_name='Port speed (Kbps)' ) - port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)') upstream_speed = models.PositiveIntegerField( - blank=True, null=True, verbose_name='Upstream speed (Kbps)', + blank=True, + null=True, + verbose_name='Upstream speed (Kbps)', help_text='Upstream speed, if different from port speed' ) - xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID') - pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)') + xconnect_id = models.CharField( + max_length=50, + blank=True, + verbose_name='Cross-connect ID' + ) + pp_info = models.CharField( + max_length=100, + blank=True, + verbose_name='Patch panel/port(s)' + ) class Meta: ordering = ['circuit', 'term_side'] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 1bf15a411..8c47c7ba6 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -17,9 +17,8 @@ from mptt.models import MPTTModel, TreeForeignKey from timezone_field import TimeZoneField from circuits.models import Circuit -from extras.models import CustomFieldModel, CustomFieldValue, ImageAttachment +from extras.models import CustomFieldModel from extras.rpc import RPC_CLIENTS -from tenancy.models import Tenant from utilities.fields import ColorField, NullableCharField from utilities.managers import NaturalOrderByManager from utilities.models import CreatedUpdatedModel @@ -38,10 +37,20 @@ class Region(MPTTModel): Sites can be grouped within geographic Regions. """ parent = TreeForeignKey( - 'self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.CASCADE + to='self', + on_delete=models.CASCADE, + related_name='children', + blank=True, + null=True, + db_index=True + ) + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True ) - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) csv_headers = ['name', 'slug', 'parent'] @@ -78,23 +87,78 @@ class Site(CreatedUpdatedModel, CustomFieldModel): 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). """ - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) - status = models.PositiveSmallIntegerField(choices=SITE_STATUS_CHOICES, default=SITE_STATUS_ACTIVE) - region = models.ForeignKey('Region', related_name='sites', blank=True, null=True, on_delete=models.SET_NULL) - tenant = models.ForeignKey(Tenant, related_name='sites', blank=True, null=True, on_delete=models.PROTECT) - facility = models.CharField(max_length=50, blank=True) - asn = ASNField(blank=True, null=True, verbose_name='ASN') - time_zone = TimeZoneField(blank=True) - description = models.CharField(max_length=100, blank=True) - physical_address = models.CharField(max_length=200, blank=True) - shipping_address = models.CharField(max_length=200, blank=True) - contact_name = models.CharField(max_length=50, blank=True) - contact_phone = models.CharField(max_length=20, blank=True) - contact_email = models.EmailField(blank=True, verbose_name="Contact E-mail") - comments = models.TextField(blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') - images = GenericRelation(ImageAttachment) + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) + status = models.PositiveSmallIntegerField( + choices=SITE_STATUS_CHOICES, + default=SITE_STATUS_ACTIVE + ) + region = models.ForeignKey( + to='dcim.Region', + on_delete=models.SET_NULL, + related_name='sites', + blank=True, + null=True + ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='sites', + blank=True, + null=True + ) + facility = models.CharField( + max_length=50, + blank=True + ) + asn = ASNField( + blank=True, + null=True, + verbose_name='ASN' + ) + time_zone = TimeZoneField( + blank=True + ) + description = models.CharField( + max_length=100, + blank=True + ) + physical_address = models.CharField( + max_length=200, + blank=True + ) + shipping_address = models.CharField( + max_length=200, + blank=True + ) + contact_name = models.CharField( + max_length=50, + blank=True + ) + contact_phone = models.CharField( + max_length=20, + blank=True + ) + contact_email = models.EmailField( + blank=True, + verbose_name='Contact E-mail' + ) + comments = models.TextField( + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) + images = GenericRelation( + to='extras.ImageAttachment' + ) objects = SiteManager() @@ -171,9 +235,15 @@ class RackGroup(models.Model): example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that campus. If a Site instead represents a single building, a RackGroup might represent a single room or floor. """ - name = models.CharField(max_length=50) + name = models.CharField( + max_length=50 + ) slug = models.SlugField() - site = models.ForeignKey('Site', related_name='rack_groups', on_delete=models.CASCADE) + site = models.ForeignKey( + to='dcim.Site', + on_delete=models.CASCADE, + related_name='rack_groups' + ) csv_headers = ['site', 'name', 'slug'] @@ -203,8 +273,13 @@ class RackRole(models.Model): """ Racks can be organized by functional role, similar to Devices. """ - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) color = ColorField() csv_headers = ['name', 'slug', 'color'] @@ -238,23 +313,79 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): 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. """ - name = models.CharField(max_length=50) - facility_id = NullableCharField(max_length=50, blank=True, null=True, verbose_name='Facility ID') - site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT) - group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL) - tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT) - role = models.ForeignKey('RackRole', related_name='racks', blank=True, null=True, on_delete=models.PROTECT) - serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number') - type = models.PositiveSmallIntegerField(choices=RACK_TYPE_CHOICES, blank=True, null=True, verbose_name='Type') - width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width', - help_text='Rail-to-rail width') - u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)', - validators=[MinValueValidator(1), MaxValueValidator(100)]) - desc_units = models.BooleanField(default=False, verbose_name='Descending units', - help_text='Units are numbered top-to-bottom') - comments = models.TextField(blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') - images = GenericRelation(ImageAttachment) + name = models.CharField( + max_length=50 + ) + facility_id = NullableCharField( + max_length=50, + blank=True, + null=True, + verbose_name='Facility ID' + ) + site = models.ForeignKey( + to='dcim.Site', + on_delete=models.PROTECT, + related_name='racks' + ) + group = models.ForeignKey( + to='dcim.RackGroup', + on_delete=models.SET_NULL, + related_name='racks', + blank=True, + null=True + ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='racks', + blank=True, + null=True + ) + role = models.ForeignKey( + to='dcim.RackRole', + on_delete=models.PROTECT, + related_name='racks', + blank=True, + null=True + ) + serial = models.CharField( + max_length=50, + blank=True, + verbose_name='Serial number' + ) + type = models.PositiveSmallIntegerField( + choices=RACK_TYPE_CHOICES, + blank=True, + null=True, + verbose_name='Type' + ) + width = models.PositiveSmallIntegerField( + choices=RACK_WIDTH_CHOICES, + default=RACK_WIDTH_19IN, + verbose_name='Width', + help_text='Rail-to-rail width' + ) + u_height = models.PositiveSmallIntegerField( + default=42, + verbose_name='Height (U)', + validators=[MinValueValidator(1), MaxValueValidator(100)] + ) + desc_units = models.BooleanField( + default=False, + verbose_name='Descending units', + help_text='Units are numbered top-to-bottom' + ) + comments = models.TextField( + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) + images = GenericRelation( + to='extras.ImageAttachment' + ) objects = RackManager() @@ -438,12 +569,31 @@ class RackReservation(models.Model): """ One or more reserved units within a Rack. """ - rack = models.ForeignKey('Rack', related_name='reservations', on_delete=models.CASCADE) - units = ArrayField(models.PositiveSmallIntegerField()) - created = models.DateTimeField(auto_now_add=True) - tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='rackreservations', on_delete=models.PROTECT) - user = models.ForeignKey(User, on_delete=models.PROTECT) - description = models.CharField(max_length=100) + rack = models.ForeignKey( + to='dcim.Rack', + on_delete=models.CASCADE, + related_name='reservations' + ) + units = ArrayField( + base_field=models.PositiveSmallIntegerField() + ) + created = models.DateTimeField( + auto_now_add=True + ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='rackreservations', + blank=True, + null=True + ) + user = models.ForeignKey( + to=User, + on_delete=models.PROTECT + ) + description = models.CharField( + max_length=100 + ) class Meta: ordering = ['created'] @@ -496,8 +646,13 @@ class Manufacturer(models.Model): """ A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. """ - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) csv_headers = ['name', 'slug'] @@ -533,27 +688,63 @@ class DeviceType(models.Model, CustomFieldModel): When a new Device of this type is created, the appropriate console, power, and interface objects (as defined by the DeviceType) are automatically created as well. """ - manufacturer = models.ForeignKey('Manufacturer', related_name='device_types', on_delete=models.PROTECT) - model = models.CharField(max_length=50) + manufacturer = models.ForeignKey( + to='dcim.Manufacturer', + on_delete=models.PROTECT, + related_name='device_types' + ) + model = models.CharField( + max_length=50 + ) slug = models.SlugField() - part_number = models.CharField(max_length=50, blank=True, help_text="Discrete part number (optional)") - u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1) - is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth", - help_text="Device consumes both front and rear rack faces") - interface_ordering = models.PositiveSmallIntegerField(choices=IFACE_ORDERING_CHOICES, - default=IFACE_ORDERING_POSITION) - is_console_server = models.BooleanField(default=False, verbose_name='Is a console server', - help_text="This type of device has console server ports") - is_pdu = models.BooleanField(default=False, verbose_name='Is a PDU', - help_text="This type of device has power outlets") - is_network_device = models.BooleanField(default=True, verbose_name='Is a network device', - help_text="This type of device has network interfaces") - subdevice_role = models.NullBooleanField(default=None, verbose_name='Parent/child status', - choices=SUBDEVICE_ROLE_CHOICES, - help_text="Parent devices house child devices in device bays. Select " - "\"None\" if this device type is neither a parent nor a child.") - comments = models.TextField(blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') + part_number = models.CharField( + max_length=50, + blank=True, + help_text='Discrete part number (optional)' + ) + u_height = models.PositiveSmallIntegerField( + default=1, + verbose_name='Height (U)' + ) + is_full_depth = models.BooleanField( + default=True, + verbose_name='Is full depth', + help_text='Device consumes both front and rear rack faces' + ) + interface_ordering = models.PositiveSmallIntegerField( + choices=IFACE_ORDERING_CHOICES, + default=IFACE_ORDERING_POSITION + ) + is_console_server = models.BooleanField( + default=False, + verbose_name='Is a console server', + help_text='This type of device has console server ports' + ) + is_pdu = models.BooleanField( + default=False, + verbose_name='Is a PDU', + help_text='This type of device has power outlets' + ) + is_network_device = models.BooleanField( + default=True, + verbose_name='Is a network device', + help_text='This type of device has network interfaces' + ) + subdevice_role = models.NullBooleanField( + default=None, + verbose_name='Parent/child status', + choices=SUBDEVICE_ROLE_CHOICES, + help_text='Parent devices house child devices in device bays. Select ' + '"None" if this device type is neither a parent nor a child.' + ) + comments = models.TextField( + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) csv_headers = [ 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', @@ -658,8 +849,14 @@ class ConsolePortTemplate(models.Model): """ A template for a ConsolePort to be created for a new Device. """ - device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=50) + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='console_port_templates' + ) + name = models.CharField( + max_length=50 + ) class Meta: ordering = ['device_type', 'name'] @@ -674,8 +871,14 @@ class ConsoleServerPortTemplate(models.Model): """ A template for a ConsoleServerPort to be created for a new Device. """ - device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=50) + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='cs_port_templates' + ) + name = models.CharField( + max_length=50 + ) class Meta: ordering = ['device_type', 'name'] @@ -690,8 +893,14 @@ class PowerPortTemplate(models.Model): """ A template for a PowerPort to be created for a new Device. """ - device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=50) + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='power_port_templates' + ) + name = models.CharField( + max_length=50 + ) class Meta: ordering = ['device_type', 'name'] @@ -706,8 +915,14 @@ class PowerOutletTemplate(models.Model): """ A template for a PowerOutlet to be created for a new Device. """ - device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=50) + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='power_outlet_templates' + ) + name = models.CharField( + max_length=50 + ) class Meta: ordering = ['device_type', 'name'] @@ -722,10 +937,22 @@ class InterfaceTemplate(models.Model): """ A template for a physical data interface on a new Device. """ - device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=64) - form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) - mgmt_only = models.BooleanField(default=False, verbose_name='Management only') + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='interface_templates' + ) + name = models.CharField( + max_length=64 + ) + form_factor = models.PositiveSmallIntegerField( + choices=IFACE_FF_CHOICES, + default=IFACE_FF_10GE_SFP_PLUS + ) + mgmt_only = models.BooleanField( + default=False, + verbose_name='Management only' + ) objects = InterfaceQuerySet.as_manager() @@ -742,8 +969,14 @@ class DeviceBayTemplate(models.Model): """ A template for a DeviceBay to be created for a new parent Device. """ - device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE) - name = models.CharField(max_length=50) + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='device_bay_templates' + ) + name = models.CharField( + max_length=50 + ) class Meta: ordering = ['device_type', 'name'] @@ -764,13 +997,18 @@ class DeviceRole(models.Model): color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to virtual machines as well. """ - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) color = ColorField() vm_role = models.BooleanField( default=True, - verbose_name="VM Role", - help_text="Virtual machines may be assigned to this role" + verbose_name='VM Role', + help_text='Virtual machines may be assigned to this role' ) csv_headers = ['name', 'slug', 'color', 'vm_role'] @@ -800,27 +1038,32 @@ class Platform(models.Model): NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by specifying a NAPALM driver. """ - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) manufacturer = models.ForeignKey( - to='Manufacturer', + to='dcim.Manufacturer', on_delete=models.PROTECT, related_name='platforms', blank=True, null=True, - help_text="Optionally limit this platform to devices of a certain manufacturer" + help_text='Optionally limit this platform to devices of a certain manufacturer' ) 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" + help_text='The name of the NAPALM driver to use when interacting with devices' ) rpc_client = models.CharField( max_length=30, choices=RPC_CLIENT_CHOICES, blank=True, - verbose_name="Legacy RPC client" + verbose_name='Legacy RPC client' ) csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver'] @@ -862,30 +1105,93 @@ class Device(CreatedUpdatedModel, CustomFieldModel): by the component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the creation of a Device. """ - device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT) - device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT) - tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='devices', on_delete=models.PROTECT) - platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL) - name = NullableCharField(max_length=64, blank=True, null=True, unique=True) - serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number') + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.PROTECT, + related_name='instances' + ) + device_role = models.ForeignKey( + to='dcim.DeviceRole', + on_delete=models.PROTECT, + related_name='devices' + ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='devices', + blank=True, + null=True + ) + platform = models.ForeignKey( + to='dcim.Platform', + on_delete=models.SET_NULL, + related_name='devices', + blank=True, + null=True + ) + name = NullableCharField( + max_length=64, + blank=True, + null=True, + unique=True + ) + serial = models.CharField( + max_length=50, + blank=True, + verbose_name='Serial number' + ) asset_tag = NullableCharField( - max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag', + max_length=50, + blank=True, + null=True, + unique=True, + verbose_name='Asset tag', help_text='A unique tag used to identify this device' ) - site = models.ForeignKey('Site', related_name='devices', on_delete=models.PROTECT) - rack = models.ForeignKey('Rack', related_name='devices', blank=True, null=True, on_delete=models.PROTECT) + site = models.ForeignKey( + to='dcim.Site', + on_delete=models.PROTECT, + related_name='devices' + ) + rack = models.ForeignKey( + to='dcim.Rack', + on_delete=models.PROTECT, + related_name='devices', + blank=True, + null=True + ) position = models.PositiveSmallIntegerField( - blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)', + blank=True, + null=True, + validators=[MinValueValidator(1)], + verbose_name='Position (U)', help_text='The lowest-numbered unit occupied by the device' ) - face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face') - status = models.PositiveSmallIntegerField(choices=DEVICE_STATUS_CHOICES, default=DEVICE_STATUS_ACTIVE, verbose_name='Status') + face = models.PositiveSmallIntegerField( + blank=True, + null=True, + choices=RACK_FACE_CHOICES, + verbose_name='Rack face' + ) + status = models.PositiveSmallIntegerField( + choices=DEVICE_STATUS_CHOICES, + default=DEVICE_STATUS_ACTIVE, + verbose_name='Status' + ) primary_ip4 = models.OneToOneField( - 'ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL, blank=True, null=True, + to='ipam.IPAddress', + on_delete=models.SET_NULL, + related_name='primary_ip4_for', + blank=True, + null=True, verbose_name='Primary IPv4' ) primary_ip6 = models.OneToOneField( - 'ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True, + to='ipam.IPAddress', + on_delete=models.SET_NULL, + related_name='primary_ip6_for', + blank=True, + null=True, verbose_name='Primary IPv6' ) cluster = models.ForeignKey( @@ -912,9 +1218,17 @@ class Device(CreatedUpdatedModel, CustomFieldModel): null=True, validators=[MaxValueValidator(255)] ) - comments = models.TextField(blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') - images = GenericRelation(ImageAttachment) + comments = models.TextField( + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) + images = GenericRelation( + to='extras.ImageAttachment' + ) objects = DeviceManager() @@ -1169,11 +1483,26 @@ class ConsolePort(models.Model): """ A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. """ - device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE) - name = models.CharField(max_length=50) - cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL, - verbose_name='Console server port', blank=True, null=True) - connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED) + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='console_ports' + ) + name = models.CharField( + max_length=50 + ) + cs_port = models.OneToOneField( + to='dcim.ConsoleServerPort', + on_delete=models.SET_NULL, + related_name='connected_console', + verbose_name='Console server port', + blank=True, + null=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + default=CONNECTION_STATUS_CONNECTED + ) csv_headers = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status'] @@ -1216,8 +1545,14 @@ class ConsoleServerPort(models.Model): """ A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. """ - device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE) - name = models.CharField(max_length=50) + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='cs_ports' + ) + name = models.CharField( + max_length=50 + ) objects = ConsoleServerPortManager() @@ -1251,11 +1586,25 @@ class PowerPort(models.Model): """ A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. """ - device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE) - name = models.CharField(max_length=50) - power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL, - blank=True, null=True) - connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED) + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='power_ports' + ) + name = models.CharField( + max_length=50 + ) + power_outlet = models.OneToOneField( + to='dcim.PowerOutlet', + on_delete=models.SET_NULL, + related_name='connected_port', + blank=True, + null=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + default=CONNECTION_STATUS_CONNECTED + ) csv_headers = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status'] @@ -1298,8 +1647,14 @@ class PowerOutlet(models.Model): """ A physical power outlet (output) within a Device which provides power to a PowerPort. """ - device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE) - name = models.CharField(max_length=50) + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='power_outlets' + ) + name = models.CharField( + max_length=50 + ) objects = PowerOutletManager() @@ -1356,17 +1711,35 @@ class Interface(models.Model): blank=True, verbose_name='Parent LAG' ) - name = models.CharField(max_length=64) - form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) - enabled = models.BooleanField(default=True) - mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address') - mtu = models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU') + name = models.CharField( + max_length=64 + ) + form_factor = models.PositiveSmallIntegerField( + choices=IFACE_FF_CHOICES, + default=IFACE_FF_10GE_SFP_PLUS + ) + enabled = models.BooleanField( + default=True + ) + mac_address = MACAddressField( + null=True, + blank=True, + verbose_name='MAC Address' + ) + mtu = models.PositiveSmallIntegerField( + blank=True, + null=True, + verbose_name='MTU' + ) mgmt_only = models.BooleanField( default=False, verbose_name='OOB Management', - help_text="This interface is used only for out-of-band management" + help_text='This interface is used only for out-of-band management' + ) + description = models.CharField( + max_length=100, + blank=True ) - description = models.CharField(max_length=100, blank=True) mode = models.PositiveSmallIntegerField( choices=IFACE_MODE_CHOICES, blank=True, @@ -1375,16 +1748,16 @@ class Interface(models.Model): untagged_vlan = models.ForeignKey( to='ipam.VLAN', on_delete=models.SET_NULL, + related_name='interfaces_as_untagged', null=True, blank=True, - verbose_name='Untagged VLAN', - related_name='interfaces_as_untagged' + verbose_name='Untagged VLAN' ) tagged_vlans = models.ManyToManyField( to='ipam.VLAN', + related_name='interfaces_as_tagged', blank=True, - verbose_name='Tagged VLANs', - related_name='interfaces_as_tagged' + verbose_name='Tagged VLANs' ) objects = InterfaceQuerySet.as_manager() @@ -1525,10 +1898,21 @@ class InterfaceConnection(models.Model): An InterfaceConnection represents a symmetrical, one-to-one connection between two Interfaces. There is no significant difference between the interface_a and interface_b fields. """ - interface_a = models.OneToOneField('Interface', related_name='connected_as_a', on_delete=models.CASCADE) - interface_b = models.OneToOneField('Interface', related_name='connected_as_b', on_delete=models.CASCADE) - connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED, - verbose_name='Status') + interface_a = models.OneToOneField( + to='dcim.Interface', + on_delete=models.CASCADE, + related_name='connected_as_a' + ) + interface_b = models.OneToOneField( + to='dcim.Interface', + on_delete=models.CASCADE, + related_name='connected_as_b' + ) + connection_status = models.BooleanField( + choices=CONNECTION_STATUS_CHOICES, + default=CONNECTION_STATUS_CONNECTED, + verbose_name='Status' + ) csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status'] @@ -1560,10 +1944,22 @@ class DeviceBay(models.Model): """ An empty space within a Device which can house a child device """ - device = models.ForeignKey('Device', related_name='device_bays', on_delete=models.CASCADE) - name = models.CharField(max_length=50, verbose_name='Name') - installed_device = models.OneToOneField('Device', related_name='parent_bay', on_delete=models.SET_NULL, blank=True, - null=True) + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='device_bays' + ) + name = models.CharField( + max_length=50, + verbose_name='Name' + ) + installed_device = models.OneToOneField( + to='dcim.Device', + on_delete=models.SET_NULL, + related_name='parent_bay', + blank=True, + null=True + ) class Meta: ordering = ['device', 'name'] @@ -1598,20 +1994,55 @@ class InventoryItem(models.Model): An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply. InventoryItems are used only for inventory purposes. """ - device = models.ForeignKey('Device', related_name='inventory_items', on_delete=models.CASCADE) - parent = models.ForeignKey('self', related_name='child_items', blank=True, null=True, on_delete=models.CASCADE) - name = models.CharField(max_length=50, verbose_name='Name') - manufacturer = models.ForeignKey( - 'Manufacturer', models.PROTECT, related_name='inventory_items', blank=True, null=True + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='inventory_items' + ) + parent = models.ForeignKey( + to='self', + on_delete=models.CASCADE, + related_name='child_items', + blank=True, + null=True + ) + name = models.CharField( + max_length=50, + verbose_name='Name' + ) + manufacturer = models.ForeignKey( + to='dcim.Manufacturer', + on_delete=models.PROTECT, + related_name='inventory_items', + blank=True, + null=True + ) + part_id = models.CharField( + max_length=50, + verbose_name='Part ID', + blank=True + ) + serial = models.CharField( + max_length=50, + verbose_name='Serial number', + blank=True ) - part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True) - serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True) asset_tag = NullableCharField( - max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag', + max_length=50, + unique=True, + blank=True, + null=True, + verbose_name='Asset tag', help_text='A unique tag used to identify this item' ) - discovered = models.BooleanField(default=False, verbose_name='Discovered') - description = models.CharField(max_length=100, blank=True) + discovered = models.BooleanField( + default=False, + verbose_name='Discovered' + ) + description = models.CharField( + max_length=100, + blank=True + ) csv_headers = [ 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 75945adcd..55db7ec25 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -73,7 +73,8 @@ class CustomField(models.Model): label = models.CharField( max_length=50, blank=True, - help_text='Name of the field as displayed to users (if not provided, the field\'s name will be used)' + help_text='Name of the field as displayed to users (if not provided, ' + 'the field\'s name will be used)' ) description = models.CharField( max_length=100, @@ -81,17 +82,20 @@ class CustomField(models.Model): ) required = models.BooleanField( default=False, - help_text='If true, this field is required when creating new objects or editing an existing object.' + help_text='If true, this field is required when creating new objects ' + 'or editing an existing object.' ) filter_logic = models.PositiveSmallIntegerField( choices=CF_FILTER_CHOICES, default=CF_FILTER_LOOSE, - help_text="Loose matches any instance of a given string; exact matches the entire field." + help_text='Loose matches any instance of a given string; exact ' + 'matches the entire field.' ) default = models.CharField( max_length=100, blank=True, - help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.' + help_text='Default value for the field. Use "true" or "false" for ' + 'booleans. N/A for selection fields.' ) weight = models.PositiveSmallIntegerField( default=100, @@ -143,11 +147,24 @@ class CustomField(models.Model): @python_2_unicode_compatible class CustomFieldValue(models.Model): - field = models.ForeignKey('CustomField', related_name='values', on_delete=models.CASCADE) - obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT) + field = models.ForeignKey( + to='extras.CustomField', + on_delete=models.CASCADE, + related_name='values' + ) + obj_type = models.ForeignKey( + to=ContentType, + on_delete=models.PROTECT, + related_name='+' + ) obj_id = models.PositiveIntegerField() - obj = GenericForeignKey('obj_type', 'obj_id') - serialized_value = models.CharField(max_length=255) + obj = GenericForeignKey( + ct_field='obj_type', + fk_field='obj_id' + ) + serialized_value = models.CharField( + max_length=255 + ) class Meta: ordering = ['obj_type', 'obj_id'] @@ -174,10 +191,19 @@ class CustomFieldValue(models.Model): @python_2_unicode_compatible class CustomFieldChoice(models.Model): - field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT}, - on_delete=models.CASCADE) - value = models.CharField(max_length=100) - weight = models.PositiveSmallIntegerField(default=100, help_text="Higher weights appear lower in the list") + field = models.ForeignKey( + to='extras.CustomField', + on_delete=models.CASCADE, + related_name='choices', + limit_choices_to={'type': CF_TYPE_SELECT} + ) + value = models.CharField( + max_length=100 + ) + weight = models.PositiveSmallIntegerField( + default=100, + help_text='Higher weights appear lower in the list' + ) class Meta: ordering = ['field', 'weight', 'value'] @@ -203,11 +229,24 @@ class CustomFieldChoice(models.Model): @python_2_unicode_compatible class Graph(models.Model): - type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES) - weight = models.PositiveSmallIntegerField(default=1000) - name = models.CharField(max_length=100, verbose_name='Name') - source = models.CharField(max_length=500, verbose_name='Source URL') - link = models.URLField(verbose_name='Link URL', blank=True) + type = models.PositiveSmallIntegerField( + choices=GRAPH_TYPE_CHOICES + ) + weight = models.PositiveSmallIntegerField( + default=1000 + ) + name = models.CharField( + max_length=100, + verbose_name='Name' + ) + source = models.CharField( + max_length=500, + verbose_name='Source URL' + ) + link = models.URLField( + blank=True, + verbose_name='Link URL' + ) class Meta: ordering = ['type', 'weight', 'name'] @@ -233,13 +272,26 @@ class Graph(models.Model): @python_2_unicode_compatible class ExportTemplate(models.Model): content_type = models.ForeignKey( - ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}, on_delete=models.CASCADE + to=ContentType, + on_delete=models.CASCADE, + limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS} + ) + name = models.CharField( + max_length=100 + ) + description = models.CharField( + max_length=200, + blank=True ) - name = models.CharField(max_length=100) - description = models.CharField(max_length=200, blank=True) template_code = models.TextField() - mime_type = models.CharField(max_length=15, blank=True) - file_extension = models.CharField(max_length=15, blank=True) + mime_type = models.CharField( + max_length=15, + blank=True + ) + file_extension = models.CharField( + max_length=15, + blank=True + ) class Meta: ordering = ['content_type', 'name'] @@ -278,25 +330,35 @@ class ExportTemplate(models.Model): @python_2_unicode_compatible class TopologyMap(models.Model): - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) type = models.PositiveSmallIntegerField( choices=TOPOLOGYMAP_TYPE_CHOICES, default=TOPOLOGYMAP_TYPE_NETWORK ) site = models.ForeignKey( to='dcim.Site', + on_delete=models.CASCADE, related_name='topology_maps', blank=True, - null=True, - on_delete=models.CASCADE + null=True ) device_patterns = models.TextField( - help_text="Identify devices to include in the diagram using regular expressions, one per line. Each line will " - "result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. " - "Devices will be rendered in the order they are defined." + help_text='Identify devices to include in the diagram using regular ' + 'expressions, one per line. Each line will result in a new ' + 'tier of the drawing. Separate multiple regexes within a ' + 'line using semicolons. Devices will be rendered in the ' + 'order they are defined.' + ) + description = models.CharField( + max_length=100, + blank=True ) - description = models.CharField(max_length=100, blank=True) class Meta: ordering = ['name'] @@ -432,14 +494,29 @@ class ImageAttachment(models.Model): """ An uploaded image which is associated with an object. """ - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + content_type = models.ForeignKey( + to=ContentType, + on_delete=models.CASCADE + ) object_id = models.PositiveIntegerField() - parent = GenericForeignKey('content_type', 'object_id') - image = models.ImageField(upload_to=image_upload, height_field='image_height', width_field='image_width') + parent = GenericForeignKey( + ct_field='content_type', + fk_field='object_id' + ) + image = models.ImageField( + upload_to=image_upload, + height_field='image_height', + width_field='image_width' + ) image_height = models.PositiveSmallIntegerField() image_width = models.PositiveSmallIntegerField() - name = models.CharField(max_length=50, blank=True) - created = models.DateTimeField(auto_now_add=True) + name = models.CharField( + max_length=50, + blank=True + ) + created = models.DateTimeField( + auto_now_add=True + ) class Meta: ordering = ['name'] @@ -482,9 +559,20 @@ class ReportResult(models.Model): """ This model stores the results from running a user-defined report. """ - report = models.CharField(max_length=255, unique=True) - created = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='+', blank=True, null=True) + report = models.CharField( + max_length=255, + unique=True + ) + created = models.DateTimeField( + auto_now_add=True + ) + user = models.ForeignKey( + to=User, + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) failed = models.BooleanField() data = JSONField() @@ -544,12 +632,29 @@ class UserAction(models.Model): """ A record of an action (add, edit, or delete) performed on an object by a User. """ - time = models.DateTimeField(auto_now_add=True, editable=False) - user = models.ForeignKey(User, related_name='actions', on_delete=models.CASCADE) - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField(blank=True, null=True) - action = models.PositiveSmallIntegerField(choices=ACTION_CHOICES) - message = models.TextField(blank=True) + time = models.DateTimeField( + auto_now_add=True, + editable=False + ) + user = models.ForeignKey( + to=User, + on_delete=models.CASCADE, + related_name='actions' + ) + content_type = models.ForeignKey( + to=ContentType, + on_delete=models.CASCADE + ) + object_id = models.PositiveIntegerField( + blank=True, + null=True + ) + action = models.PositiveSmallIntegerField( + choices=ACTION_CHOICES + ) + message = models.TextField( + blank=True + ) objects = UserActionManager() diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 9aea44229..2f83bb0f2 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -12,8 +12,7 @@ from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from dcim.models import Interface -from extras.models import CustomFieldModel, CustomFieldValue -from tenancy.models import Tenant +from extras.models import CustomFieldModel from utilities.models import CreatedUpdatedModel from .constants import * from .fields import IPNetworkField, IPAddressField @@ -27,13 +26,35 @@ class VRF(CreatedUpdatedModel, CustomFieldModel): 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=50) - rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher') - tenant = models.ForeignKey(Tenant, related_name='vrfs', blank=True, null=True, on_delete=models.PROTECT) - 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=100, blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') + name = models.CharField( + max_length=50 + ) + rd = models.CharField( + max_length=21, + unique=True, + verbose_name='Route distinguisher' + ) + 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=100, + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description'] @@ -70,10 +91,18 @@ class RIR(models.Model): 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. """ - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) - is_private = models.BooleanField(default=False, verbose_name='Private', - help_text='IP space managed by this RIR is considered private') + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) + is_private = models.BooleanField( + default=False, + verbose_name='Private', + help_text='IP space managed by this RIR is considered private' + ) csv_headers = ['name', 'slug', 'is_private'] @@ -102,12 +131,29 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): 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. """ - family = models.PositiveSmallIntegerField(choices=AF_CHOICES) + family = models.PositiveSmallIntegerField( + choices=AF_CHOICES + ) prefix = IPNetworkField() - rir = models.ForeignKey('RIR', related_name='aggregates', on_delete=models.PROTECT, verbose_name='RIR') - date_added = models.DateField(blank=True, null=True) - description = models.CharField(max_length=100, blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') + rir = models.ForeignKey( + to='ipam.RIR', + on_delete=models.PROTECT, + related_name='aggregates', + verbose_name='RIR' + ) + date_added = models.DateField( + blank=True, + null=True + ) + description = models.CharField( + max_length=100, + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) csv_headers = ['prefix', 'rir', 'date_added', 'description'] @@ -178,9 +224,16 @@ class Role(models.Model): A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or "Management." """ - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) - weight = models.PositiveSmallIntegerField(default=1000) + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) + weight = models.PositiveSmallIntegerField( + default=1000 + ) csv_headers = ['name', 'slug', 'weight'] @@ -205,22 +258,71 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be assigned to a VLAN where appropriate. """ - family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False) - prefix = IPNetworkField(help_text="IPv4 or IPv6 network with mask") - site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True) - vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True, - verbose_name='VRF') - tenant = models.ForeignKey(Tenant, related_name='prefixes', blank=True, null=True, on_delete=models.PROTECT) - vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True, - verbose_name='VLAN') - status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=PREFIX_STATUS_ACTIVE, - help_text="Operational status of this prefix") - role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True, - help_text="The primary function of this prefix") - is_pool = models.BooleanField(verbose_name='Is a pool', default=False, - help_text="All IP addresses within this prefix are considered usable") - description = models.CharField(max_length=100, blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') + family = models.PositiveSmallIntegerField( + choices=AF_CHOICES, + editable=False + ) + prefix = IPNetworkField( + help_text='IPv4 or IPv6 network with mask' + ) + site = models.ForeignKey( + to='dcim.Site', + on_delete=models.PROTECT, + related_name='prefixes', + blank=True, + null=True + ) + vrf = models.ForeignKey( + to='ipam.VRF', + on_delete=models.PROTECT, + related_name='prefixes', + blank=True, + null=True, + verbose_name='VRF' + ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='prefixes', + blank=True, + null=True + ) + vlan = models.ForeignKey( + to='ipam.VLAN', + on_delete=models.PROTECT, + related_name='prefixes', + blank=True, + null=True, + verbose_name='VLAN' + ) + status = models.PositiveSmallIntegerField( + choices=PREFIX_STATUS_CHOICES, + default=PREFIX_STATUS_ACTIVE, + verbose_name='Status', + help_text='Operational status of this prefix' + ) + role = models.ForeignKey( + to='ipam.Role', + on_delete=models.SET_NULL, + related_name='prefixes', + blank=True, + null=True, + help_text='The primary function of this prefix' + ) + is_pool = models.BooleanField( + verbose_name='Is a pool', + default=False, + help_text='All IP addresses within this prefix are considered usable' + ) + description = models.CharField( + max_length=100, + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) objects = PrefixQuerySet.as_manager() @@ -400,25 +502,66 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): for example, when mapping public addresses to private addresses. When an Interface has been assigned an IPAddress which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP. """ - family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False) - address = IPAddressField(help_text="IPv4 or IPv6 address (with mask)") - vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True, - verbose_name='VRF') - tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT) + family = models.PositiveSmallIntegerField( + choices=AF_CHOICES, + editable=False + ) + address = IPAddressField( + help_text='IPv4 or IPv6 address (with mask)' + ) + vrf = models.ForeignKey( + to='ipam.VRF', + on_delete=models.PROTECT, + related_name='ip_addresses', + blank=True, + null=True, + verbose_name='VRF' + ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='ip_addresses', + blank=True, + null=True + ) status = models.PositiveSmallIntegerField( - 'Status', choices=IPADDRESS_STATUS_CHOICES, default=IPADDRESS_STATUS_ACTIVE, + choices=IPADDRESS_STATUS_CHOICES, + default=IPADDRESS_STATUS_ACTIVE, + verbose_name='Status', help_text='The operational status of this IP' ) role = models.PositiveSmallIntegerField( - 'Role', choices=IPADDRESS_ROLE_CHOICES, blank=True, null=True, help_text='The functional role of this IP' + verbose_name='Role', + choices=IPADDRESS_ROLE_CHOICES, + blank=True, + null=True, + help_text='The functional role of this IP' + ) + interface = models.ForeignKey( + to='dcim.Interface', + on_delete=models.CASCADE, + related_name='ip_addresses', + blank=True, + null=True + ) + nat_inside = models.OneToOneField( + to='self', + on_delete=models.SET_NULL, + related_name='nat_outside', + blank=True, + null=True, + verbose_name='NAT (Inside)', + help_text='The IP for which this address is the "outside" IP' + ) + description = models.CharField( + max_length=100, + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' ) - interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True, - null=True) - nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True, - null=True, verbose_name='NAT (Inside)', - help_text="The IP for which this address is the \"outside\" IP") - description = models.CharField(max_length=100, blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') objects = IPAddressManager() @@ -509,9 +652,17 @@ class VLANGroup(models.Model): """ A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique. """ - name = models.CharField(max_length=50) + name = models.CharField( + max_length=50 + ) slug = models.SlugField() - site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.PROTECT, blank=True, null=True) + site = models.ForeignKey( + to='dcim.Site', + on_delete=models.PROTECT, + related_name='vlan_groups', + blank=True, + null=True + ) csv_headers = ['name', 'slug', 'site'] @@ -558,18 +709,55 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): 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('dcim.Site', related_name='vlans', on_delete=models.PROTECT, blank=True, null=True) - group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT) - vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[ - MinValueValidator(1), - MaxValueValidator(4094) - ]) - name = models.CharField(max_length=64) - tenant = models.ForeignKey(Tenant, related_name='vlans', blank=True, null=True, on_delete=models.PROTECT) - status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1) - role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True) - description = models.CharField(max_length=100, blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') + 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.PositiveSmallIntegerField( + choices=VLAN_STATUS_CHOICES, + default=1, + verbose_name='Status' + ) + role = models.ForeignKey( + to='ipam.Role', + on_delete=models.SET_NULL, + related_name='vlans', + blank=True, + null=True + ) + description = models.CharField( + max_length=100, + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description'] diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index e1f367d03..e39d46eef 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -13,7 +13,6 @@ from django.db import models from django.urls import reverse from django.utils.encoding import force_bytes, python_2_unicode_compatible -from dcim.models import Device from utilities.models import CreatedUpdatedModel from .exceptions import InvalidKey from .hashers import SecretValidationHasher @@ -54,9 +53,21 @@ class UserKey(CreatedUpdatedModel): copy of the master encryption key. The encrypted instance of the master key can be decrypted only with the user's matching (private) decryption key. """ - user = models.OneToOneField(User, related_name='user_key', editable=False, on_delete=models.CASCADE) - public_key = models.TextField(verbose_name='RSA public key') - master_key_cipher = models.BinaryField(max_length=512, blank=True, null=True, editable=False) + user = models.OneToOneField( + to=User, + on_delete=models.CASCADE, + related_name='user_key', + editable=False + ) + public_key = models.TextField( + verbose_name='RSA public key' + ) + master_key_cipher = models.BinaryField( + max_length=512, + blank=True, + null=True, + editable=False + ) objects = UserKeyQuerySet.as_manager() @@ -172,10 +183,23 @@ class SessionKey(models.Model): """ A SessionKey stores a User's temporary key to be used for the encryption and decryption of secrets. """ - userkey = models.OneToOneField(UserKey, related_name='session_key', on_delete=models.CASCADE, editable=False) - cipher = models.BinaryField(max_length=512, editable=False) - hash = models.CharField(max_length=128, editable=False) - created = models.DateTimeField(auto_now_add=True) + userkey = models.OneToOneField( + to='secrets.UserKey', + on_delete=models.CASCADE, + related_name='session_key', + editable=False + ) + cipher = models.BinaryField( + max_length=512, + editable=False + ) + hash = models.CharField( + max_length=128, + editable=False + ) + created = models.DateTimeField( + auto_now_add=True + ) key = None @@ -234,10 +258,23 @@ class SecretRole(models.Model): By default, only superusers will have access to decrypt Secrets. To allow other users to decrypt Secrets, grant them access to the appropriate SecretRoles either individually or by group. """ - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) - users = models.ManyToManyField(User, related_name='secretroles', blank=True) - groups = models.ManyToManyField(Group, related_name='secretroles', blank=True) + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) + users = models.ManyToManyField( + to=User, + related_name='secretroles', + blank=True + ) + groups = models.ManyToManyField( + to=Group, + related_name='secretroles', + blank=True + ) csv_headers = ['name', 'slug'] @@ -276,11 +313,28 @@ class Secret(CreatedUpdatedModel): A Secret can be up to 65,536 bytes (64KB) in length. Each secret string will be padded with random data to a minimum of 64 bytes during encryption in order to protect short strings from ciphertext analysis. """ - device = models.ForeignKey(Device, related_name='secrets', on_delete=models.CASCADE) - role = models.ForeignKey('SecretRole', related_name='secrets', on_delete=models.PROTECT) - name = models.CharField(max_length=100, blank=True) - ciphertext = models.BinaryField(editable=False, max_length=65568) # 16B IV + 2B pad length + {62-65550}B padded - hash = models.CharField(max_length=128, editable=False) + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='secrets' + ) + role = models.ForeignKey( + to='secrets.SecretRole', + on_delete=models.PROTECT, + related_name='secrets' + ) + name = models.CharField( + max_length=100, + blank=True + ) + ciphertext = models.BinaryField( + max_length=65568, # 16B IV + 2B pad length + {62-65550}B padded + editable=False + ) + hash = models.CharField( + max_length=128, + editable=False + ) plaintext = None csv_headers = ['device', 'role', 'name', 'plaintext'] diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 1fea2ceaf..9df714680 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -5,7 +5,7 @@ from django.db import models from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible -from extras.models import CustomFieldModel, CustomFieldValue +from extras.models import CustomFieldModel from utilities.models import CreatedUpdatedModel @@ -14,8 +14,13 @@ class TenantGroup(models.Model): """ An arbitrary collection of Tenants. """ - name = models.CharField(max_length=50, unique=True) - slug = models.SlugField(unique=True) + name = models.CharField( + max_length=50, + unique=True + ) + slug = models.SlugField( + unique=True + ) csv_headers = ['name', 'slug'] @@ -41,12 +46,33 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel): A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal department. """ - name = models.CharField(max_length=30, unique=True) - slug = models.SlugField(unique=True) - group = models.ForeignKey('TenantGroup', related_name='tenants', blank=True, null=True, on_delete=models.SET_NULL) - description = models.CharField(max_length=100, blank=True, help_text="Long-form name (optional)") - comments = models.TextField(blank=True) - custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') + name = models.CharField( + max_length=30, + unique=True + ) + slug = models.SlugField( + unique=True + ) + group = models.ForeignKey( + to='tenancy.TenantGroup', + on_delete=models.SET_NULL, + related_name='tenants', + blank=True, + null=True + ) + description = models.CharField( + max_length=100, + blank=True, + help_text='Long-form name (optional)' + ) + comments = models.TextField( + blank=True + ) + custom_field_values = GenericRelation( + to='extras.CustomFieldValue', + content_type_field='obj_type', + object_id_field='obj_id' + ) csv_headers = ['name', 'slug', 'group', 'description', 'comments'] diff --git a/netbox/users/models.py b/netbox/users/models.py index 02f5bc0a0..b3698d925 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -16,12 +16,31 @@ class Token(models.Model): 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. """ - user = models.ForeignKey(User, related_name='tokens', on_delete=models.CASCADE) - created = models.DateTimeField(auto_now_add=True) - expires = models.DateTimeField(blank=True, null=True) - key = models.CharField(max_length=40, unique=True, validators=[MinLengthValidator(40)]) - write_enabled = models.BooleanField(default=True, help_text="Permit create/update/delete operations using this key") - description = models.CharField(max_length=100, blank=True) + user = models.ForeignKey( + to=User, + on_delete=models.CASCADE, + related_name='tokens' + ) + created = models.DateTimeField( + auto_now_add=True + ) + expires = models.DateTimeField( + blank=True, + null=True + ) + key = models.CharField( + max_length=40, + unique=True, + validators=[MinLengthValidator(40)] + ) + write_enabled = models.BooleanField( + default=True, + help_text='Permit create/update/delete operations using this key' + ) + description = models.CharField( + max_length=100, + blank=True + ) class Meta: default_permissions = [] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 0a6abc400..b58cf93e8 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -8,7 +8,7 @@ from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from dcim.models import Device -from extras.models import CustomFieldModel, CustomFieldValue +from extras.models import CustomFieldModel from utilities.models import CreatedUpdatedModel from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES @@ -119,7 +119,7 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel): blank=True ) custom_field_values = GenericRelation( - to=CustomFieldValue, + to='extras.CustomFieldValue', content_type_field='obj_type', object_id_field='obj_id' ) @@ -167,7 +167,7 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel): A virtual machine which runs inside a Cluster. """ cluster = models.ForeignKey( - to=Cluster, + to='virtualization.Cluster', on_delete=models.PROTECT, related_name='virtual_machines' ) @@ -196,9 +196,9 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel): ) role = models.ForeignKey( to='dcim.DeviceRole', - limit_choices_to={'vm_role': True}, on_delete=models.PROTECT, related_name='virtual_machines', + limit_choices_to={'vm_role': True}, blank=True, null=True ) @@ -237,7 +237,7 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel): blank=True ) custom_field_values = GenericRelation( - to=CustomFieldValue, + to='extras.CustomFieldValue', content_type_field='obj_type', object_id_field='obj_id' )