فهرست منبع

Code formatting cleanup

Jeremy Stretch 7 سال پیش
والد
کامیت
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.fields import ASNField
-from extras.models import CustomFieldModel, CustomFieldValue
-from tenancy.models import Tenant
+from extras.models import CustomFieldModel
 from utilities.models import CreatedUpdatedModel
 from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
 
@@ -19,15 +18,43 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
     Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
     stores information pertinent to the user's relationship with the Provider.
     """
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
-    asn = ASNField(blank=True, null=True, verbose_name='ASN')
-    account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
-    portal_url = models.URLField(blank=True, verbose_name='Portal')
-    noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
-    admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
-    comments = models.TextField(blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
+    asn = ASNField(
+        blank=True,
+        null=True,
+        verbose_name='ASN'
+    )
+    account = models.CharField(
+        max_length=30,
+        blank=True,
+        verbose_name='Account number'
+    )
+    portal_url = models.URLField(
+        blank=True,
+        verbose_name='Portal'
+    )
+    noc_contact = models.TextField(
+        blank=True,
+        verbose_name='NOC contact'
+    )
+    admin_contact = models.TextField(
+        blank=True,
+        verbose_name='Admin contact'
+    )
+    comments = models.TextField(
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
+    )
 
     csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
 
@@ -59,8 +86,13 @@ class CircuitType(models.Model):
     Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
     "Long Haul," "Metro," or "Out-of-Band".
     """
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
 
     csv_headers = ['name', 'slug']
 
@@ -87,16 +119,52 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
     circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
     interface, but this is not required. Circuit port speed and commit rate are measured in Kbps.
     """
-    cid = models.CharField(max_length=50, verbose_name='Circuit ID')
-    provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
-    type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
-    status = models.PositiveSmallIntegerField(choices=CIRCUIT_STATUS_CHOICES, default=CIRCUIT_STATUS_ACTIVE)
-    tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
-    install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
-    commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
-    description = models.CharField(max_length=100, blank=True)
-    comments = models.TextField(blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
+    cid = models.CharField(
+        max_length=50,
+        verbose_name='Circuit ID'
+    )
+    provider = models.ForeignKey(
+        to='circuits.Provider',
+        on_delete=models.PROTECT,
+        related_name='circuits'
+    )
+    type = models.ForeignKey(
+        to='CircuitType',
+        on_delete=models.PROTECT,
+        related_name='circuits'
+    )
+    status = models.PositiveSmallIntegerField(
+        choices=CIRCUIT_STATUS_CHOICES,
+        default=CIRCUIT_STATUS_ACTIVE
+    )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='circuits',
+        blank=True,
+        null=True
+    )
+    install_date = models.DateField(
+        blank=True,
+        null=True,
+        verbose_name='Date installed'
+    )
+    commit_rate = models.PositiveIntegerField(
+        blank=True,
+        null=True,
+        verbose_name='Commit rate (Kbps)')
+    description = models.CharField(
+        max_length=100,
+        blank=True
+    )
+    comments = models.TextField(
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
+    )
 
     csv_headers = [
         'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
@@ -145,19 +213,47 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
 
 @python_2_unicode_compatible
 class CircuitTermination(models.Model):
-    circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
-    term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
-    site = models.ForeignKey('dcim.Site', related_name='circuit_terminations', on_delete=models.PROTECT)
+    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(
-        '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(
-        blank=True, null=True, verbose_name='Upstream speed (Kbps)',
+        blank=True,
+        null=True,
+        verbose_name='Upstream speed (Kbps)',
         help_text='Upstream speed, if different from port speed'
     )
-    xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
-    pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
+    xconnect_id = models.CharField(
+        max_length=50,
+        blank=True,
+        verbose_name='Cross-connect ID'
+    )
+    pp_info = models.CharField(
+        max_length=100,
+        blank=True,
+        verbose_name='Patch panel/port(s)'
+    )
 
     class Meta:
         ordering = ['circuit', 'term_side']

+ 585 - 154
netbox/dcim/models.py

@@ -17,9 +17,8 @@ from mptt.models import MPTTModel, TreeForeignKey
 from timezone_field import TimeZoneField
 
 from circuits.models import Circuit
-from extras.models import CustomFieldModel, CustomFieldValue, ImageAttachment
+from extras.models import CustomFieldModel
 from extras.rpc import RPC_CLIENTS
-from tenancy.models import Tenant
 from utilities.fields import ColorField, NullableCharField
 from utilities.managers import NaturalOrderByManager
 from utilities.models import CreatedUpdatedModel
@@ -38,10 +37,20 @@ class Region(MPTTModel):
     Sites can be grouped within geographic Regions.
     """
     parent = TreeForeignKey(
-        'self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.CASCADE
+        to='self',
+        on_delete=models.CASCADE,
+        related_name='children',
+        blank=True,
+        null=True,
+        db_index=True
+    )
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
     )
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
 
     csv_headers = ['name', 'slug', 'parent']
 
@@ -78,23 +87,78 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
     A Site represents a geographic location within a network; typically a building or campus. The optional facility
     field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
     """
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
-    status = models.PositiveSmallIntegerField(choices=SITE_STATUS_CHOICES, default=SITE_STATUS_ACTIVE)
-    region = models.ForeignKey('Region', related_name='sites', blank=True, null=True, on_delete=models.SET_NULL)
-    tenant = models.ForeignKey(Tenant, related_name='sites', blank=True, null=True, on_delete=models.PROTECT)
-    facility = models.CharField(max_length=50, blank=True)
-    asn = ASNField(blank=True, null=True, verbose_name='ASN')
-    time_zone = TimeZoneField(blank=True)
-    description = models.CharField(max_length=100, blank=True)
-    physical_address = models.CharField(max_length=200, blank=True)
-    shipping_address = models.CharField(max_length=200, blank=True)
-    contact_name = models.CharField(max_length=50, blank=True)
-    contact_phone = models.CharField(max_length=20, blank=True)
-    contact_email = models.EmailField(blank=True, verbose_name="Contact E-mail")
-    comments = models.TextField(blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
-    images = GenericRelation(ImageAttachment)
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
+    status = models.PositiveSmallIntegerField(
+        choices=SITE_STATUS_CHOICES,
+        default=SITE_STATUS_ACTIVE
+    )
+    region = models.ForeignKey(
+        to='dcim.Region',
+        on_delete=models.SET_NULL,
+        related_name='sites',
+        blank=True,
+        null=True
+    )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='sites',
+        blank=True,
+        null=True
+    )
+    facility = models.CharField(
+        max_length=50,
+        blank=True
+    )
+    asn = ASNField(
+        blank=True,
+        null=True,
+        verbose_name='ASN'
+    )
+    time_zone = TimeZoneField(
+        blank=True
+    )
+    description = models.CharField(
+        max_length=100,
+        blank=True
+    )
+    physical_address = models.CharField(
+        max_length=200,
+        blank=True
+    )
+    shipping_address = models.CharField(
+        max_length=200,
+        blank=True
+    )
+    contact_name = models.CharField(
+        max_length=50,
+        blank=True
+    )
+    contact_phone = models.CharField(
+        max_length=20,
+        blank=True
+    )
+    contact_email = models.EmailField(
+        blank=True,
+        verbose_name='Contact E-mail'
+    )
+    comments = models.TextField(
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
+    )
+    images = GenericRelation(
+        to='extras.ImageAttachment'
+    )
 
     objects = SiteManager()
 
@@ -171,9 +235,15 @@ class RackGroup(models.Model):
     example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
     campus. If a Site instead represents a single building, a RackGroup might represent a single room or floor.
     """
