Просмотр исходного кода

Renamed CreatedUpdatedModel to ChangeLoggedModel and applied it to all primary and organizational models

Jeremy Stretch 7 лет назад
Родитель
Сommit
b556d2d626

+ 45 - 0
netbox/circuits/migrations/0012_change_logging.py

@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.12 on 2018-06-13 17:14
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('circuits', '0011_tags'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='circuittype',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='circuittype',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='circuit',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='circuit',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='provider',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='provider',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+    ]

+ 7 - 8
netbox/circuits/models.py

@@ -9,12 +9,12 @@ from taggit.managers import TaggableManager
 from dcim.constants import STATUS_CLASSES
 from dcim.fields import ASNField
 from extras.models import CustomFieldModel
-from utilities.models import CreatedUpdatedModel
+from utilities.models import ChangeLoggedModel
 from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
 
 
 @python_2_unicode_compatible
-class Provider(CreatedUpdatedModel, CustomFieldModel):
+class Provider(ChangeLoggedModel, 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.
@@ -59,9 +59,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
 
     tags = TaggableManager()
 
-    csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
-
     serializer = 'circuits.api.serializers.ProviderSerializer'
+    csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
 
     class Meta:
         ordering = ['name']
@@ -86,7 +85,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
 
 
 @python_2_unicode_compatible
-class CircuitType(models.Model):
+class CircuitType(ChangeLoggedModel):
     """
     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".
@@ -99,6 +98,7 @@ class CircuitType(models.Model):
         unique=True
     )
 
+    serializer = 'circuits.api.serializers.CircuitTypeSerializer'
     csv_headers = ['name', 'slug']
 
     class Meta:
@@ -118,7 +118,7 @@ class CircuitType(models.Model):
 
 
 @python_2_unicode_compatible
-class Circuit(CreatedUpdatedModel, CustomFieldModel):
+class Circuit(ChangeLoggedModel, CustomFieldModel):
     """
     A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
     circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
@@ -173,12 +173,11 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
 
     tags = TaggableManager()
 
+    serializer = 'circuits.api.serializers.CircuitSerializer'
     csv_headers = [
         'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
     ]
 
-    serializer = 'circuits.api.serializers.CircuitSerializer'
-
     class Meta:
         ordering = ['provider', 'cid']
         unique_together = ['provider', 'cid']

+ 135 - 0
netbox/dcim/migrations/0059_change_logging.py

@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.12 on 2018-06-13 17:14
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0058_relax_rack_naming_constraints'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='devicerole',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='devicerole',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='devicetype',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='devicetype',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='manufacturer',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='manufacturer',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='platform',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='platform',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='rackgroup',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='rackgroup',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='rackreservation',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='rackrole',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='rackrole',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='region',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='region',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='virtualchassis',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='virtualchassis',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='device',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='device',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='rack',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='rack',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='rackreservation',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='site',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='site',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+    ]

+ 0 - 27
netbox/dcim/migrations/0059_devicetype_add_created_updated_times.py

@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.12 on 2018-05-30 17:30
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import django.utils.timezone
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('dcim', '0058_relax_rack_naming_constraints'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='devicetype',
-            name='created',
-            field=models.DateField(auto_now_add=True, default=django.utils.timezone.now),
-            preserve_default=False,
-        ),
-        migrations.AddField(
-            model_name='devicetype',
-            name='last_updated',
-            field=models.DateTimeField(auto_now=True),
-        ),
-    ]

+ 27 - 24
netbox/dcim/models.py

@@ -22,7 +22,7 @@ from extras.models import CustomFieldModel
 from extras.rpc import RPC_CLIENTS
 from utilities.fields import ColorField, NullableCharField
 from utilities.managers import NaturalOrderByManager
-from utilities.models import CreatedUpdatedModel
+from utilities.models import ChangeLoggedModel
 from .constants import *
 from .fields import ASNField, MACAddressField
 from .querysets import InterfaceQuerySet
@@ -33,7 +33,7 @@ from .querysets import InterfaceQuerySet
 #
 
 @python_2_unicode_compatible
