Jeremy Stretch 7 лет назад
Родитель
Сommit
9725f19bae
8 измененных файлов с 1250 добавлено и 331 удалено
  1. 127 31
      netbox/circuits/models.py
  2. 585 154
      netbox/dcim/models.py
  3. 149 44
      netbox/extras/models.py
  4. 253 65
      netbox/ipam/models.py
  5. 71 17
      netbox/secrets/models.py
  6. 35 9
      netbox/tenancy/models.py
  7. 25 6
      netbox/users/models.py
  8. 5 5
      netbox/virtualization/models.py

+ 127 - 31
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.constants import STATUS_CLASSES
 from dcim.fields import ASNField
 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 utilities.models import CreatedUpdatedModel
 from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
 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
     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.
     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']
     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
     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".
     "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']
     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
     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.
     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 = [
     csv_headers = [
         'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
         'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
@@ -145,19 +213,47 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
 
 
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class CircuitTermination(models.Model):
 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)
+    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(
     interface = models.OneToOneField(
-        'dcim.Interface', related_name='circuit_termination', blank=True, null=True, on_delete=models.PROTECT
+        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(
     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'
         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:
     class Meta:
         ordering = ['circuit', 'term_side']
         ordering = ['circuit', 'term_side']

+ 585 - 154
netbox/dcim/models.py

@@ -17,9 +17,8 @@ from mptt.models import MPTTModel, TreeForeignKey
 from timezone_field import TimeZoneField
 from timezone_field import TimeZoneField
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
-from extras.models import CustomFieldModel, CustomFieldValue, ImageAttachment
+from extras.models import CustomFieldModel
 from extras.rpc import RPC_CLIENTS
 from extras.rpc import RPC_CLIENTS
-from tenancy.models import Tenant
 from utilities.fields import ColorField, NullableCharField
 from utilities.fields import ColorField, NullableCharField
 from utilities.managers import NaturalOrderByManager
 from utilities.managers import NaturalOrderByManager
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
@@ -38,10 +37,20 @@ class Region(MPTTModel):
     Sites can be grouped within geographic Regions.
     Sites can be grouped within geographic Regions.
     """
     """
     parent = TreeForeignKey(
     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']
     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
     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).
     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()
     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
     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.
     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()
     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']
     csv_headers = ['site', 'name', 'slug']
 
 
@@ -203,8 +273,13 @@ class RackRole(models.Model):
     """
     """
     Racks can be organized by functional role, similar to Devices.
     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()
     color = ColorField()
 
 
     csv_headers = ['name', 'slug', 'color']
     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.
     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.
     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()
     objects = RackManager()
 
 
@@ -438,12 +569,31 @@ class RackReservation(models.Model):
     """
     """
     One or more reserved units within a Rack.
     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:
     class Meta:
         ordering = ['created']
         ordering = ['created']
@@ -496,8 +646,13 @@ class Manufacturer(models.Model):
     """
     """
     A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
     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']
     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
     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.
     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()
     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 = [
     csv_headers = [
         'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
         '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.
     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:
     class Meta:
         ordering = ['device_type', 'name']
         ordering = ['device_type', 'name']
@@ -674,8 +871,14 @@ class ConsoleServerPortTemplate(models.Model):
     """
     """
     A template for a ConsoleServerPort to be created for a new Device.
     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:
     class Meta:
         ordering = ['device_type', 'name']
         ordering = ['device_type', 'name']
@@ -690,8 +893,14 @@ class PowerPortTemplate(models.Model):
     """
     """
     A template for a PowerPort to be created for a new Device.
     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:
     class Meta:
         ordering = ['device_type', 'name']
         ordering = ['device_type', 'name']
@@ -706,8 +915,14 @@ class PowerOutletTemplate(models.Model):
     """
     """
     A template for a PowerOutlet to be created for a new Device.
     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:
     class Meta:
         ordering = ['device_type', 'name']
         ordering = ['device_type', 'name']
@@ -722,10 +937,22 @@ class InterfaceTemplate(models.Model):
     """
     """
     A template for a physical data interface on a new Device.
     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()
     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.
     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:
     class Meta:
         ordering = ['device_type', 'name']
         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
     color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
     virtual machines as well.
     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()
     color = ColorField()
     vm_role = models.BooleanField(
     vm_role = models.BooleanField(
         default=True,
         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']
     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
     NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
     specifying a NAPALM driver.
     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(
     manufacturer = models.ForeignKey(
-        to='Manufacturer',
+        to='dcim.Manufacturer',
         on_delete=models.PROTECT,
         on_delete=models.PROTECT,
         related_name='platforms',
         related_name='platforms',
         blank=True,
         blank=True,
         null=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(
     napalm_driver = models.CharField(
         max_length=50,
         max_length=50,
         blank=True,
         blank=True,
         verbose_name='NAPALM driver',
         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(
     rpc_client = models.CharField(
         max_length=30,
         max_length=30,
         choices=RPC_CLIENT_CHOICES,
         choices=RPC_CLIENT_CHOICES,
         blank=True,
         blank=True,
-        verbose_name="Legacy RPC client"
+        verbose_name='Legacy RPC client'
     )
     )
 
 
     csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver']
     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
     by the component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the
     creation of a Device.
     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(
     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'
         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(
     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'
         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(
     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'
         verbose_name='Primary IPv4'
     )
     )
     primary_ip6 = models.OneToOneField(
     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'
         verbose_name='Primary IPv6'
     )
     )
     cluster = models.ForeignKey(
     cluster = models.ForeignKey(
@@ -912,9 +1218,17 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
         null=True,
         null=True,
         validators=[MaxValueValidator(255)]
         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()
     objects = DeviceManager()
 
 
@@ -1169,11 +1483,26 @@ class ConsolePort(models.Model):
     """
     """
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
     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']
     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.
     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()
     objects = ConsoleServerPortManager()
 
 
@@ -1251,11 +1586,25 @@ class PowerPort(models.Model):
     """
     """
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
     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']
     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.
     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()
     objects = PowerOutletManager()
 
 
@@ -1356,17 +1711,35 @@ class Interface(models.Model):
         blank=True,
         blank=True,
         verbose_name='Parent LAG'
         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(
     mgmt_only = models.BooleanField(
         default=False,
         default=False,
         verbose_name='OOB Management',
         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(
     mode = models.PositiveSmallIntegerField(
         choices=IFACE_MODE_CHOICES,
         choices=IFACE_MODE_CHOICES,
         blank=True,
         blank=True,
@@ -1375,16 +1748,16 @@ class Interface(models.Model):
     untagged_vlan = models.ForeignKey(
     untagged_vlan = models.ForeignKey(
         to='ipam.VLAN',
         to='ipam.VLAN',
         on_delete=models.SET_NULL,
         on_delete=models.SET_NULL,
+        related_name='interfaces_as_untagged',
         null=True,
         null=True,
         blank=True,
         blank=True,
-        verbose_name='Untagged VLAN',
-        related_name='interfaces_as_untagged'
+        verbose_name='Untagged VLAN'
     )
     )
     tagged_vlans = models.ManyToManyField(
     tagged_vlans = models.ManyToManyField(
         to='ipam.VLAN',
         to='ipam.VLAN',
+        related_name='interfaces_as_tagged',
         blank=True,
         blank=True,
-        verbose_name='Tagged VLANs',
-        related_name='interfaces_as_tagged'
+        verbose_name='Tagged VLANs'
     )
     )
 
 
     objects = InterfaceQuerySet.as_manager()
     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
     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.
     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']
     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
     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:
     class Meta:
         ordering = ['device', 'name']
         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.
     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.
     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')
+    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(
     manufacturer = models.ForeignKey(
-        'Manufacturer', models.PROTECT, related_name='inventory_items', blank=True, null=True
+        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(
     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'
         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 = [
     csv_headers = [
         'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
         'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',

+ 149 - 44
netbox/extras/models.py

@@ -73,7 +73,8 @@ class CustomField(models.Model):
     label = models.CharField(
     label = models.CharField(
         max_length=50,
         max_length=50,
         blank=True,
         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(
     description = models.CharField(
         max_length=100,
         max_length=100,
@@ -81,17 +82,20 @@ class CustomField(models.Model):
     )
     )
     required = models.BooleanField(
     required = models.BooleanField(
         default=False,
         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(
     filter_logic = models.PositiveSmallIntegerField(
         choices=CF_FILTER_CHOICES,
         choices=CF_FILTER_CHOICES,
         default=CF_FILTER_LOOSE,
         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(
     default = models.CharField(
         max_length=100,
         max_length=100,
         blank=True,
         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(
     weight = models.PositiveSmallIntegerField(
         default=100,
         default=100,
@@ -143,11 +147,24 @@ class CustomField(models.Model):
 
 
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class CustomFieldValue(models.Model):
 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_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:
     class Meta:
         ordering = ['obj_type', 'obj_id']
         ordering = ['obj_type', 'obj_id']
@@ -174,10 +191,19 @@ class CustomFieldValue(models.Model):
 
 
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class CustomFieldChoice(models.Model):
 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:
     class Meta:
         ordering = ['field', 'weight', 'value']
         ordering = ['field', 'weight', 'value']
@@ -203,11 +229,24 @@ class CustomFieldChoice(models.Model):
 
 
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class Graph(models.Model):
 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:
     class Meta:
         ordering = ['type', 'weight', 'name']
         ordering = ['type', 'weight', 'name']
@@ -233,13 +272,26 @@ class Graph(models.Model):
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class ExportTemplate(models.Model):
 class ExportTemplate(models.Model):
     content_type = models.ForeignKey(
     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()
     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:
     class Meta:
         ordering = ['content_type', 'name']
         ordering = ['content_type', 'name']
@@ -278,25 +330,35 @@ class ExportTemplate(models.Model):
 
 
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class TopologyMap(models.Model):
 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(
     type = models.PositiveSmallIntegerField(
         choices=TOPOLOGYMAP_TYPE_CHOICES,
         choices=TOPOLOGYMAP_TYPE_CHOICES,
         default=TOPOLOGYMAP_TYPE_NETWORK
         default=TOPOLOGYMAP_TYPE_NETWORK
     )
     )
     site = models.ForeignKey(
     site = models.ForeignKey(
         to='dcim.Site',
         to='dcim.Site',
+        on_delete=models.CASCADE,
         related_name='topology_maps',
         related_name='topology_maps',
         blank=True,
         blank=True,
-        null=True,
-        on_delete=models.CASCADE
+        null=True
     )
     )
     device_patterns = models.TextField(
     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:
     class Meta:
         ordering = ['name']
         ordering = ['name']
@@ -432,14 +494,29 @@ class ImageAttachment(models.Model):
     """
     """
     An uploaded image which is associated with an object.
     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()
     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_height = models.PositiveSmallIntegerField()
     image_width = 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:
     class Meta:
         ordering = ['name']
         ordering = ['name']
@@ -482,9 +559,20 @@ class ReportResult(models.Model):
     """
     """
     This model stores the results from running a user-defined report.
     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()
     failed = models.BooleanField()
     data = JSONField()
     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.
     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()
     objects = UserActionManager()
 
 

+ 253 - 65
netbox/ipam/models.py

@@ -12,8 +12,7 @@ from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.encoding import python_2_unicode_compatible
 
 
 from dcim.models import Interface
 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 utilities.models import CreatedUpdatedModel
 from .constants import *
 from .constants import *
 from .fields import IPNetworkField, IPAddressField
 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
     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.)
     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']
     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
     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.
     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']
     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
     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.
     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()
     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']
     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
     A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
     "Management."
     "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']
     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
     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.
     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()
     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
     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.
     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 = 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'
         help_text='The operational status of this IP'
     )
     )
     role = models.PositiveSmallIntegerField(
     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()
     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.
     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()
     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']
     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
     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.
     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']
     csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
 
 

+ 71 - 17
netbox/secrets/models.py

@@ -13,7 +13,6 @@ from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.encoding import force_bytes, python_2_unicode_compatible
 from django.utils.encoding import force_bytes, python_2_unicode_compatible
 
 
-from dcim.models import Device
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
 from .exceptions import InvalidKey
 from .exceptions import InvalidKey
 from .hashers import SecretValidationHasher
 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
     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.
     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()
     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.
     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
     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
     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.
     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']
     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
     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.
     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
     plaintext = None
     csv_headers = ['device', 'role', 'name', 'plaintext']
     csv_headers = ['device', 'role', 'name', 'plaintext']

+ 35 - 9
netbox/tenancy/models.py

@@ -5,7 +5,7 @@ from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
 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
 from utilities.models import CreatedUpdatedModel
 
 
 
 
@@ -14,8 +14,13 @@ class TenantGroup(models.Model):
     """
     """
     An arbitrary collection of Tenants.
     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']
     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
     A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
     department.
     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']
     csv_headers = ['name', 'slug', 'group', 'description', 'comments']
 
 

+ 25 - 6
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.
     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.
     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:
     class Meta:
         default_permissions = []
         default_permissions = []

+ 5 - 5
netbox/virtualization/models.py

@@ -8,7 +8,7 @@ from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.encoding import python_2_unicode_compatible
 
 
 from dcim.models import Device
 from dcim.models import Device
-from extras.models import CustomFieldModel, CustomFieldValue
+from extras.models import CustomFieldModel
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
 from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
 from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
 
 
@@ -119,7 +119,7 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
         blank=True
         blank=True
     )
     )
     custom_field_values = GenericRelation(
     custom_field_values = GenericRelation(
-        to=CustomFieldValue,
+        to='extras.CustomFieldValue',
         content_type_field='obj_type',
         content_type_field='obj_type',
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )
@@ -167,7 +167,7 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
     A virtual machine which runs inside a Cluster.
     A virtual machine which runs inside a Cluster.
     """
     """
     cluster = models.ForeignKey(
     cluster = models.ForeignKey(
-        to=Cluster,
+        to='virtualization.Cluster',
         on_delete=models.PROTECT,
         on_delete=models.PROTECT,
         related_name='virtual_machines'
         related_name='virtual_machines'
     )
     )
@@ -196,9 +196,9 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
     )
     )
     role = models.ForeignKey(
     role = models.ForeignKey(
         to='dcim.DeviceRole',
         to='dcim.DeviceRole',
-        limit_choices_to={'vm_role': True},
         on_delete=models.PROTECT,
         on_delete=models.PROTECT,
         related_name='virtual_machines',
         related_name='virtual_machines',
+        limit_choices_to={'vm_role': True},
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
@@ -237,7 +237,7 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
         blank=True
         blank=True
     )
     )
     custom_field_values = GenericRelation(
     custom_field_values = GenericRelation(
-        to=CustomFieldValue,
+        to='extras.CustomFieldValue',
         content_type_field='obj_type',
         content_type_field='obj_type',
         object_id_field='obj_id'
         object_id_field='obj_id'
     )
     )