-    name = models.CharField(max_length=50)
+    name = models.CharField(
+        max_length=50
+    )
     slug = models.SlugField()
-    site = models.ForeignKey('Site', related_name='rack_groups', on_delete=models.CASCADE)
+    site = models.ForeignKey(
+        to='dcim.Site',
+        on_delete=models.CASCADE,
+        related_name='rack_groups'
+    )
 
     csv_headers = ['site', 'name', 'slug']
 
@@ -203,8 +273,13 @@ class RackRole(models.Model):
     """
     Racks can be organized by functional role, similar to Devices.
     """
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
     color = ColorField()
 
     csv_headers = ['name', 'slug', 'color']
@@ -238,23 +313,79 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
     Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
     Each Rack is assigned to a Site and (optionally) a RackGroup.
     """
-    name = models.CharField(max_length=50)
-    facility_id = NullableCharField(max_length=50, blank=True, null=True, verbose_name='Facility ID')
-    site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
-    group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
-    tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
-    role = models.ForeignKey('RackRole', related_name='racks', blank=True, null=True, on_delete=models.PROTECT)
-    serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
-    type = models.PositiveSmallIntegerField(choices=RACK_TYPE_CHOICES, blank=True, null=True, verbose_name='Type')
-    width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width',
-                                             help_text='Rail-to-rail width')
-    u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)',
-                                                validators=[MinValueValidator(1), MaxValueValidator(100)])
-    desc_units = models.BooleanField(default=False, verbose_name='Descending units',
-                                     help_text='Units are numbered top-to-bottom')
-    comments = models.TextField(blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
-    images = GenericRelation(ImageAttachment)
+    name = models.CharField(
+        max_length=50
+    )
+    facility_id = NullableCharField(
+        max_length=50,
+        blank=True,
+        null=True,
+        verbose_name='Facility ID'
+    )
+    site = models.ForeignKey(
+        to='dcim.Site',
+        on_delete=models.PROTECT,
+        related_name='racks'
+    )
+    group = models.ForeignKey(
+        to='dcim.RackGroup',
+        on_delete=models.SET_NULL,
+        related_name='racks',
+        blank=True,
+        null=True
+    )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='racks',
+        blank=True,
+        null=True
+    )
+    role = models.ForeignKey(
+        to='dcim.RackRole',
+        on_delete=models.PROTECT,
+        related_name='racks',
+        blank=True,
+        null=True
+    )
+    serial = models.CharField(
+        max_length=50,
+        blank=True,
+        verbose_name='Serial number'
+    )
+    type = models.PositiveSmallIntegerField(
+        choices=RACK_TYPE_CHOICES,
+        blank=True,
+        null=True,
+        verbose_name='Type'
+    )
+    width = models.PositiveSmallIntegerField(
+        choices=RACK_WIDTH_CHOICES,
+        default=RACK_WIDTH_19IN,
+        verbose_name='Width',
+        help_text='Rail-to-rail width'
+    )
+    u_height = models.PositiveSmallIntegerField(
+        default=42,
+        verbose_name='Height (U)',
+        validators=[MinValueValidator(1), MaxValueValidator(100)]
+    )
+    desc_units = models.BooleanField(
+        default=False,
+        verbose_name='Descending units',
+        help_text='Units are numbered top-to-bottom'
+    )
+    comments = models.TextField(
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
+    )
+    images = GenericRelation(
+        to='extras.ImageAttachment'
+    )
 
     objects = RackManager()
 
@@ -438,12 +569,31 @@ class RackReservation(models.Model):
     """
     One or more reserved units within a Rack.
     """
-    rack = models.ForeignKey('Rack', related_name='reservations', on_delete=models.CASCADE)
-    units = ArrayField(models.PositiveSmallIntegerField())
-    created = models.DateTimeField(auto_now_add=True)
-    tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='rackreservations', on_delete=models.PROTECT)
-    user = models.ForeignKey(User, on_delete=models.PROTECT)
-    description = models.CharField(max_length=100)
+    rack = models.ForeignKey(
+        to='dcim.Rack',
+        on_delete=models.CASCADE,
+        related_name='reservations'
+    )
+    units = ArrayField(
+        base_field=models.PositiveSmallIntegerField()
+    )
+    created = models.DateTimeField(
+        auto_now_add=True
+    )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='rackreservations',
+        blank=True,
+        null=True
+    )
+    user = models.ForeignKey(
+        to=User,
+        on_delete=models.PROTECT
+    )
+    description = models.CharField(
+        max_length=100
+    )
 
     class Meta:
         ordering = ['created']
@@ -496,8 +646,13 @@ class Manufacturer(models.Model):
     """
     A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
     """
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
 
     csv_headers = ['name', 'slug']
 
@@ -533,27 +688,63 @@ class DeviceType(models.Model, CustomFieldModel):
     When a new Device of this type is created, the appropriate console, power, and interface objects (as defined by the
     DeviceType) are automatically created as well.
     """
-    manufacturer = models.ForeignKey('Manufacturer', related_name='device_types', on_delete=models.PROTECT)
-    model = models.CharField(max_length=50)
+    manufacturer = models.ForeignKey(
+        to='dcim.Manufacturer',
+        on_delete=models.PROTECT,
+        related_name='device_types'
+    )
+    model = models.CharField(
+        max_length=50
+    )
     slug = models.SlugField()