-class Region(MPTTModel):
+class Region(ChangeLoggedModel, MPTTModel):
     """
     Sites can be grouped within geographic Regions.
     """
@@ -53,6 +53,7 @@ class Region(MPTTModel):
         unique=True
     )
 
+    serializer = 'dcim.api.serializers.RegionSerializer'
     csv_headers = ['name', 'slug', 'parent']
 
     class MPTTMeta:
@@ -81,7 +82,7 @@ class SiteManager(NaturalOrderByManager):
 
 
 @python_2_unicode_compatible
-class Site(CreatedUpdatedModel, CustomFieldModel):
+class Site(ChangeLoggedModel, 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).
@@ -162,13 +163,12 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
     objects = SiteManager()
     tags = TaggableManager()
 
+    serializer = 'dcim.api.serializers.SiteSerializer'
     csv_headers = [
         'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
         'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
     ]
 
-    serializer = 'dcim.api.serializers.SiteSerializer'
-
     class Meta:
         ordering = ['name']
 
@@ -231,7 +231,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
 #
 
 @python_2_unicode_compatible
-class RackGroup(models.Model):
+class RackGroup(ChangeLoggedModel):
     """
     Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
     example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
@@ -247,9 +247,8 @@ class RackGroup(models.Model):
         related_name='rack_groups'
     )
 
-    csv_headers = ['site', 'name', 'slug']
-
     serializer = 'dcim.api.serializers.RackGroupSerializer'
+    csv_headers = ['site', 'name', 'slug']
 
     class Meta:
         ordering = ['site', 'name']
@@ -273,7 +272,7 @@ class RackGroup(models.Model):
 
 
 @python_2_unicode_compatible
-class RackRole(models.Model):
+class RackRole(ChangeLoggedModel):
     """
     Racks can be organized by functional role, similar to Devices.
     """
@@ -286,6 +285,7 @@ class RackRole(models.Model):
     )
     color = ColorField()
 
+    serializer = 'dcim.api.serializers.RackRoleSerializer'
     csv_headers = ['name', 'slug', 'color']
 
     class Meta:
@@ -310,7 +310,7 @@ class RackManager(NaturalOrderByManager):
 
 
 @python_2_unicode_compatible
-class Rack(CreatedUpdatedModel, CustomFieldModel):
+class Rack(ChangeLoggedModel, 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.
@@ -392,13 +392,12 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
     objects = RackManager()
     tags = TaggableManager()
 
+    serializer = 'dcim.api.serializers.RackSerializer'
     csv_headers = [
         'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'serial', 'width', 'u_height',
         'desc_units', 'comments',
     ]
 
-    serializer = 'dcim.api.serializers.RackSerializer'
-
     class Meta:
         ordering = ['site', 'group', 'name']
         unique_together = [
@@ -570,7 +569,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
 
 
 @python_2_unicode_compatible
-class RackReservation(models.Model):
+class RackReservation(ChangeLoggedModel):
     """
     One or more reserved units within a Rack.
     """
@@ -582,9 +581,6 @@ class RackReservation(models.Model):
     units = ArrayField(
         base_field=models.PositiveSmallIntegerField()
     )
-    created = models.DateTimeField(
-        auto_now_add=True
-    )
     tenant = models.ForeignKey(
         to='tenancy.Tenant',
         on_delete=models.PROTECT,
@@ -600,6 +596,8 @@ class RackReservation(models.Model):
         max_length=100
     )
 
+    serializer = 'dcim.api.serializers.RackReservationSerializer'
+
     class Meta:
         ordering = ['created']
 
@@ -647,7 +645,7 @@ class RackReservation(models.Model):
 #
 
 @python_2_unicode_compatible
-class Manufacturer(models.Model):
+class Manufacturer(ChangeLoggedModel):
     """
     A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
     """
@@ -659,6 +657,7 @@ class Manufacturer(models.Model):
         unique=True
     )
 
+    serializer = 'dcim.api.serializers.ManufacturerSerializer'
     csv_headers = ['name', 'slug']
 
     class Meta:
@@ -678,7 +677,7 @@ class Manufacturer(models.Model):
 
 
 @python_2_unicode_compatible
-class DeviceType(CreatedUpdatedModel, CustomFieldModel):
+class DeviceType(ChangeLoggedModel, CustomFieldModel):
     """
     A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
     well as high-level functional role(s).