-    part_number = models.CharField(max_length=50, blank=True, help_text="Discrete part number (optional)")
-    u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1)
-    is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth",
-                                        help_text="Device consumes both front and rear rack faces")
-    interface_ordering = models.PositiveSmallIntegerField(choices=IFACE_ORDERING_CHOICES,
-                                                          default=IFACE_ORDERING_POSITION)
-    is_console_server = models.BooleanField(default=False, verbose_name='Is a console server',
-                                            help_text="This type of device has console server ports")
-    is_pdu = models.BooleanField(default=False, verbose_name='Is a PDU',
-                                 help_text="This type of device has power outlets")
-    is_network_device = models.BooleanField(default=True, verbose_name='Is a network device',
-                                            help_text="This type of device has network interfaces")
-    subdevice_role = models.NullBooleanField(default=None, verbose_name='Parent/child status',
-                                             choices=SUBDEVICE_ROLE_CHOICES,
-                                             help_text="Parent devices house child devices in device bays. Select "
-                                                       "\"None\" if this device type is neither a parent nor a child.")
-    comments = models.TextField(blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
+    part_number = models.CharField(
+        max_length=50,
+        blank=True,
+        help_text='Discrete part number (optional)'
+    )
+    u_height = models.PositiveSmallIntegerField(
+        default=1,
+        verbose_name='Height (U)'
+    )
+    is_full_depth = models.BooleanField(
+        default=True,
+        verbose_name='Is full depth',
+        help_text='Device consumes both front and rear rack faces'
+    )
+    interface_ordering = models.PositiveSmallIntegerField(
+        choices=IFACE_ORDERING_CHOICES,
+        default=IFACE_ORDERING_POSITION
+    )
+    is_console_server = models.BooleanField(
+        default=False,
+        verbose_name='Is a console server',
+        help_text='This type of device has console server ports'
+    )
+    is_pdu = models.BooleanField(
+        default=False,
+        verbose_name='Is a PDU',
+        help_text='This type of device has power outlets'
+    )
+    is_network_device = models.BooleanField(
+        default=True,
+        verbose_name='Is a network device',
+        help_text='This type of device has network interfaces'
+    )
+    subdevice_role = models.NullBooleanField(
+        default=None,
+        verbose_name='Parent/child status',
+        choices=SUBDEVICE_ROLE_CHOICES,
+        help_text='Parent devices house child devices in device bays. Select '
+                  '"None" if this device type is neither a parent nor a child.'
+    )
+    comments = models.TextField(
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
+    )
 
     csv_headers = [
         'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
@@ -658,8 +849,14 @@ class ConsolePortTemplate(models.Model):
     """
     A template for a ConsolePort to be created for a new Device.
     """
-    device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE)
-    name = models.CharField(max_length=50)
+    device_type = models.ForeignKey(
+        to='dcim.DeviceType',
+        on_delete=models.CASCADE,
+        related_name='console_port_templates'
+    )
+    name = models.CharField(
+        max_length=50
+    )
 
     class Meta:
         ordering = ['device_type', 'name']
@@ -674,8 +871,14 @@ class ConsoleServerPortTemplate(models.Model):
     """
     A template for a ConsoleServerPort to be created for a new Device.
     """
-    device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE)
-    name = models.CharField(max_length=50)
+    device_type = models.ForeignKey(
+        to='dcim.DeviceType',
+        on_delete=models.CASCADE,
+        related_name='cs_port_templates'
+    )
+    name = models.CharField(
+        max_length=50
+    )
 
     class Meta:
         ordering = ['device_type', 'name']
@@ -690,8 +893,14 @@ class PowerPortTemplate(models.Model):
     """
     A template for a PowerPort to be created for a new Device.
     """
-    device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE)
-    name = models.CharField(max_length=50)
+    device_type = models.ForeignKey(
+        to='dcim.DeviceType',
+        on_delete=models.CASCADE,
+        related_name='power_port_templates'
+    )
+    name = models.CharField(
+        max_length=50
+    )
 
     class Meta:
         ordering = ['device_type', 'name']
@@ -706,8 +915,14 @@ class PowerOutletTemplate(models.Model):
     """
     A template for a PowerOutlet to be created for a new Device.
     """
-    device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE)
-    name = models.CharField(max_length=50)
+    device_type = models.ForeignKey(
+        to='dcim.DeviceType',
+        on_delete=models.CASCADE,
+        related_name='power_outlet_templates'
+    )
+    name = models.CharField(
+        max_length=50
+    )
 
     class Meta:
         ordering = ['device_type', 'name']
@@ -722,10 +937,22 @@ class InterfaceTemplate(models.Model):
     """
     A template for a physical data interface on a new Device.
     """
-    device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
-    name = models.CharField(max_length=64)
-    form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
-    mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
+    device_type = models.ForeignKey(
+        to='dcim.DeviceType',
+        on_delete=models.CASCADE,
+        related_name='interface_templates'
+    )
+    name = models.CharField(
+        max_length=64
+    )
+    form_factor = models.PositiveSmallIntegerField(
+        choices=IFACE_FF_CHOICES,
+        default=IFACE_FF_10GE_SFP_PLUS
+    )
+    mgmt_only = models.BooleanField(
+        default=False,
+        verbose_name='Management only'
+    )
 
     objects = InterfaceQuerySet.as_manager()
 
@@ -742,8 +969,14 @@ class DeviceBayTemplate(models.Model):
     """
     A template for a DeviceBay to be created for a new parent Device.
     """
-    device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE)
-    name = models.CharField(max_length=50)
+    device_type = models.ForeignKey(
+        to='dcim.DeviceType',
+        on_delete=models.CASCADE,
+        related_name='device_bay_templates'
+    )
+    name = models.CharField(
+        max_length=50
+    )
 
     class Meta:
         ordering = ['device_type', 'name']
@@ -764,13 +997,18 @@ class DeviceRole(models.Model):
     color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
     virtual machines as well.
     """
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
     color = ColorField()
     vm_role = models.BooleanField(
         default=True,
-        verbose_name="VM Role",
-        help_text="Virtual machines may be assigned to this role"
+        verbose_name='VM Role',
+        help_text='Virtual machines may be assigned to this role'
     )
 
     csv_headers = ['name', 'slug', 'color', 'vm_role']
@@ -800,27 +1038,32 @@ class Platform(models.Model):
     NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
     specifying a NAPALM driver.
     """
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
     manufacturer = models.ForeignKey(
-        to='Manufacturer',
+        to='dcim.Manufacturer',
         on_delete=models.PROTECT,
         related_name='platforms',
         blank=True,
         null=True,
-        help_text="Optionally limit this platform to devices of a certain manufacturer"
+        help_text='Optionally limit this platform to devices of a certain manufacturer'
     )
     napalm_driver = models.CharField(
         max_length=50,
         blank=True,
         verbose_name='NAPALM driver',
-        help_text="The name of the NAPALM driver to use when interacting with devices"
+        help_text='The name of the NAPALM driver to use when interacting with devices'
     )
     rpc_client = models.CharField(
         max_length=30,
         choices=RPC_CLIENT_CHOICES,
         blank=True,
-        verbose_name="Legacy RPC client"
+        verbose_name='Legacy RPC client'
     )
 
     csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver']
@@ -862,30 +1105,93 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
     by the component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the
     creation of a Device.
     """
-    device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
-    device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
-    tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='devices', on_delete=models.PROTECT)
-    platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
-    name = NullableCharField(max_length=64, blank=True, null=True, unique=True)
-    serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
+    device_type = models.ForeignKey(
+        to='dcim.DeviceType',
+        on_delete=models.PROTECT,
+        related_name='instances'
+    )
+    device_role = models.ForeignKey(
+        to='dcim.DeviceRole',
+        on_delete=models.PROTECT,
+        related_name='devices'
+    )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='devices',
+        blank=True,
+        null=True
+    )
+    platform = models.ForeignKey(
+        to='dcim.Platform',
+        on_delete=models.SET_NULL,
+        related_name='devices',
+        blank=True,
+        null=True
+    )
+    name = NullableCharField(
+        max_length=64,
+        blank=True,
+        null=True,
+        unique=True
+    )
+    serial = models.CharField(
+        max_length=50,
+        blank=True,
+        verbose_name='Serial number'
+    )
     asset_tag = NullableCharField(
-        max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
+        max_length=50,
+        blank=True,
+        null=True,
+        unique=True,
+        verbose_name='Asset tag',
         help_text='A unique tag used to identify this device'
     )
-    site = models.ForeignKey('Site', related_name='devices', on_delete=models.PROTECT)
-    rack = models.ForeignKey('Rack', related_name='devices', blank=True, null=True, on_delete=models.PROTECT)
+    site = models.ForeignKey(
+        to='dcim.Site',
+        on_delete=models.PROTECT,
+        related_name='devices'
+    )
+    rack = models.ForeignKey(
+        to='dcim.Rack',
+        on_delete=models.PROTECT,
+        related_name='devices',
+        blank=True,
+        null=True
+    )
     position = models.PositiveSmallIntegerField(
-        blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)',
+        blank=True,
+        null=True,
+        validators=[MinValueValidator(1)],
+        verbose_name='Position (U)',
         help_text='The lowest-numbered unit occupied by the device'
     )
-    face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
-    status = models.PositiveSmallIntegerField(choices=DEVICE_STATUS_CHOICES, default=DEVICE_STATUS_ACTIVE, verbose_name='Status')
+    face = models.PositiveSmallIntegerField(
+        blank=True,
+        null=True,
+        choices=RACK_FACE_CHOICES,
+        verbose_name='Rack face'
+    )
+    status = models.PositiveSmallIntegerField(
+        choices=DEVICE_STATUS_CHOICES,
+        default=DEVICE_STATUS_ACTIVE,
+        verbose_name='Status'
+    )
     primary_ip4 = models.OneToOneField(
-        'ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL, blank=True, null=True,
+        to='ipam.IPAddress',
+        on_delete=models.SET_NULL,
+        related_name='primary_ip4_for',
+        blank=True,
+        null=True,
         verbose_name='Primary IPv4'
     )
     primary_ip6 = models.OneToOneField(
-        'ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True,
+        to='ipam.IPAddress',
+        on_delete=models.SET_NULL,
+        related_name='primary_ip6_for',
+        blank=True,
+        null=True,
         verbose_name='Primary IPv6'
     )
     cluster = models.ForeignKey(
@@ -912,9 +1218,17 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
         null=True,
         validators=[MaxValueValidator(255)]
     )
-    comments = models.TextField(blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
-    images = GenericRelation(ImageAttachment)
+    comments = models.TextField(
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
+    )
+    images = GenericRelation(
+        to='extras.ImageAttachment'
+    )
 
     objects = DeviceManager()
 
@@ -1169,11 +1483,26 @@ class ConsolePort(models.Model):
     """
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
     """
-    device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE)
-    name = models.CharField(max_length=50)
-    cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL,
-                                   verbose_name='Console server port', blank=True, null=True)
-    connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
+    device = models.ForeignKey(
+        to='dcim.Device',
+        on_delete=models.CASCADE,
+        related_name='console_ports'
+    )
+    name = models.CharField(
+        max_length=50
+    )
+    cs_port = models.OneToOneField(
+        to='dcim.ConsoleServerPort',
+        on_delete=models.SET_NULL,
+        related_name='connected_console',
+        verbose_name='Console server port',
+        blank=True,
+        null=True
+    )
+    connection_status = models.NullBooleanField(
+        choices=CONNECTION_STATUS_CHOICES,
+        default=CONNECTION_STATUS_CONNECTED
+    )
 
     csv_headers = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
 
@@ -1216,8 +1545,14 @@ class ConsoleServerPort(models.Model):
     """
     A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
     """
-    device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE)
-    name = models.CharField(max_length=50)
+    device = models.ForeignKey(
+        to='dcim.Device',
+        on_delete=models.CASCADE,
+        related_name='cs_ports'
+    )
+    name = models.CharField(
+        max_length=50
+    )
 
     objects = ConsoleServerPortManager()
 
@@ -1251,11 +1586,25 @@ class PowerPort(models.Model):
     """
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
     """
-    device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE)
-    name = models.CharField(max_length=50)
-    power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL,
-                                        blank=True, null=True)
-    connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
+    device = models.ForeignKey(
+        to='dcim.Device',
+        on_delete=models.CASCADE,
+        related_name='power_ports'
+    )
+    name = models.CharField(
+        max_length=50
+    )
+    power_outlet = models.OneToOneField(
+        to='dcim.PowerOutlet',
+        on_delete=models.SET_NULL,
+        related_name='connected_port',
+        blank=True,
+        null=True
+    )
+    connection_status = models.NullBooleanField(
+        choices=CONNECTION_STATUS_CHOICES,
+        default=CONNECTION_STATUS_CONNECTED
+    )
 
     csv_headers = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
 
@@ -1298,8 +1647,14 @@ class PowerOutlet(models.Model):
     """
     A physical power outlet (output) within a Device which provides power to a PowerPort.
     """
-    device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE)
-    name = models.CharField(max_length=50)
+    device = models.ForeignKey(
+        to='dcim.Device',
+        on_delete=models.CASCADE,
+        related_name='power_outlets'
+    )
+    name = models.CharField(
+        max_length=50
+    )
 
     objects = PowerOutletManager()
 
@@ -1356,17 +1711,35 @@ class Interface(models.Model):
         blank=True,
         verbose_name='Parent LAG'
     )
-    name = models.CharField(max_length=64)
-    form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
-    enabled = models.BooleanField(default=True)
-    mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
-    mtu = models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU')
+    name = models.CharField(
+        max_length=64
+    )
+    form_factor = models.PositiveSmallIntegerField(
+        choices=IFACE_FF_CHOICES,
+        default=IFACE_FF_10GE_SFP_PLUS
+    )
+    enabled = models.BooleanField(
+        default=True
+    )
+    mac_address = MACAddressField(
+        null=True,
+        blank=True,
+        verbose_name='MAC Address'
+    )
+    mtu = models.PositiveSmallIntegerField(
+        blank=True,
+        null=True,
+        verbose_name='MTU'
+    )
     mgmt_only = models.BooleanField(
         default=False,
         verbose_name='OOB Management',
-        help_text="This interface is used only for out-of-band management"
+        help_text='This interface is used only for out-of-band management'
+    )
+    description = models.CharField(
+        max_length=100,
+        blank=True
     )
-    description = models.CharField(max_length=100, blank=True)
     mode = models.PositiveSmallIntegerField(
         choices=IFACE_MODE_CHOICES,
         blank=True,
@@ -1375,16 +1748,16 @@ class Interface(models.Model):
     untagged_vlan = models.ForeignKey(
         to='ipam.VLAN',
         on_delete=models.SET_NULL,
+        related_name='interfaces_as_untagged',
         null=True,
         blank=True,
-        verbose_name='Untagged VLAN',
-        related_name='interfaces_as_untagged'
+        verbose_name='Untagged VLAN'
     )
     tagged_vlans = models.ManyToManyField(
         to='ipam.VLAN',
+        related_name='interfaces_as_tagged',
         blank=True,
-        verbose_name='Tagged VLANs',
-        related_name='interfaces_as_tagged'
+        verbose_name='Tagged VLANs'
     )
 
     objects = InterfaceQuerySet.as_manager()
@@ -1525,10 +1898,21 @@ class InterfaceConnection(models.Model):
     An InterfaceConnection represents a symmetrical, one-to-one connection between two Interfaces. There is no
     significant difference between the interface_a and interface_b fields.
     """