@@ -753,6 +752,7 @@ class DeviceType(CreatedUpdatedModel, CustomFieldModel):
 
     tags = TaggableManager()
 
+    serializer = 'dcim.api.serializers.DeviceTypeSerializer'
     csv_headers = [
         'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
         'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments',
@@ -998,7 +998,7 @@ class DeviceBayTemplate(models.Model):
 #
 
 @python_2_unicode_compatible
-class DeviceRole(models.Model):
+class DeviceRole(ChangeLoggedModel):
     """
     Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
     color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
@@ -1018,6 +1018,7 @@ class DeviceRole(models.Model):
         help_text='Virtual machines may be assigned to this role'
     )
 
+    serializer = 'dcim.api.serializers.DeviceRoleSerializer'
     csv_headers = ['name', 'slug', 'color', 'vm_role']
 
     class Meta:
@@ -1039,7 +1040,7 @@ class DeviceRole(models.Model):
 
 
 @python_2_unicode_compatible
-class Platform(models.Model):
+class Platform(ChangeLoggedModel):
     """
     Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
     NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
@@ -1073,6 +1074,7 @@ class Platform(models.Model):
         verbose_name='Legacy RPC client'
     )
 
+    serializer = 'dcim.api.serializers.PlatformSerializer'
     csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver']
 
     class Meta:
@@ -1098,7 +1100,7 @@ class DeviceManager(NaturalOrderByManager):
 
 
 @python_2_unicode_compatible
-class Device(CreatedUpdatedModel, CustomFieldModel):
+class Device(ChangeLoggedModel, CustomFieldModel):
     """
     A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
     DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
@@ -1238,13 +1240,12 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
     objects = DeviceManager()
     tags = TaggableManager()
 
+    serializer = 'dcim.api.serializers.DeviceSerializer'
     csv_headers = [
         'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
         'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
     ]
 
-    serializer = 'dcim.api.serializers.DeviceSerializer'
-
     class Meta:
         ordering = ['name']
         unique_together = [
@@ -2098,7 +2099,7 @@ class InventoryItem(models.Model):
 #
 
 @python_2_unicode_compatible
-class VirtualChassis(models.Model):
+class VirtualChassis(ChangeLoggedModel):
     """
     A collection of Devices which operate with a shared control plane (e.g. a switch stack).
     """
@@ -2112,6 +2113,8 @@ class VirtualChassis(models.Model):
         blank=True
     )
 
+    serializer = 'dcim.api.serializers.VirtualChassisSerializer'
+
     class Meta:
         ordering = ['master']
         verbose_name_plural = 'virtual chassis'

+ 105 - 0
netbox/ipam/migrations/0023_change_logging.py

@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.12 on 2018-06-13 17:14
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ipam', '0022_tags'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='rir',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='rir',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='role',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='role',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='vlangroup',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='vlangroup',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='aggregate',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='aggregate',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='ipaddress',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='ipaddress',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='prefix',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='prefix',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='service',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='service',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='vlan',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='vlan',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='vrf',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='vrf',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+    ]

+ 18 - 22
netbox/ipam/models.py

@@ -14,14 +14,14 @@ from taggit.managers import TaggableManager
 
 from dcim.models import Interface
 from extras.models import CustomFieldModel
-from utilities.models import CreatedUpdatedModel
+from utilities.models import ChangeLoggedModel
 from .constants import *
 from .fields import IPNetworkField, IPAddressField
 from .querysets import PrefixQuerySet
 
 
 @python_2_unicode_compatible
-class VRF(CreatedUpdatedModel, CustomFieldModel):
+class VRF(ChangeLoggedModel, CustomFieldModel):
     """
     A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
     table). Prefixes and IPAddresses can optionally be assigned to VRFs. (Prefixes and IPAddresses not assigned to a VRF
@@ -59,9 +59,8 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
 
     tags = TaggableManager()
 
-    csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
-
     serializer = 'ipam.api.serializers.VRFSerializer'
+    csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
 
     class Meta:
         ordering = ['name', 'rd']
@@ -91,7 +90,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
 
 
 @python_2_unicode_compatible
-class RIR(models.Model):
+class RIR(ChangeLoggedModel):
     """
     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.
@@ -109,6 +108,7 @@ class RIR(models.Model):
         help_text='IP space managed by this RIR is considered private'
     )
 
+    serializer = 'ipam.api.serializers.RIRSerializer'
     csv_headers = ['name', 'slug', 'is_private']
 
     class Meta:
@@ -131,7 +131,7 @@ class RIR(models.Model):
 
 
 @python_2_unicode_compatible
-class Aggregate(CreatedUpdatedModel, CustomFieldModel):
+class Aggregate(ChangeLoggedModel, 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.
@@ -162,9 +162,8 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
 
     tags = TaggableManager()
 
-    csv_headers = ['prefix', 'rir', 'date_added', 'description']
-
     serializer = 'ipam.api.serializers.AggregateSerializer'
+    csv_headers = ['prefix', 'rir', 'date_added', 'description']
 
     class Meta:
         ordering = ['family', 'prefix']
@@ -228,7 +227,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
 
 
 @python_2_unicode_compatible
-class Role(models.Model):
+class Role(ChangeLoggedModel):
     """
     A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
     "Management."
@@ -244,6 +243,7 @@ class Role(models.Model):
         default=1000
     )
 