-    interface_a = models.OneToOneField('Interface', related_name='connected_as_a', on_delete=models.CASCADE)
-    interface_b = models.OneToOneField('Interface', related_name='connected_as_b', on_delete=models.CASCADE)
-    connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED,
-                                            verbose_name='Status')
+    interface_a = models.OneToOneField(
+        to='dcim.Interface',
+        on_delete=models.CASCADE,
+        related_name='connected_as_a'
+    )
+    interface_b = models.OneToOneField(
+        to='dcim.Interface',
+        on_delete=models.CASCADE,
+        related_name='connected_as_b'
+    )
+    connection_status = models.BooleanField(
+        choices=CONNECTION_STATUS_CHOICES,
+        default=CONNECTION_STATUS_CONNECTED,
+        verbose_name='Status'
+    )
 
     csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
 
@@ -1560,10 +1944,22 @@ class DeviceBay(models.Model):
     """
     An empty space within a Device which can house a child device
     """
-    device = models.ForeignKey('Device', related_name='device_bays', on_delete=models.CASCADE)
-    name = models.CharField(max_length=50, verbose_name='Name')
-    installed_device = models.OneToOneField('Device', related_name='parent_bay', on_delete=models.SET_NULL, blank=True,
-                                            null=True)
+    device = models.ForeignKey(
+        to='dcim.Device',
+        on_delete=models.CASCADE,
+        related_name='device_bays'
+    )
+    name = models.CharField(
+        max_length=50,
+        verbose_name='Name'
+    )
+    installed_device = models.OneToOneField(
+        to='dcim.Device',
+        on_delete=models.SET_NULL,
+        related_name='parent_bay',
+        blank=True,
+        null=True
+    )
 
     class Meta:
         ordering = ['device', 'name']
@@ -1598,20 +1994,55 @@ class InventoryItem(models.Model):
     An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
     InventoryItems are used only for inventory purposes.
     """
-    device = models.ForeignKey('Device', related_name='inventory_items', on_delete=models.CASCADE)
-    parent = models.ForeignKey('self', related_name='child_items', blank=True, null=True, on_delete=models.CASCADE)
-    name = models.CharField(max_length=50, verbose_name='Name')
+    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.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(
-        max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
+        max_length=50,
+        unique=True,
+        blank=True,
+        null=True,
+        verbose_name='Asset tag',
         help_text='A unique tag used to identify this item'
     )
-    discovered = models.BooleanField(default=False, verbose_name='Discovered')
-    description = models.CharField(max_length=100, blank=True)
+    discovered = models.BooleanField(
+        default=False,
+        verbose_name='Discovered'
+    )
+    description = models.CharField(
+        max_length=100,
+        blank=True
+    )
 
     csv_headers = [
         'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',

+ 149 - 44
netbox/extras/models.py

@@ -73,7 +73,8 @@ class CustomField(models.Model):
     label = models.CharField(
         max_length=50,
         blank=True,
-        help_text='Name of the field as displayed to users (if not provided, the field\'s name will be used)'
+        help_text='Name of the field as displayed to users (if not provided, '
+                  'the field\'s name will be used)'
     )
     description = models.CharField(
         max_length=100,
@@ -81,17 +82,20 @@ class CustomField(models.Model):
     )
     required = models.BooleanField(
         default=False,
-        help_text='If true, this field is required when creating new objects or editing an existing object.'
+        help_text='If true, this field is required when creating new objects '
+                  'or editing an existing object.'
     )
     filter_logic = models.PositiveSmallIntegerField(
         choices=CF_FILTER_CHOICES,
         default=CF_FILTER_LOOSE,
-        help_text="Loose matches any instance of a given string; exact matches the entire field."
+        help_text='Loose matches any instance of a given string; exact '
+                  'matches the entire field.'
     )
     default = models.CharField(
         max_length=100,
         blank=True,
-        help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.'
+        help_text='Default value for the field. Use "true" or "false" for '
+                  'booleans. N/A for selection fields.'
     )
     weight = models.PositiveSmallIntegerField(
         default=100,
@@ -143,11 +147,24 @@ class CustomField(models.Model):
 
 @python_2_unicode_compatible
 class CustomFieldValue(models.Model):
-    field = models.ForeignKey('CustomField', related_name='values', on_delete=models.CASCADE)
-    obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
+    field = models.ForeignKey(
+        to='extras.CustomField',
+        on_delete=models.CASCADE,
+        related_name='values'
+    )
+    obj_type = models.ForeignKey(
+        to=ContentType,
+        on_delete=models.PROTECT,
+        related_name='+'
+    )
     obj_id = models.PositiveIntegerField()
-    obj = GenericForeignKey('obj_type', 'obj_id')
-    serialized_value = models.CharField(max_length=255)
+    obj = GenericForeignKey(
+        ct_field='obj_type',
+        fk_field='obj_id'
+    )
+    serialized_value = models.CharField(
+        max_length=255
+    )
 
     class Meta:
         ordering = ['obj_type', 'obj_id']
@@ -174,10 +191,19 @@ class CustomFieldValue(models.Model):
 
 @python_2_unicode_compatible
 class CustomFieldChoice(models.Model):
-    field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
-                              on_delete=models.CASCADE)
-    value = models.CharField(max_length=100)
-    weight = models.PositiveSmallIntegerField(default=100, help_text="Higher weights appear lower in the list")
+    field = models.ForeignKey(
+        to='extras.CustomField',
+        on_delete=models.CASCADE,
+        related_name='choices',
+        limit_choices_to={'type': CF_TYPE_SELECT}
+    )
+    value = models.CharField(
+        max_length=100
+    )
+    weight = models.PositiveSmallIntegerField(
+        default=100,
+        help_text='Higher weights appear lower in the list'
+    )
 
     class Meta:
         ordering = ['field', 'weight', 'value']
@@ -203,11 +229,24 @@ class CustomFieldChoice(models.Model):
 
 @python_2_unicode_compatible
 class Graph(models.Model):
-    type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
-    weight = models.PositiveSmallIntegerField(default=1000)
-    name = models.CharField(max_length=100, verbose_name='Name')
-    source = models.CharField(max_length=500, verbose_name='Source URL')
-    link = models.URLField(verbose_name='Link URL', blank=True)
+    type = models.PositiveSmallIntegerField(
+        choices=GRAPH_TYPE_CHOICES
+    )
+    weight = models.PositiveSmallIntegerField(
+        default=1000
+    )
+    name = models.CharField(
+        max_length=100,
+        verbose_name='Name'
+    )
+    source = models.CharField(
+        max_length=500,
+        verbose_name='Source URL'
+    )
+    link = models.URLField(
+        blank=True,
+        verbose_name='Link URL'
+    )
 
     class Meta:
         ordering = ['type', 'weight', 'name']
@@ -233,13 +272,26 @@ class Graph(models.Model):
 @python_2_unicode_compatible
 class ExportTemplate(models.Model):
     content_type = models.ForeignKey(
-        ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}, on_delete=models.CASCADE
+        to=ContentType,
+        on_delete=models.CASCADE,
+        limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}
+    )
+    name = models.CharField(
+        max_length=100
+    )
+    description = models.CharField(
+        max_length=200,
+        blank=True
     )
-    name = models.CharField(max_length=100)
-    description = models.CharField(max_length=200, blank=True)
     template_code = models.TextField()
-    mime_type = models.CharField(max_length=15, blank=True)
-    file_extension = models.CharField(max_length=15, blank=True)
+    mime_type = models.CharField(
+        max_length=15,
+        blank=True
+    )
+    file_extension = models.CharField(
+        max_length=15,
+        blank=True
+    )
 
     class Meta:
         ordering = ['content_type', 'name']
@@ -278,25 +330,35 @@ class ExportTemplate(models.Model):
 
 @python_2_unicode_compatible
 class TopologyMap(models.Model):
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
     type = models.PositiveSmallIntegerField(
         choices=TOPOLOGYMAP_TYPE_CHOICES,
         default=TOPOLOGYMAP_TYPE_NETWORK
     )
     site = models.ForeignKey(
         to='dcim.Site',
+        on_delete=models.CASCADE,
         related_name='topology_maps',
         blank=True,
-        null=True,
-        on_delete=models.CASCADE
+        null=True
     )
     device_patterns = models.TextField(
-        help_text="Identify devices to include in the diagram using regular expressions, one per line. Each line will "
-                  "result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. "
-                  "Devices will be rendered in the order they are defined."
+        help_text='Identify devices to include in the diagram using regular '
+                  'expressions, one per line. Each line will result in a new '
+                  'tier of the drawing. Separate multiple regexes within a '
+                  'line using semicolons. Devices will be rendered in the '
+                  'order they are defined.'
+    )
+    description = models.CharField(
+        max_length=100,
+        blank=True
     )
-    description = models.CharField(max_length=100, blank=True)
 
     class Meta:
         ordering = ['name']
@@ -432,14 +494,29 @@ class ImageAttachment(models.Model):
     """
     An uploaded image which is associated with an object.
     """
-    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
+    content_type = models.ForeignKey(
+        to=ContentType,
+        on_delete=models.CASCADE
+    )
     object_id = models.PositiveIntegerField()
-    parent = GenericForeignKey('content_type', 'object_id')
-    image = models.ImageField(upload_to=image_upload, height_field='image_height', width_field='image_width')
+    parent = GenericForeignKey(
+        ct_field='content_type',
+        fk_field='object_id'
+    )
+    image = models.ImageField(
+        upload_to=image_upload,
+        height_field='image_height',
+        width_field='image_width'
+    )
     image_height = models.PositiveSmallIntegerField()
     image_width = models.PositiveSmallIntegerField()
-    name = models.CharField(max_length=50, blank=True)
-    created = models.DateTimeField(auto_now_add=True)
+    name = models.CharField(
+        max_length=50,
+        blank=True
+    )
+    created = models.DateTimeField(
+        auto_now_add=True
+    )
 
     class Meta:
         ordering = ['name']
@@ -482,9 +559,20 @@ class ReportResult(models.Model):
     """
     This model stores the results from running a user-defined report.
     """
-    report = models.CharField(max_length=255, unique=True)
-    created = models.DateTimeField(auto_now_add=True)
-    user = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='+', blank=True, null=True)
+    report = models.CharField(
+        max_length=255,
+        unique=True
+    )
+    created = models.DateTimeField(
+        auto_now_add=True
+    )
+    user = models.ForeignKey(
+        to=User,
+        on_delete=models.SET_NULL,
+        related_name='+',
+        blank=True,
+        null=True
+    )
     failed = models.BooleanField()
     data = JSONField()
 
@@ -544,12 +632,29 @@ class UserAction(models.Model):
     """
     A record of an action (add, edit, or delete) performed on an object by a User.
     """
-    time = models.DateTimeField(auto_now_add=True, editable=False)
-    user = models.ForeignKey(User, related_name='actions', on_delete=models.CASCADE)
-    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
-    object_id = models.PositiveIntegerField(blank=True, null=True)
-    action = models.PositiveSmallIntegerField(choices=ACTION_CHOICES)
-    message = models.TextField(blank=True)
+    time = models.DateTimeField(
+        auto_now_add=True,
+        editable=False
+    )
+    user = models.ForeignKey(
+        to=User,
+        on_delete=models.CASCADE,
+        related_name='actions'
+    )
+    content_type = models.ForeignKey(
+        to=ContentType,
+        on_delete=models.CASCADE
+    )
+    object_id = models.PositiveIntegerField(
+        blank=True,
+        null=True
+    )
+    action = models.PositiveSmallIntegerField(
+        choices=ACTION_CHOICES
+    )
+    message = models.TextField(
+        blank=True
+    )
 
     objects = UserActionManager()
 

+ 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 dcim.models import Interface
-from extras.models import CustomFieldModel, CustomFieldValue
-from tenancy.models import Tenant
+from extras.models import CustomFieldModel
 from utilities.models import CreatedUpdatedModel
 from .constants import *
 from .fields import IPNetworkField, IPAddressField
@@ -27,13 +26,35 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
     table). Prefixes and IPAddresses can optionally be assigned to VRFs. (Prefixes and IPAddresses not assigned to a VRF
     are said to exist in the "global" table.)
     """
-    name = models.CharField(max_length=50)
-    rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
-    tenant = models.ForeignKey(Tenant, related_name='vrfs', blank=True, null=True, on_delete=models.PROTECT)
-    enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space',
-                                         help_text="Prevent duplicate prefixes/IP addresses within this VRF")
-    description = models.CharField(max_length=100, blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
+    name = models.CharField(
+        max_length=50
+    )
+    rd = models.CharField(
+        max_length=21,
+        unique=True,
+        verbose_name='Route distinguisher'
+    )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='vrfs',
+        blank=True,
+        null=True
+    )
+    enforce_unique = models.BooleanField(
+        default=True,
+        verbose_name='Enforce unique space',
+        help_text='Prevent duplicate prefixes/IP addresses within this VRF'
+    )
+    description = models.CharField(
+        max_length=100,
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
+    )
 
     csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
 
@@ -70,10 +91,18 @@ class RIR(models.Model):
     A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
     space. This can be an organization like ARIN or RIPE, or a governing standard such as RFC 1918.
     """
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
-    is_private = models.BooleanField(default=False, verbose_name='Private',
-                                     help_text='IP space managed by this RIR is considered private')
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
+    is_private = models.BooleanField(
+        default=False,
+        verbose_name='Private',
+        help_text='IP space managed by this RIR is considered private'
+    )
 
     csv_headers = ['name', 'slug', 'is_private']
 
@@ -102,12 +131,29 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
     An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
     the hierarchy and track the overall utilization of available address space. Each Aggregate is assigned to a RIR.
     """
-    family = models.PositiveSmallIntegerField(choices=AF_CHOICES)
+    family = models.PositiveSmallIntegerField(
+        choices=AF_CHOICES
+    )
     prefix = IPNetworkField()