+    serializer = 'ipam.api.serializers.RoleSerializer'
     csv_headers = ['name', 'slug', 'weight']
 
     class Meta:
@@ -261,7 +261,7 @@ class Role(models.Model):
 
 
 @python_2_unicode_compatible
-class Prefix(CreatedUpdatedModel, CustomFieldModel):
+class Prefix(ChangeLoggedModel, CustomFieldModel):
     """
     A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
     VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be
@@ -336,12 +336,11 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
     objects = PrefixQuerySet.as_manager()
     tags = TaggableManager()
 
+    serializer = 'ipam.api.serializers.PrefixSerializer'
     csv_headers = [
         'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
     ]
 
-    serializer = 'ipam.api.serializers.PrefixSerializer'
-
     class Meta:
         ordering = ['vrf', 'family', 'prefix']
         verbose_name_plural = 'prefixes'
@@ -503,7 +502,7 @@ class IPAddressManager(models.Manager):
 
 
 @python_2_unicode_compatible
-class IPAddress(CreatedUpdatedModel, CustomFieldModel):
+class IPAddress(ChangeLoggedModel, CustomFieldModel):
     """
     An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
     configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
@@ -578,13 +577,12 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
     objects = IPAddressManager()
     tags = TaggableManager()
 
+    serializer = 'ipam.api.serializers.IPAddressSerializer'
     csv_headers = [
         'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
         'description',
     ]
 
-    serializer = 'ipam.api.serializers.IPAddressSerializer'
-
     class Meta:
         ordering = ['family', 'address']
         verbose_name = 'IP address'
@@ -663,7 +661,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
 
 
 @python_2_unicode_compatible
-class VLANGroup(models.Model):
+class VLANGroup(ChangeLoggedModel):
     """
     A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
     """
@@ -679,9 +677,8 @@ class VLANGroup(models.Model):
         null=True
     )
 
-    csv_headers = ['name', 'slug', 'site']
-
     serializer = 'ipam.api.serializers.VLANGroupSerializer'
+    csv_headers = ['name', 'slug', 'site']
 
     class Meta:
         ordering = ['site', 'name']
@@ -717,7 +714,7 @@ class VLANGroup(models.Model):
 
 
 @python_2_unicode_compatible
-class VLAN(CreatedUpdatedModel, CustomFieldModel):
+class VLAN(ChangeLoggedModel, CustomFieldModel):
     """
     A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
     to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
@@ -778,9 +775,8 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
 
     tags = TaggableManager()
 
-    csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
-
     serializer = 'ipam.api.serializers.VLANSerializer'
+    csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
 
     class Meta:
         ordering = ['site', 'group', 'vid']
@@ -835,7 +831,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
 
 
 @python_2_unicode_compatible
-class Service(CreatedUpdatedModel):
+class Service(ChangeLoggedModel):
     """
     A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
     optionally be tied to one or more specific IPAddresses belonging to its parent.