-    rir = models.ForeignKey('RIR', related_name='aggregates', on_delete=models.PROTECT, verbose_name='RIR')
-    date_added = models.DateField(blank=True, null=True)
-    description = models.CharField(max_length=100, blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
+    rir = models.ForeignKey(
+        to='ipam.RIR',
+        on_delete=models.PROTECT,
+        related_name='aggregates',
+        verbose_name='RIR'
+    )
+    date_added = models.DateField(
+        blank=True,
+        null=True
+    )
+    description = models.CharField(
+        max_length=100,
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
+    )
 
     csv_headers = ['prefix', 'rir', 'date_added', 'description']
 
@@ -178,9 +224,16 @@ class Role(models.Model):
     A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
     "Management."
     """
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
-    weight = models.PositiveSmallIntegerField(default=1000)
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
+    weight = models.PositiveSmallIntegerField(
+        default=1000
+    )
 
     csv_headers = ['name', 'slug', 'weight']
 
@@ -205,22 +258,71 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
     VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be
     assigned to a VLAN where appropriate.
     """
-    family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
-    prefix = IPNetworkField(help_text="IPv4 or IPv6 network with mask")
-    site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True)
-    vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
-                            verbose_name='VRF')
-    tenant = models.ForeignKey(Tenant, related_name='prefixes', blank=True, null=True, on_delete=models.PROTECT)
-    vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
-                             verbose_name='VLAN')
-    status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=PREFIX_STATUS_ACTIVE,
-                                              help_text="Operational status of this prefix")
-    role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True,
-                             help_text="The primary function of this prefix")
-    is_pool = models.BooleanField(verbose_name='Is a pool', default=False,
-                                  help_text="All IP addresses within this prefix are considered usable")
-    description = models.CharField(max_length=100, blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
+    family = models.PositiveSmallIntegerField(
+        choices=AF_CHOICES,
+        editable=False
+    )
+    prefix = IPNetworkField(
+        help_text='IPv4 or IPv6 network with mask'
+    )
+    site = models.ForeignKey(
+        to='dcim.Site',
+        on_delete=models.PROTECT,
+        related_name='prefixes',
+        blank=True,
+        null=True
+    )
+    vrf = models.ForeignKey(
+        to='ipam.VRF',
+        on_delete=models.PROTECT,
+        related_name='prefixes',
+        blank=True,
+        null=True,
+        verbose_name='VRF'
+    )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='prefixes',
+        blank=True,
+        null=True
+    )
+    vlan = models.ForeignKey(
+        to='ipam.VLAN',
+        on_delete=models.PROTECT,
+        related_name='prefixes',
+        blank=True,
+        null=True,
+        verbose_name='VLAN'
+    )
+    status = models.PositiveSmallIntegerField(
+        choices=PREFIX_STATUS_CHOICES,
+        default=PREFIX_STATUS_ACTIVE,
+        verbose_name='Status',
+        help_text='Operational status of this prefix'
+    )
+    role = models.ForeignKey(
+        to='ipam.Role',
+        on_delete=models.SET_NULL,
+        related_name='prefixes',
+        blank=True,
+        null=True,
+        help_text='The primary function of this prefix'
+    )
+    is_pool = models.BooleanField(
+        verbose_name='Is a pool',
+        default=False,
+        help_text='All IP addresses within this prefix are considered usable'
+    )
+    description = models.CharField(
+        max_length=100,
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
+    )
 
     objects = PrefixQuerySet.as_manager()
 
@@ -400,25 +502,66 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
     for example, when mapping public addresses to private addresses. When an Interface has been assigned an IPAddress
     which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP.
     """
-    family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
-    address = IPAddressField(help_text="IPv4 or IPv6 address (with mask)")
-    vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
-                            verbose_name='VRF')
-    tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
+    family = models.PositiveSmallIntegerField(
+        choices=AF_CHOICES,
+        editable=False
+    )
+    address = IPAddressField(
+        help_text='IPv4 or IPv6 address (with mask)'
+    )
+    vrf = models.ForeignKey(
+        to='ipam.VRF',
+        on_delete=models.PROTECT,
+        related_name='ip_addresses',
+        blank=True,
+        null=True,
+        verbose_name='VRF'
+    )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='ip_addresses',
+        blank=True,
+        null=True
+    )
     status = models.PositiveSmallIntegerField(
-        'Status', choices=IPADDRESS_STATUS_CHOICES, default=IPADDRESS_STATUS_ACTIVE,
+        choices=IPADDRESS_STATUS_CHOICES,
+        default=IPADDRESS_STATUS_ACTIVE,
+        verbose_name='Status',
         help_text='The operational status of this IP'
     )
     role = models.PositiveSmallIntegerField(
-        'Role', choices=IPADDRESS_ROLE_CHOICES, blank=True, null=True, help_text='The functional role of this IP'
+        verbose_name='Role',
+        choices=IPADDRESS_ROLE_CHOICES,
+        blank=True,
+        null=True,
+        help_text='The functional role of this IP'
+    )
+    interface = models.ForeignKey(
+        to='dcim.Interface',
+        on_delete=models.CASCADE,
+        related_name='ip_addresses',
+        blank=True,
+        null=True
+    )
+    nat_inside = models.OneToOneField(
+        to='self',
+        on_delete=models.SET_NULL,
+        related_name='nat_outside',
+        blank=True,
+        null=True,
+        verbose_name='NAT (Inside)',
+        help_text='The IP for which this address is the "outside" IP'
+    )
+    description = models.CharField(
+        max_length=100,
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
     )
-    interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
-                                  null=True)
-    nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
-                                      null=True, verbose_name='NAT (Inside)',
-                                      help_text="The IP for which this address is the \"outside\" IP")
-    description = models.CharField(max_length=100, blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     objects = IPAddressManager()
 
@@ -509,9 +652,17 @@ class VLANGroup(models.Model):
     """
     A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
     """
-    name = models.CharField(max_length=50)
+    name = models.CharField(
+        max_length=50
+    )
     slug = models.SlugField()
-    site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.PROTECT, blank=True, null=True)
+    site = models.ForeignKey(
+        to='dcim.Site',
+        on_delete=models.PROTECT,
+        related_name='vlan_groups',
+        blank=True,
+        null=True
+    )
 
     csv_headers = ['name', 'slug', 'site']
 
@@ -558,18 +709,55 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
     Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
     or more Prefixes assigned to it.
     """
-    site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT, blank=True, null=True)
-    group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
-    vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
-        MinValueValidator(1),
-        MaxValueValidator(4094)
-    ])
-    name = models.CharField(max_length=64)
-    tenant = models.ForeignKey(Tenant, related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
-    status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
-    role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
-    description = models.CharField(max_length=100, blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
+    site = models.ForeignKey(
+        to='dcim.Site',
+        on_delete=models.PROTECT,
+        related_name='vlans',
+        blank=True,
+        null=True
+    )
+    group = models.ForeignKey(
+        to='ipam.VLANGroup',
+        on_delete=models.PROTECT,
+        related_name='vlans',
+        blank=True,
+        null=True
+    )
+    vid = models.PositiveSmallIntegerField(
+        verbose_name='ID',
+        validators=[MinValueValidator(1), MaxValueValidator(4094)]
+    )
+    name = models.CharField(
+        max_length=64
+    )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='vlans',
+        blank=True,
+        null=True
+    )
+    status = models.PositiveSmallIntegerField(
+        choices=VLAN_STATUS_CHOICES,
+        default=1,
+        verbose_name='Status'
+    )
+    role = models.ForeignKey(
+        to='ipam.Role',
+        on_delete=models.SET_NULL,
+        related_name='vlans',
+        blank=True,
+        null=True
+    )
+    description = models.CharField(
+        max_length=100,
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
+    )
 
     csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
 

+ 71 - 17
netbox/secrets/models.py

@@ -13,7 +13,6 @@ from django.db import models
 from django.urls import reverse
 from django.utils.encoding import force_bytes, python_2_unicode_compatible
 
-from dcim.models import Device
 from utilities.models import CreatedUpdatedModel
 from .exceptions import InvalidKey
 from .hashers import SecretValidationHasher
@@ -54,9 +53,21 @@ class UserKey(CreatedUpdatedModel):
     copy of the master encryption key. The encrypted instance of the master key can be decrypted only with the user's
     matching (private) decryption key.
     """
-    user = models.OneToOneField(User, related_name='user_key', editable=False, on_delete=models.CASCADE)
-    public_key = models.TextField(verbose_name='RSA public key')
-    master_key_cipher = models.BinaryField(max_length=512, blank=True, null=True, editable=False)
+    user = models.OneToOneField(
+        to=User,
+        on_delete=models.CASCADE,
+        related_name='user_key',
+        editable=False
+    )
+    public_key = models.TextField(
+        verbose_name='RSA public key'
+    )
+    master_key_cipher = models.BinaryField(
+        max_length=512,
+        blank=True,
+        null=True,
+        editable=False
+    )
 
     objects = UserKeyQuerySet.as_manager()
 
@@ -172,10 +183,23 @@ class SessionKey(models.Model):
     """
     A SessionKey stores a User's temporary key to be used for the encryption and decryption of secrets.
     """
-    userkey = models.OneToOneField(UserKey, related_name='session_key', on_delete=models.CASCADE, editable=False)
-    cipher = models.BinaryField(max_length=512, editable=False)
-    hash = models.CharField(max_length=128, editable=False)
-    created = models.DateTimeField(auto_now_add=True)
+    userkey = models.OneToOneField(
+        to='secrets.UserKey',
+        on_delete=models.CASCADE,
+        related_name='session_key',
+        editable=False
+    )
+    cipher = models.BinaryField(
+        max_length=512,
+        editable=False
+    )
+    hash = models.CharField(
+        max_length=128,
+        editable=False
+    )
+    created = models.DateTimeField(
+        auto_now_add=True
+    )
 
     key = None
 
@@ -234,10 +258,23 @@ class SecretRole(models.Model):
     By default, only superusers will have access to decrypt Secrets. To allow other users to decrypt Secrets, grant them
     access to the appropriate SecretRoles either individually or by group.
     """
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
-    users = models.ManyToManyField(User, related_name='secretroles', blank=True)
-    groups = models.ManyToManyField(Group, related_name='secretroles', blank=True)
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
+    users = models.ManyToManyField(
+        to=User,
+        related_name='secretroles',
+        blank=True
+    )
+    groups = models.ManyToManyField(
+        to=Group,
+        related_name='secretroles',
+        blank=True
+    )
 
     csv_headers = ['name', 'slug']
 
@@ -276,11 +313,28 @@ class Secret(CreatedUpdatedModel):
     A Secret can be up to 65,536 bytes (64KB) in length. Each secret string will be padded with random data to a minimum
     of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
     """
-    device = models.ForeignKey(Device, related_name='secrets', on_delete=models.CASCADE)
-    role = models.ForeignKey('SecretRole', related_name='secrets', on_delete=models.PROTECT)
-    name = models.CharField(max_length=100, blank=True)
-    ciphertext = models.BinaryField(editable=False, max_length=65568)  # 16B IV + 2B pad length + {62-65550}B padded
-    hash = models.CharField(max_length=128, editable=False)
+    device = models.ForeignKey(
+        to='dcim.Device',
+        on_delete=models.CASCADE,
+        related_name='secrets'
+    )
+    role = models.ForeignKey(
+        to='secrets.SecretRole',
+        on_delete=models.PROTECT,
+        related_name='secrets'
+    )
+    name = models.CharField(
+        max_length=100,
+        blank=True
+    )
+    ciphertext = models.BinaryField(
+        max_length=65568,  # 16B IV + 2B pad length + {62-65550}B padded
+        editable=False
+    )
+    hash = models.CharField(
+        max_length=128,
+        editable=False
+    )
 
     plaintext = None
     csv_headers = ['device', 'role', 'name', 'plaintext']

+ 35 - 9
netbox/tenancy/models.py

@@ -5,7 +5,7 @@ from django.db import models
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
 
-from extras.models import CustomFieldModel, CustomFieldValue
+from extras.models import CustomFieldModel
 from utilities.models import CreatedUpdatedModel
 
 
@@ -14,8 +14,13 @@ class TenantGroup(models.Model):
     """
     An arbitrary collection of Tenants.
     """
-    name = models.CharField(max_length=50, unique=True)
-    slug = models.SlugField(unique=True)
+    name = models.CharField(
+        max_length=50,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
 
     csv_headers = ['name', 'slug']
 
@@ -41,12 +46,33 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
     A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
     department.
     """
-    name = models.CharField(max_length=30, unique=True)
-    slug = models.SlugField(unique=True)
-    group = models.ForeignKey('TenantGroup', related_name='tenants', blank=True, null=True, on_delete=models.SET_NULL)
-    description = models.CharField(max_length=100, blank=True, help_text="Long-form name (optional)")
-    comments = models.TextField(blank=True)
-    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
+    name = models.CharField(
+        max_length=30,
+        unique=True
+    )
+    slug = models.SlugField(
+        unique=True
+    )
+    group = models.ForeignKey(
+        to='tenancy.TenantGroup',
+        on_delete=models.SET_NULL,
+        related_name='tenants',
+        blank=True,
+        null=True
+    )
+    description = models.CharField(
+        max_length=100,
+        blank=True,
+        help_text='Long-form name (optional)'
+    )
+    comments = models.TextField(
+        blank=True
+    )
+    custom_field_values = GenericRelation(
+        to='extras.CustomFieldValue',
+        content_type_field='obj_type',
+        object_id_field='obj_id'
+    )
 
     csv_headers = ['name', 'slug', 'group', 'description', 'comments']
 

+ 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.
     It also supports setting an expiration time and toggling write ability.
     """
-    user = models.ForeignKey(User, related_name='tokens', on_delete=models.CASCADE)
-    created = models.DateTimeField(auto_now_add=True)
-    expires = models.DateTimeField(blank=True, null=True)
-    key = models.CharField(max_length=40, unique=True, validators=[MinLengthValidator(40)])
-    write_enabled = models.BooleanField(default=True, help_text="Permit create/update/delete operations using this key")
-    description = models.CharField(max_length=100, blank=True)
+    user = models.ForeignKey(
+        to=User,
+        on_delete=models.CASCADE,
+        related_name='tokens'
+    )
+    created = models.DateTimeField(
+        auto_now_add=True
+    )
+    expires = models.DateTimeField(
+        blank=True,
+        null=True
+    )
+    key = models.CharField(
+        max_length=40,
+        unique=True,
+        validators=[MinLengthValidator(40)]
+    )
+    write_enabled = models.BooleanField(
+        default=True,
+        help_text='Permit create/update/delete operations using this key'
+    )
+    description = models.CharField(
+        max_length=100,
+        blank=True
+    )
 
     class Meta:
         default_permissions = []

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