+ 35 - 0
netbox/secrets/migrations/0005_change_logging.py

@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.12 on 2018-06-13 17:29
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('secrets', '0004_tags'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='secretrole',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='secretrole',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='secret',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='secret',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+    ]

+ 12 - 4
netbox/secrets/models.py

@@ -14,7 +14,7 @@ from django.urls import reverse
 from django.utils.encoding import force_bytes, python_2_unicode_compatible
 from taggit.managers import TaggableManager
 
-from utilities.models import CreatedUpdatedModel
+from utilities.models import ChangeLoggedModel
 from .exceptions import InvalidKey
 from .hashers import SecretValidationHasher
 from .querysets import UserKeyQuerySet
@@ -48,12 +48,18 @@ def decrypt_master_key(master_key_cipher, private_key):
 
 
 @python_2_unicode_compatible
-class UserKey(CreatedUpdatedModel):
+class UserKey(models.Model):
     """
     A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted
     copy of the master encryption key. The encrypted instance of the master key can be decrypted only with the user's
     matching (private) decryption key.
     """
+    created = models.DateField(
+        auto_now_add=True
+    )
+    last_updated = models.DateTimeField(
+        auto_now=True
+    )
     user = models.OneToOneField(
         to=User,
         on_delete=models.CASCADE,
@@ -251,7 +257,7 @@ class SessionKey(models.Model):
 
 
 @python_2_unicode_compatible
-class SecretRole(models.Model):
+class SecretRole(ChangeLoggedModel):
     """
     A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
     such as "Login Credentials" or "SNMP Communities."
@@ -277,6 +283,7 @@ class SecretRole(models.Model):
         blank=True
     )
 
+    serializer = 'ipam.api.secrets.SecretSerializer'
     csv_headers = ['name', 'slug']
 
     class Meta:
@@ -304,7 +311,7 @@ class SecretRole(models.Model):
 
 
 @python_2_unicode_compatible
-class Secret(CreatedUpdatedModel):
+class Secret(ChangeLoggedModel):
     """
     A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
     SHA-256 hash is stored along with the ciphertext for validation upon decryption. Each Secret is assigned to a
@@ -340,6 +347,7 @@ class Secret(CreatedUpdatedModel):
     tags = TaggableManager()
 
     plaintext = None
+    serializer = 'ipam.api.secrets.SecretSerializer'
     csv_headers = ['device', 'role', 'name', 'plaintext']
 
     class Meta:

+ 35 - 0
netbox/tenancy/migrations/0005_change_logging.py

@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.12 on 2018-06-13 17:14
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tenancy', '0004_tags'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='tenantgroup',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='tenantgroup',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='tenant',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='tenant',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+    ]

+ 5 - 7
netbox/tenancy/models.py

@@ -7,11 +7,11 @@ from django.utils.encoding import python_2_unicode_compatible
 from taggit.managers import TaggableManager
 
 from extras.models import CustomFieldModel
-from utilities.models import CreatedUpdatedModel
+from utilities.models import ChangeLoggedModel
 
 
 @python_2_unicode_compatible
-class TenantGroup(models.Model):
+class TenantGroup(ChangeLoggedModel):
     """
     An arbitrary collection of Tenants.
     """
@@ -23,9 +23,8 @@ class TenantGroup(models.Model):
         unique=True
     )
 
-    csv_headers = ['name', 'slug']
-
     serializer = 'tenancy.api.serializers.TenantGroupSerializer'
+    csv_headers = ['name', 'slug']
 
     class Meta:
         ordering = ['name']
@@ -44,7 +43,7 @@ class TenantGroup(models.Model):
 
 
 @python_2_unicode_compatible
-class Tenant(CreatedUpdatedModel, CustomFieldModel):
+class Tenant(ChangeLoggedModel, CustomFieldModel):
     """
     A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
     department.
@@ -79,9 +78,8 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
 
     tags = TaggableManager()
 
-    csv_headers = ['name', 'slug', 'group', 'description', 'comments']
-
     serializer = 'tenancy.api.serializers.TenantSerializer'
+    csv_headers = ['name', 'slug', 'group', 'description', 'comments']
 
     class Meta:
         ordering = ['group', 'name']

+ 15 - 3
netbox/utilities/models.py

@@ -3,9 +3,21 @@ from __future__ import unicode_literals
 from django.db import models
 
 
-class CreatedUpdatedModel(models.Model):
-    created = models.DateField(auto_now_add=True)
-    last_updated = models.DateTimeField(auto_now=True)
+class ChangeLoggedModel(models.Model):
+    """
+    An abstract model which adds fields to store the creation and last-updated times for an object. Both fields can be
+    null to facilitate adding these fields to existing instances via a database migration.
+    """
+    created = models.DateField(
+        auto_now_add=True,
+        blank=True,
+        null=True
+    )
+    last_updated = models.DateTimeField(
+        auto_now=True,
+        blank=True,
+        null=True
+    )
 
     class Meta:
         abstract = True

+ 55 - 0
netbox/virtualization/migrations/0007_change_logging.py

@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.12 on 2018-06-13 17:14
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('virtualization', '0006_tags'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='clustergroup',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='clustergroup',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='clustertype',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='clustertype',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='cluster',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='cluster',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='virtualmachine',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='virtualmachine',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+    ]

+ 9 - 11
netbox/virtualization/models.py

@@ -10,7 +10,7 @@ from taggit.managers import TaggableManager
 
 from dcim.models import Device
 from extras.models import CustomFieldModel
-from utilities.models import CreatedUpdatedModel
+from utilities.models import ChangeLoggedModel
 from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
 
 
@@ -19,7 +19,7 @@ from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSE
 #
 
 @python_2_unicode_compatible
-class ClusterType(models.Model):
+class ClusterType(ChangeLoggedModel):
     """
     A type of Cluster.
     """
@@ -31,6 +31,7 @@ class ClusterType(models.Model):
         unique=True
     )
 
+    serializer = 'virtualization.api.serializers.ClusterTypeSerializer'
     csv_headers = ['name', 'slug']
 
     class Meta:
@@ -54,7 +55,7 @@ class ClusterType(models.Model):
 #
 
 @python_2_unicode_compatible
-class ClusterGroup(models.Model):
+class ClusterGroup(ChangeLoggedModel):
     """
     An organizational group of Clusters.
     """
@@ -66,9 +67,8 @@ class ClusterGroup(models.Model):
         unique=True
     )
 
-    csv_headers = ['name', 'slug']
-
     serializer = 'virtualization.api.serializers.ClusterGroupSerializer'
+    csv_headers = ['name', 'slug']
 
     class Meta:
         ordering = ['name']
@@ -91,7 +91,7 @@ class ClusterGroup(models.Model):
 #
 
 @python_2_unicode_compatible
-class Cluster(CreatedUpdatedModel, CustomFieldModel):
+class Cluster(ChangeLoggedModel, CustomFieldModel):
     """
     A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
     """
@@ -129,9 +129,8 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
 
     tags = TaggableManager()
 
-    csv_headers = ['name', 'type', 'group', 'site', 'comments']
-
     serializer = 'virtualization.api.serializers.ClusterSerializer'
+    csv_headers = ['name', 'type', 'group', 'site', 'comments']
 
     class Meta:
         ordering = ['name']
@@ -169,7 +168,7 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
 #
 
 @python_2_unicode_compatible
-class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
+class VirtualMachine(ChangeLoggedModel, CustomFieldModel):
     """
     A virtual machine which runs inside a Cluster.
     """
@@ -251,12 +250,11 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel):
 
     tags = TaggableManager()
 
+    serializer = 'virtualization.api.serializers.VirtualMachineSerializer'
     csv_headers = [
         'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
     ]
 
-    serializer = 'virtualization.api.serializers.VirtualMachineSerializer'
-
     class Meta:
         ordering = ['name']