From b556d2d6267f583b49bae4ec6ce223bbb8373375 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Jun 2018 15:40:16 -0400 Subject: [PATCH 01/22] Renamed CreatedUpdatedModel to ChangeLoggedModel and applied it to all primary and organizational models --- .../migrations/0012_change_logging.py | 45 ++++++ netbox/circuits/models.py | 15 +- netbox/dcim/migrations/0059_change_logging.py | 135 ++++++++++++++++++ ...59_devicetype_add_created_updated_times.py | 27 ---- netbox/dcim/models.py | 51 +++---- netbox/ipam/migrations/0023_change_logging.py | 105 ++++++++++++++ netbox/ipam/models.py | 40 +++--- .../secrets/migrations/0005_change_logging.py | 35 +++++ netbox/secrets/models.py | 16 ++- .../tenancy/migrations/0005_change_logging.py | 35 +++++ netbox/tenancy/models.py | 12 +- netbox/utilities/models.py | 18 ++- .../migrations/0007_change_logging.py | 55 +++++++ netbox/virtualization/models.py | 20 ++- 14 files changed, 503 insertions(+), 106 deletions(-) create mode 100644 netbox/circuits/migrations/0012_change_logging.py create mode 100644 netbox/dcim/migrations/0059_change_logging.py delete mode 100644 netbox/dcim/migrations/0059_devicetype_add_created_updated_times.py create mode 100644 netbox/ipam/migrations/0023_change_logging.py create mode 100644 netbox/secrets/migrations/0005_change_logging.py create mode 100644 netbox/tenancy/migrations/0005_change_logging.py create mode 100644 netbox/virtualization/migrations/0007_change_logging.py diff --git a/netbox/circuits/migrations/0012_change_logging.py b/netbox/circuits/migrations/0012_change_logging.py new file mode 100644 index 000000000..db5057858 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 0a36ba366..10ca8d7db 100644 --- a/netbox/circuits/models.py +++ b/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'] diff --git a/netbox/dcim/migrations/0059_change_logging.py b/netbox/dcim/migrations/0059_change_logging.py new file mode 100644 index 000000000..b64e2570b --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/dcim/migrations/0059_devicetype_add_created_updated_times.py b/netbox/dcim/migrations/0059_devicetype_add_created_updated_times.py deleted file mode 100644 index 6a16656ed..000000000 --- a/netbox/dcim/migrations/0059_devicetype_add_created_updated_times.py +++ /dev/null @@ -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), - ), - ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 1209153bc..f430eb095 100644 --- a/netbox/dcim/models.py +++ b/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' diff --git a/netbox/ipam/migrations/0023_change_logging.py b/netbox/ipam/migrations/0023_change_logging.py new file mode 100644 index 000000000..d548fdf15 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index a3d8736c1..f1414bd27 100644 --- a/netbox/ipam/models.py +++ b/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. diff --git a/netbox/secrets/migrations/0005_change_logging.py b/netbox/secrets/migrations/0005_change_logging.py new file mode 100644 index 000000000..947087934 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index dcb38db70..3c253c104 100644 --- a/netbox/secrets/models.py +++ b/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: diff --git a/netbox/tenancy/migrations/0005_change_logging.py b/netbox/tenancy/migrations/0005_change_logging.py new file mode 100644 index 000000000..7712e9d02 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index bc87ccd8c..33073e326 100644 --- a/netbox/tenancy/models.py +++ b/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'] diff --git a/netbox/utilities/models.py b/netbox/utilities/models.py index c6768c4c1..9a0c05de5 100644 --- a/netbox/utilities/models.py +++ b/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 diff --git a/netbox/virtualization/migrations/0007_change_logging.py b/netbox/virtualization/migrations/0007_change_logging.py new file mode 100644 index 000000000..954f9f2a9 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 42b6591f4..70a73dc05 100644 --- a/netbox/virtualization/models.py +++ b/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'] From 33cf227bc8336d8ba30c658d7a2869520930740c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Jun 2018 17:06:33 -0400 Subject: [PATCH 02/22] Implemented new object change logging to replace UserActions --- netbox/extras/admin.py | 47 +++++++++++++- netbox/extras/constants.py | 10 +++ netbox/extras/middleware.py | 63 ++++++++++++++++++ netbox/extras/migrations/0013_objectchange.py | 37 +++++++++++ netbox/extras/models.py | 65 +++++++++++++++++++ netbox/netbox/settings.py | 1 + 6 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 netbox/extras/middleware.py create mode 100644 netbox/extras/migrations/0013_objectchange.py diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index e96ae9ac8..200387f88 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -5,9 +5,9 @@ from django.contrib import admin from django.utils.safestring import mark_safe from utilities.forms import LaxURLField +from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE from .models import ( - CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction, - Webhook + CustomField, CustomFieldChoice, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction, Webhook, ) @@ -125,6 +125,49 @@ class TopologyMapAdmin(admin.ModelAdmin): } +# +# Change logging +# + +@admin.register(ObjectChange) +class ObjectChangeAdmin(admin.ModelAdmin): + actions = None + fields = ['time', 'content_type', 'display_object', 'action', 'display_user'] + list_display = ['time', 'content_type', 'display_object', 'display_action', 'display_user'] + list_filter = ['time', 'action', 'user__username'] + list_select_related = ['content_type', 'user'] + readonly_fields = fields + search_fields = ['user_name', 'object_repr'] + + def has_add_permission(self, request): + return False + + def display_user(self, obj): + if obj.user is not None: + return obj.user + else: + return '{} (deleted)'.format(obj.user_name) + display_user.short_description = 'user' + + def display_action(self, obj): + icon = { + OBJECTCHANGE_ACTION_CREATE: 'addlink', + OBJECTCHANGE_ACTION_UPDATE: 'changelink', + OBJECTCHANGE_ACTION_DELETE: 'deletelink', + } + return mark_safe('{}'.format(icon[obj.action], obj.get_action_display())) + display_user.short_description = 'action' + + def display_object(self, obj): + if hasattr(obj.changed_object, 'get_absolute_url'): + return mark_safe('{}'.format(obj.changed_object.get_absolute_url(), obj.changed_object)) + elif obj.changed_object is not None: + return obj.changed_object + else: + return '{} (deleted)'.format(obj.object_repr) + display_object.short_description = 'object' + + # # User actions # diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 8a615c076..84c84dfcf 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -66,6 +66,16 @@ TOPOLOGYMAP_TYPE_CHOICES = ( (TOPOLOGYMAP_TYPE_POWER, 'Power'), ) +# Change log actions +OBJECTCHANGE_ACTION_CREATE = 1 +OBJECTCHANGE_ACTION_UPDATE = 2 +OBJECTCHANGE_ACTION_DELETE = 3 +OBJECTCHANGE_ACTION_CHOICES = ( + (OBJECTCHANGE_ACTION_CREATE, 'Created'), + (OBJECTCHANGE_ACTION_UPDATE, 'Updated'), + (OBJECTCHANGE_ACTION_DELETE, 'Deleted'), +) + # User action types ACTION_CREATE = 1 ACTION_IMPORT = 2 diff --git a/netbox/extras/middleware.py b/netbox/extras/middleware.py new file mode 100644 index 000000000..cf0efdfae --- /dev/null +++ b/netbox/extras/middleware.py @@ -0,0 +1,63 @@ +from __future__ import unicode_literals + +import json + +from django.core.serializers import serialize +from django.db.models.signals import post_delete, post_save +from django.utils.functional import curry, SimpleLazyObject + +from utilities.models import ChangeLoggedModel +from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE +from .models import ObjectChange + + +def record_object_change(user, instance, **kwargs): + """ + Create an ObjectChange in response to an object being created or deleted. + """ + if not isinstance(instance, ChangeLoggedModel): + return + + # Determine what action is being performed. The post_save signal sends a `created` boolean, whereas post_delete + # does not. + if 'created' in kwargs: + action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE + else: + action = OBJECTCHANGE_ACTION_DELETE + + # Serialize the object using Django's built-in JSON serializer, then extract only the `fields` dict. + json_str = serialize('json', [instance]) + object_data = json.loads(json_str)[0]['fields'] + + ObjectChange( + user=user, + changed_object=instance, + action=action, + object_data=object_data + ).save() + + +class ChangeLoggingMiddleware(object): + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + + def get_user(request): + return request.user + + # DRF employs a separate authentication mechanism outside Django's normal request/response cycle, so calling + # request.user in middleware will always return AnonymousUser for API requests. To work around this, we point + # to a lazy object that doesn't resolve the user until after DRF's authentication has been called. For more + # detail, see https://stackoverflow.com/questions/26240832/ + user = SimpleLazyObject(lambda: get_user(request)) + + # Django doesn't provide any request context with the post_save/post_delete signals, so we curry + # record_object_change() to include the user associated with the current request. + _record_object_change = curry(record_object_change, user) + + post_save.connect(_record_object_change) + post_delete.connect(_record_object_change) + + return self.get_response(request) diff --git a/netbox/extras/migrations/0013_objectchange.py b/netbox/extras/migrations/0013_objectchange.py new file mode 100644 index 000000000..fdaf7dfd5 --- /dev/null +++ b/netbox/extras/migrations/0013_objectchange.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-06-13 20:05 +from __future__ import unicode_literals + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('extras', '0012_webhooks'), + ] + + operations = [ + migrations.CreateModel( + name='ObjectChange', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField(auto_now_add=True)), + ('user_name', models.CharField(editable=False, max_length=150)), + ('action', models.PositiveSmallIntegerField(choices=[(1, 'Created'), (2, 'Updated'), (3, 'Deleted')])), + ('object_id', models.PositiveIntegerField()), + ('object_repr', models.CharField(editable=False, max_length=200)), + ('object_data', django.contrib.postgres.fields.jsonb.JSONField(editable=False)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='changes', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-time'], + }, + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 865ff9fbb..e9fb2d543 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -656,6 +656,71 @@ class ReportResult(models.Model): ordering = ['report'] +# +# Change logging +# + +@python_2_unicode_compatible +class ObjectChange(models.Model): + """ + Record a change to an object and the user account associated with that change. + """ + time = models.DateTimeField( + auto_now_add=True, + editable=False + ) + user = models.ForeignKey( + to=User, + on_delete=models.SET_NULL, + related_name='changes', + blank=True, + null=True + ) + user_name = models.CharField( + max_length=150, + editable=False + ) + action = models.PositiveSmallIntegerField( + choices=OBJECTCHANGE_ACTION_CHOICES + ) + content_type = models.ForeignKey( + to=ContentType, + on_delete=models.CASCADE + ) + object_id = models.PositiveIntegerField() + changed_object = GenericForeignKey( + ct_field='content_type', + fk_field='object_id' + ) + object_repr = models.CharField( + max_length=200, + editable=False + ) + object_data = JSONField( + editable=False + ) + + class Meta: + ordering = ['-time'] + + def __str__(self): + attribution = 'by {}'.format(self.user_name) if self.user_name else '(no attribution)' + return '{} {} {}'.format( + self.object_repr, + self.get_action_display().lower(), + attribution + ) + + def save(self, *args, **kwargs): + + # Record the user's name and the object's representation as static strings + if self.user is not None: + self.user_name = self.user.username + self.object_repr = str(self.changed_object) + + return super(ObjectChange, self).save(*args, **kwargs) + + # # User actions # diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 7acb611f3..83686df94 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -174,6 +174,7 @@ MIDDLEWARE = ( 'utilities.middleware.ExceptionHandlingMiddleware', 'utilities.middleware.LoginRequiredMiddleware', 'utilities.middleware.APIVersionMiddleware', + 'extras.middleware.ChangeLoggingMiddleware', ) ROOT_URLCONF = 'netbox.urls' From 21c4085c51863f589da8e4a7a91ec41425c2b83d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 14 Jun 2018 13:14:35 -0400 Subject: [PATCH 03/22] Moved object header templates into object base templates --- netbox/dcim/views.py | 4 + netbox/ipam/views.py | 14 +- netbox/templates/_base.html | 1 + netbox/templates/dcim/device.html | 1215 +++++++++-------- netbox/templates/dcim/device_config.html | 3 +- netbox/templates/dcim/device_inventory.html | 137 +- .../templates/dcim/device_lldp_neighbors.html | 3 +- netbox/templates/dcim/device_status.html | 3 +- netbox/templates/dcim/inc/device_header.html | 65 - netbox/templates/ipam/inc/prefix_header.html | 55 - netbox/templates/ipam/inc/vlan_header.html | 46 - netbox/templates/ipam/prefix.html | 343 +++-- netbox/templates/ipam/prefix_ipaddresses.html | 5 +- netbox/templates/ipam/prefix_prefixes.html | 5 +- netbox/templates/ipam/vlan.html | 270 ++-- netbox/templates/ipam/vlan_members.html | 5 +- 16 files changed, 1083 insertions(+), 1091 deletions(-) delete mode 100644 netbox/templates/dcim/inc/device_header.html delete mode 100644 netbox/templates/ipam/inc/prefix_header.html delete mode 100644 netbox/templates/ipam/inc/vlan_header.html diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 6e7aa070c..d4851845a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -945,6 +945,7 @@ class DeviceInventoryView(View): return render(request, 'dcim/device_inventory.html', { 'device': device, 'inventory_items': inventory_items, + 'active_tab': 'inventory', }) @@ -957,6 +958,7 @@ class DeviceStatusView(PermissionRequiredMixin, View): return render(request, 'dcim/device_status.html', { 'device': device, + 'active_tab': 'status', }) @@ -975,6 +977,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View): return render(request, 'dcim/device_lldp_neighbors.html', { 'device': device, 'interfaces': interfaces, + 'active_tab': 'lldp-neighbors', }) @@ -987,6 +990,7 @@ class DeviceConfigView(PermissionRequiredMixin, View): return render(request, 'dcim/device_config.html', { 'device': device, + 'active_tab': 'config', }) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 1d4575e34..70ef83f49 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -522,6 +522,7 @@ class PrefixPrefixesView(View): 'prefix_table': prefix_table, 'permissions': permissions, 'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix), + 'active_tab': 'prefixes', }) @@ -560,6 +561,7 @@ class PrefixIPAddressesView(View): 'ip_table': ip_table, 'permissions': permissions, 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix), + 'active_tab': 'ip-addresses', }) @@ -859,8 +861,6 @@ class VLANMembersView(View): members = vlan.get_members().select_related('device', 'virtual_machine') members_table = tables.VLANMemberTable(members) - # if request.user.has_perm('dcim.change_interface'): - # members_table.columns.show('pk') paginate = { 'klass': EnhancedPaginator, @@ -868,18 +868,10 @@ class VLANMembersView(View): } RequestConfig(request, paginate).configure(members_table) - # Compile permissions list for rendering the object table - # permissions = { - # 'add': request.user.has_perm('ipam.add_ipaddress'), - # 'change': request.user.has_perm('ipam.change_ipaddress'), - # 'delete': request.user.has_perm('ipam.delete_ipaddress'), - # } - return render(request, 'ipam/vlan_members.html', { 'vlan': vlan, 'members_table': members_table, - # 'permissions': permissions, - # 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix), + 'active_tab': 'members', }) diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index f34c0fbde..27ebb052d 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -34,6 +34,7 @@ {{ message }} {% endfor %} + {% block header %}{% endblock %} {% block content %}{% endblock %}
{% if settings.BANNER_BOTTOM %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 1b1d3d23a..0cc1e4cf8 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -2,631 +2,696 @@ {% load static from staticfiles %} {% load helpers %} -{% block title %}{{ device }}{% endblock %} +{% block header %} +
+
+ +
+
+
+
+ + + + +
+
+
+
+
+ {% if perms.dcim.change_device %} + + + Edit this device + + {% endif %} + {% if perms.dcim.delete_device %} + + + Delete this device + + {% endif %} +
+

{% block title %}{{ device }}{% endblock %}

+ {% include 'inc/created_updated.html' with obj=device %} + +{% endblock %} {% block content %} -{% include 'dcim/inc/device_header.html' with active_tab='info' %} -
-
-
-
- Device -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Site - {% if device.site.region %} - {{ device.site.region }} - - {% endif %} - {{ device.site }} -
Rack - {% if device.rack %} - {% if device.rack.group %} - {{ device.rack.group }} - - {% endif %} - {{ device.rack }} - {% else %} - None - {% endif %} -
Position - {% if device.parent_bay %} - {% with device.parent_bay.device as parent %} - {{ parent }} {{ device.parent_bay }} - {% if parent.position %} - (U{{ parent.position }} / {{ parent.get_face_display }}) - {% endif %} - {% endwith %} - {% elif device.rack and device.position %} - U{{ device.position }} / {{ device.get_face_display }} - {% elif device.rack and device.device_type.u_height %} - Not racked - {% else %} - N/A - {% endif %} -
Tenant - {% if device.tenant %} - {% if device.tenant.group %} - {{ device.tenant.group }} - - {% endif %} - {{ device.tenant }} - {% else %} - None - {% endif %} -
Device Type - {{ device.device_type.full_name }} ({{ device.device_type.u_height }}U) -
Serial Number - {% if device.serial %} - {{ device.serial }} - {% else %} - N/A - {% endif %} -
Asset Tag - {% if device.asset_tag %} - {{ device.asset_tag }} - {% else %} - N/A - {% endif %} -
Tags - {% for tag in device.tags.all %} - {% tag 'dcim:device_list' tag %} - {% empty %} - N/A - {% endfor %} -
-
- {% if vc_members %} +
+
- Virtual Chassis + Device
- - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - {% for vc_member in vc_members %} - - - - - - - {% endfor %}
DevicePositionMasterPrioritySite + {% if device.site.region %} + {{ device.site.region }} + + {% endif %} + {{ device.site }} +
Rack + {% if device.rack %} + {% if device.rack.group %} + {{ device.rack.group }} + + {% endif %} + {{ device.rack }} + {% else %} + None + {% endif %} +
Position + {% if device.parent_bay %} + {% with device.parent_bay.device as parent %} + {{ parent }} {{ device.parent_bay }} + {% if parent.position %} + (U{{ parent.position }} / {{ parent.get_face_display }}) + {% endif %} + {% endwith %} + {% elif device.rack and device.position %} + U{{ device.position }} / {{ device.get_face_display }} + {% elif device.rack and device.device_type.u_height %} + Not racked + {% else %} + N/A + {% endif %} +
Tenant + {% if device.tenant %} + {% if device.tenant.group %} + {{ device.tenant.group }} + + {% endif %} + {{ device.tenant }} + {% else %} + None + {% endif %} +
Device Type + {{ device.device_type.full_name }} ({{ device.device_type.u_height }}U) +
Serial Number + {% if device.serial %} + {{ device.serial }} + {% else %} + N/A + {% endif %} +
Asset Tag + {% if device.asset_tag %} + {{ device.asset_tag }} + {% else %} + N/A + {% endif %} +
Tags + {% for tag in device.tags.all %} + {% tag 'dcim:device_list' tag %} + {% empty %} + N/A + {% endfor %} +
- {{ vc_member }} - {{ vc_member.vc_position }}{% if device.virtual_chassis.master == vc_member %}{% endif %}{{ vc_member.vc_priority|default:"" }}
- + {% if vc_members %} +
+
+ Virtual Chassis +
+ + + + + + + + {% for vc_member in vc_members %} + + + + + + + {% endfor %} +
DevicePositionMasterPriority
+ {{ vc_member }} + {{ vc_member.vc_position }}{% if device.virtual_chassis.master == vc_member %}{% endif %}{{ vc_member.vc_priority|default:"" }}
+ +
+ {% endif %} +
+
+ Management +
+ + + + + + + + + + + + + + + + + + + + + + {% if device.cluster %} + + + + {% endif %} - {% if perms.dcim.delete_virtualchassis %} - - Delete Virtual Chassis - +
Role + {{ device.device_role }} +
Platform + {% if device.platform %} + {{ device.platform }} + {% else %} + None + {% endif %} +
Status + {{ device.get_status_display }} +
Primary IPv4 + {% if device.primary_ip4 %} + {{ device.primary_ip4.address.ip }} + {% if device.primary_ip4.nat_inside %} + (NAT for {{ device.primary_ip4.nat_inside.address.ip }}) + {% elif device.primary_ip4.nat_outside %} + (NAT: {{ device.primary_ip4.nat_outside.address.ip }}) + {% endif %} + {% else %} + N/A + {% endif %} +
Primary IPv6 + {% if device.primary_ip6 %} + {{ device.primary_ip6.address.ip }} + {% if device.primary_ip6.nat_inside %} + (NAT for {{ device.primary_ip6.nat_inside.address.ip }}) + {% elif device.primary_ip6.nat_outside %} + (NAT: {{ device.primary_ip6.nat_outside.address.ip }}) + {% endif %} + {% else %} + N/A + {% endif %} +
Cluster + {% if device.cluster.group %} + {{ device.cluster.group }} + + {% endif %} + {{ device.cluster }} +
+
+ {% with device.get_custom_fields as custom_fields %} + {% include 'inc/custom_fields_panel.html' %} + {% endwith %} +
+
+ Comments +
+
+ {% if device.comments %} + {{ device.comments|gfm }} + {% else %} + None {% endif %}
- {% endif %} -
-
- Management -
- - - - - - - - - - - - - - - - - - - - - - {% if device.cluster %} - - - - - {% endif %} -
Role - {{ device.device_role }} -
Platform - {% if device.platform %} - {{ device.platform }} - {% else %} - None - {% endif %} -
Status - {{ device.get_status_display }} -
Primary IPv4 - {% if device.primary_ip4 %} - {{ device.primary_ip4.address.ip }} - {% if device.primary_ip4.nat_inside %} - (NAT for {{ device.primary_ip4.nat_inside.address.ip }}) - {% elif device.primary_ip4.nat_outside %} - (NAT: {{ device.primary_ip4.nat_outside.address.ip }}) - {% endif %} - {% else %} - N/A - {% endif %} -
Primary IPv6 - {% if device.primary_ip6 %} - {{ device.primary_ip6.address.ip }} - {% if device.primary_ip6.nat_inside %} - (NAT for {{ device.primary_ip6.nat_inside.address.ip }}) - {% elif device.primary_ip6.nat_outside %} - (NAT: {{ device.primary_ip6.nat_outside.address.ip }}) - {% endif %} - {% else %} - N/A - {% endif %} -
Cluster - {% if device.cluster.group %} - {{ device.cluster.group }} - - {% endif %} - {{ device.cluster }} -
- {% with device.get_custom_fields as custom_fields %} - {% include 'inc/custom_fields_panel.html' %} - {% endwith %} -
-
- Comments -
-
- {% if device.comments %} - {{ device.comments|gfm }} - {% else %} - None +
+
+
+ Console / Power +
+ + {% for cp in console_ports %} + {% include 'dcim/inc/consoleport.html' %} + {% empty %} + {% if device.device_type.console_port_templates.exists %} + + + + {% endif %} + {% endfor %} + {% for pp in power_ports %} + {% include 'dcim/inc/powerport.html' %} + {% empty %} + {% if device.device_type.power_port_templates.exists %} + + + + {% endif %} + {% endfor %} +
+ No console ports defined + {% if perms.dcim.add_consoleport %} + + {% endif %} +
+ No power ports defined + {% if perms.dcim.add_powerport %} + + {% endif %} +
+ {% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %} + {% endif %}
-
-
-
-
-
- Console / Power -
- - {% for cp in console_ports %} - {% include 'dcim/inc/consoleport.html' %} - {% empty %} - {% if device.device_type.console_port_templates.exists %} - - - + {% if request.user.is_authenticated %} +
+
+ Secrets +
+ {% if secrets %} +
- No console ports defined - {% if perms.dcim.add_consoleport %} - - {% endif %} -
+ {% for secret in secrets %} + {% include 'secrets/inc/secret_tr.html' %} + {% endfor %} +
+ {% else %} +
+ None found +
{% endif %} - {% endfor %} - {% for pp in power_ports %} - {% include 'dcim/inc/powerport.html' %} - {% empty %} - {% if device.device_type.power_port_templates.exists %} - - - No power ports defined - {% if perms.dcim.add_powerport %} - - {% endif %} - - - {% endif %} - {% endfor %} - - {% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %} - {% endif %} -
- {% if request.user.is_authenticated %}
- Secrets + Services
- {% if secrets %} + {% if services %} - {% for secret in secrets %} - {% include 'secrets/inc/secret_tr.html' %} + {% for service in services %} + {% include 'ipam/inc/service.html' %} {% endfor %}
{% else %}
- None found + None
{% endif %} - {% if perms.secrets.add_secret %} -
- {% csrf_token %} -
+ {% if perms.ipam.add_service %} {% endif %}
- {% endif %} -
-
- Services -
- {% if services %} - - {% for service in services %} - {% include 'ipam/inc/service.html' %} - {% endfor %} -
- {% else %} -
- None -
- {% endif %} - {% if perms.ipam.add_service %} - - {% endif %} -
-
-
- Images -
- {% include 'inc/image_attachments.html' with images=device.images.all %} - {% if perms.extras.add_imageattachment %} - - {% endif %} -
-
-
- Related Devices -
- {% if related_devices %} - - {% for rd in related_devices %} - - - - - - {% endfor %} -
- {{ rd }} - - {% if rd.rack %} - Rack {{ rd.rack }} - {% else %} - - {% endif %} - {{ rd.device_type.full_name }}
- {% else %} -
None found
- {% endif %} -
-
-
-
-
- {% if device_bays or device.device_type.is_parent_device %} - {% if perms.dcim.delete_devicebay %} -
- {% csrf_token %} - {% endif %}
- Device Bays + Images
- - - - {% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %} - - {% endif %} - - - - - - - {% for devicebay in device_bays %} - {% include 'dcim/inc/devicebay.html' %} - {% empty %} + {% include 'inc/image_attachments.html' with images=device.images.all %} + {% if perms.extras.add_imageattachment %} + + {% endif %} + +
+
+ Related Devices +
+ {% if related_devices %} +
NameInstalled Device
+ {% for rd in related_devices %} - + + + {% endfor %} - -
— No device bays defined — + {{ rd }} + + {% if rd.rack %} + Rack {{ rd.rack }} + {% else %} + + {% endif %} + {{ rd.device_type.full_name }}
- + + {% else %} +
None found
+ {% endif %}
- {% if perms.dcim.delete_devicebay %} -
+
+
+
+
+ {% if device_bays or device.device_type.is_parent_device %} + {% if perms.dcim.delete_devicebay %} +
+ {% csrf_token %} + {% endif %} +
+
+ Device Bays +
+ + + + {% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %} + + {% endif %} + + + + + + + {% for devicebay in device_bays %} + {% include 'dcim/inc/devicebay.html' %} + {% empty %} + + + + {% endfor %} + +
NameInstalled Device
— No device bays defined —
+ +
+ {% if perms.dcim.delete_devicebay %} +
+ {% endif %} {% endif %} - {% endif %} - {% if interfaces or device.device_type.is_network_device %} - {% if perms.dcim.change_interface or perms.dcim.delete_interface %} -
- {% csrf_token %} - + {% if interfaces or device.device_type.is_network_device %} + {% if perms.dcim.change_interface or perms.dcim.delete_interface %} + + {% csrf_token %} + + {% endif %} +
+
+ Interfaces +
+ +
+
+ + + + {% if perms.dcim.change_interface or perms.dcim.delete_interface %} + + {% endif %} + + + + + + + + + + + {% for iface in interfaces %} + {% include 'dcim/inc/interface.html' %} + {% empty %} + + + + {% endfor %} + +
NameLAGDescriptionMTUMAC AddressConnection
— No interfaces defined —
+ +
+ {% if perms.dcim.delete_interface %} +
+ {% endif %} {% endif %} -
-
- Interfaces -
- + {% if cs_ports or device.device_type.is_console_server %} + {% if perms.dcim.delete_consoleserverport %} +
+ {% csrf_token %} + {% endif %} +
+
+ Console Server Ports +
+ + + + {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %} + + {% endif %} + + + + + + + {% for csp in cs_ports %} + {% include 'dcim/inc/consoleserverport.html' %} + {% empty %} + + + + {% endfor %} + +
NameConnection
— No console server ports defined —
+
- - - - {% if perms.dcim.change_interface or perms.dcim.delete_interface %} - - {% endif %} - - - - - - - - - - - {% for iface in interfaces %} - {% include 'dcim/inc/interface.html' %} - {% empty %} + {% if perms.dcim.delete_consoleserverport %} + + {% endif %} + {% endif %} + {% if power_outlets or device.device_type.is_pdu %} + {% if perms.dcim.delete_poweroutlet %} + + {% csrf_token %} + {% endif %} +
+
+ Power Outlets +
+
NameLAGDescriptionMTUMAC AddressConnection
+ - + {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %} + + {% endif %} + + + - {% endfor %} - -
— No interfaces defined —NameConnection
- -
- {% if perms.dcim.delete_interface %} - - {% endif %} - {% endif %} - {% if cs_ports or device.device_type.is_console_server %} - {% if perms.dcim.delete_consoleserverport %} -
- {% csrf_token %} - {% endif %} -
-
- Console Server Ports + + + {% for po in power_outlets %} + {% include 'dcim/inc/poweroutlet.html' %} + {% empty %} + + — No power outlets defined — + + {% endfor %} + + +
- - - - {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %} - - {% endif %} - - - - - - - {% for csp in cs_ports %} - {% include 'dcim/inc/consoleserverport.html' %} - {% empty %} - - - - {% endfor %} - -
NameConnection
— No console server ports defined —
- -
- {% if perms.dcim.delete_consoleserverport %} -
+ {% if perms.dcim.delete_poweroutlet %} + + {% endif %} {% endif %} - {% endif %} - {% if power_outlets or device.device_type.is_pdu %} - {% if perms.dcim.delete_poweroutlet %} -
- {% csrf_token %} - {% endif %} -
-
- Power Outlets -
- - - - {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %} - - {% endif %} - - - - - - - {% for po in power_outlets %} - {% include 'dcim/inc/poweroutlet.html' %} - {% empty %} - - - - {% endfor %} - -
NameConnection
— No power outlets defined —
- -
- {% if perms.dcim.delete_poweroutlet %} -
- {% endif %} - {% endif %} -
-
+
+
{% include 'inc/graphs_modal.html' %} {% include 'secrets/inc/private_key_modal.html' %} {% endblock %} diff --git a/netbox/templates/dcim/device_config.html b/netbox/templates/dcim/device_config.html index b62ff0211..210a9379a 100644 --- a/netbox/templates/dcim/device_config.html +++ b/netbox/templates/dcim/device_config.html @@ -1,11 +1,10 @@ -{% extends '_base.html' %} +{% extends 'dcim/device.html' %} {% load staticfiles %} {% block title %}{{ device }} - Config{% endblock %} {% block content %} {% include 'inc/ajax_loader.html' %} - {% include 'dcim/inc/device_header.html' with active_tab='config' %}
diff --git a/netbox/templates/dcim/device_inventory.html b/netbox/templates/dcim/device_inventory.html index 1db2dcefa..1efbd0fbc 100644 --- a/netbox/templates/dcim/device_inventory.html +++ b/netbox/templates/dcim/device_inventory.html @@ -1,77 +1,76 @@ -{% extends '_base.html' %} +{% extends 'dcim/device.html' %} {% block title %}{{ device }} - Inventory{% endblock %} {% block content %} -{% include 'dcim/inc/device_header.html' with active_tab='inventory' %} -
-
-
-
- Chassis -
- - - - - - - - - - - - - -
Model{{ device.device_type.full_name }}
Serial Number - {% if device.serial %} - {{ device.serial }} - {% else %} - N/A - {% endif %} -
Asset Tag - {% if device.asset_tag %} - {{ device.asset_tag }} - {% else %} - N/A - {% endif %} -
-
-
-
-
-
- Hardware -
- - - - - - - - - - - - - - - {% for item in inventory_items %} - {% with template_name='dcim/inc/inventoryitem.html' indent=0 %} - {% include template_name %} - {% endwith %} - {% endfor %} - -
NameManufacturerPart NumberSerial NumberAsset TagDescription
- {% if perms.dcim.add_inventoryitem %} - {% endblock %} diff --git a/netbox/templates/dcim/device_lldp_neighbors.html b/netbox/templates/dcim/device_lldp_neighbors.html index 0e423ad56..c0c82f459 100644 --- a/netbox/templates/dcim/device_lldp_neighbors.html +++ b/netbox/templates/dcim/device_lldp_neighbors.html @@ -1,10 +1,9 @@ -{% extends '_base.html' %} +{% extends 'dcim/device.html' %} {% block title %}{{ device }} - LLDP Neighbors{% endblock %} {% block content %} {% include 'inc/ajax_loader.html' %} - {% include 'dcim/inc/device_header.html' with active_tab='lldp-neighbors' %}
LLDP Neighbors diff --git a/netbox/templates/dcim/device_status.html b/netbox/templates/dcim/device_status.html index 7c62b3971..7743cc635 100644 --- a/netbox/templates/dcim/device_status.html +++ b/netbox/templates/dcim/device_status.html @@ -1,11 +1,10 @@ -{% extends '_base.html' %} +{% extends 'dcim/device.html' %} {% load staticfiles %} {% block title %}{{ device }} - Status{% endblock %} {% block content %} {% include 'inc/ajax_loader.html' %} - {% include 'dcim/inc/device_header.html' with active_tab='status' %}
diff --git a/netbox/templates/dcim/inc/device_header.html b/netbox/templates/dcim/inc/device_header.html deleted file mode 100644 index 92acd297d..000000000 --- a/netbox/templates/dcim/inc/device_header.html +++ /dev/null @@ -1,65 +0,0 @@ -
-
- -
-
-
-
- - - - -
-
-
-
-
- {% if perms.dcim.change_device %} - - - Edit this device - - {% endif %} - {% if perms.dcim.delete_device %} - - - Delete this device - -{% endif %} -
-

{{ device }}

-{% include 'inc/created_updated.html' with obj=device %} - diff --git a/netbox/templates/ipam/inc/prefix_header.html b/netbox/templates/ipam/inc/prefix_header.html deleted file mode 100644 index f3c694c64..000000000 --- a/netbox/templates/ipam/inc/prefix_header.html +++ /dev/null @@ -1,55 +0,0 @@ -
-
- -
-
-
-
- - - - -
-
-
-
-
- {% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %} - - Add Child Prefix - - {% endif %} - {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %} - - - Add an IP Address - - {% endif %} - {% if perms.ipam.change_prefix %} - - - Edit this prefix - - {% endif %} - {% if perms.ipam.delete_prefix %} - - - Delete this prefix - - {% endif %} -
-

{{ prefix }}

-{% include 'inc/created_updated.html' with obj=prefix %} - diff --git a/netbox/templates/ipam/inc/vlan_header.html b/netbox/templates/ipam/inc/vlan_header.html deleted file mode 100644 index bf5d4ccdd..000000000 --- a/netbox/templates/ipam/inc/vlan_header.html +++ /dev/null @@ -1,46 +0,0 @@ -
-
- -
-
-
-
- - - - -
-
-
-
-
- {% if perms.ipam.change_vlan %} - - - Edit this VLAN - - {% endif %} - {% if perms.ipam.delete_vlan %} - - - Delete this VLAN - - {% endif %} -
-

{% block title %}VLAN {{ vlan.display_name }}{% endblock %}

-{% include 'inc/created_updated.html' with obj=vlan %} - diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 29e9c07a0..c24e02414 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -1,152 +1,207 @@ {% extends '_base.html' %} {% load helpers %} -{% block title %}{{ prefix }}{% endblock %} +{% block header %} +
+
+ +
+
+
+
+ + + + +
+
+
+
+
+ {% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %} + + Add Child Prefix + + {% endif %} + {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %} + + + Add an IP Address + + {% endif %} + {% if perms.ipam.change_prefix %} + + + Edit this prefix + + {% endif %} + {% if perms.ipam.delete_prefix %} + + + Delete this prefix + + {% endif %} +
+

{% block title %}{{ prefix }}{% endblock %}

+ {% include 'inc/created_updated.html' with obj=prefix %} + +{% endblock %} {% block content %} -{% include 'ipam/inc/prefix_header.html' with active_tab='prefix' %} -
-
-
-
- Prefix +
+
+
+
+ Prefix +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Family{{ prefix.get_family_display }}
VRF + {% if prefix.vrf %} + {{ prefix.vrf }} ({{ prefix.vrf.rd }}) + {% else %} + Global + {% endif %} +
Tenant + {% if prefix.tenant %} + {% if prefix.tenant.group %} + {{ prefix.tenant.group }} + + {% endif %} + {{ prefix.tenant }} + {% elif prefix.vrf.tenant %} + {% if prefix.vrf.tenant.group %} + {{ prefix.vrf.tenant.group }} + + {% endif %} + {{ prefix.vrf.tenant }} + + {% else %} + None + {% endif %} +
Aggregate + {% if aggregate %} + {{ aggregate.prefix }} ({{ aggregate.rir }}) + {% else %} + None + {% endif %} +
Site + {% if prefix.site %} + {% if prefix.site.region %} + {{ prefix.site.region }} + + {% endif %} + {{ prefix.site }} + {% else %} + None + {% endif %} +
VLAN + {% if prefix.vlan %} + {% if prefix.vlan.group %} + {{ prefix.vlan.group }} + + {% endif %} + {{ prefix.vlan.display_name }} + {% else %} + None + {% endif %} +
Status + {{ prefix.get_status_display }} +
Role + {% if prefix.role %} + {{ prefix.role }} + {% else %} + None + {% endif %} +
Description + {% if prefix.description %} + {{ prefix.description }} + {% else %} + N/A + {% endif %} +
Is a pool + {% if prefix.is_pool %} + + {% else %} + + {% endif %} +
Tags + {% for tag in prefix.tags.all %} + {% tag 'ipam:prefix_list' tag %} + {% empty %} + N/A + {% endfor %} +
Utilization{% utilization_graph prefix.get_utilization %}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Family{{ prefix.get_family_display }}
VRF - {% if prefix.vrf %} - {{ prefix.vrf }} ({{ prefix.vrf.rd }}) - {% else %} - Global - {% endif %} -
Tenant - {% if prefix.tenant %} - {% if prefix.tenant.group %} - {{ prefix.tenant.group }} - - {% endif %} - {{ prefix.tenant }} - {% elif prefix.vrf.tenant %} - {% if prefix.vrf.tenant.group %} - {{ prefix.vrf.tenant.group }} - - {% endif %} - {{ prefix.vrf.tenant }} - - {% else %} - None - {% endif %} -
Aggregate - {% if aggregate %} - {{ aggregate.prefix }} ({{ aggregate.rir }}) - {% else %} - None - {% endif %} -
Site - {% if prefix.site %} - {% if prefix.site.region %} - {{ prefix.site.region }} - - {% endif %} - {{ prefix.site }} - {% else %} - None - {% endif %} -
VLAN - {% if prefix.vlan %} - {% if prefix.vlan.group %} - {{ prefix.vlan.group }} - - {% endif %} - {{ prefix.vlan.display_name }} - {% else %} - None - {% endif %} -
Status - {{ prefix.get_status_display }} -
Role - {% if prefix.role %} - {{ prefix.role }} - {% else %} - None - {% endif %} -
Description - {% if prefix.description %} - {{ prefix.description }} - {% else %} - N/A - {% endif %} -
Is a pool - {% if prefix.is_pool %} - - {% else %} - - {% endif %} -
Tags - {% for tag in prefix.tags.all %} - {% tag 'ipam:prefix_list' tag %} - {% empty %} - N/A - {% endfor %} -
Utilization{% utilization_graph prefix.get_utilization %}
+ {% with prefix.get_custom_fields as custom_fields %} + {% include 'inc/custom_fields_panel.html' %} + {% endwith %} +
- {% with prefix.get_custom_fields as custom_fields %} - {% include 'inc/custom_fields_panel.html' %} - {% endwith %} -
-
-
- {% if duplicate_prefix_table.rows %} - {% include 'panel_table.html' with table=duplicate_prefix_table heading='Duplicate Prefixes' panel_class='danger' %} - {% endif %} - {% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' panel_class='default' %} -
-
+
+ {% if duplicate_prefix_table.rows %} + {% include 'panel_table.html' with table=duplicate_prefix_table heading='Duplicate Prefixes' panel_class='danger' %} + {% endif %} + {% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' panel_class='default' %} +
+
{% endblock %} diff --git a/netbox/templates/ipam/prefix_ipaddresses.html b/netbox/templates/ipam/prefix_ipaddresses.html index 02e90569d..1da5b7518 100644 --- a/netbox/templates/ipam/prefix_ipaddresses.html +++ b/netbox/templates/ipam/prefix_ipaddresses.html @@ -1,9 +1,8 @@ -{% extends '_base.html' %} +{% extends 'ipam/prefix.html' %} -{% block title %}{{ prefix }} - IP Addresses{% endblock %} +{% block title %}{{ block.super }} - IP Addresses{% endblock %} {% block content %} - {% include 'ipam/inc/prefix_header.html' with active_tab='ip-addresses' %}
{% include 'utilities/obj_table.html' with table=ip_table table_template='panel_table.html' heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %} diff --git a/netbox/templates/ipam/prefix_prefixes.html b/netbox/templates/ipam/prefix_prefixes.html index 2535b672d..9cf50a640 100644 --- a/netbox/templates/ipam/prefix_prefixes.html +++ b/netbox/templates/ipam/prefix_prefixes.html @@ -1,9 +1,8 @@ -{% extends '_base.html' %} +{% extends 'ipam/prefix.html' %} -{% block title %}{{ prefix }} - Prefixes{% endblock %} +{% block title %}{{ block.super }} - Prefixes{% endblock %} {% block content %} - {% include 'ipam/inc/prefix_header.html' with active_tab='prefixes' %}
{% include 'utilities/obj_table.html' with table=prefix_table table_template='panel_table.html' heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %} diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index ac874282f..76ee5f65e 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -1,118 +1,166 @@ {% extends '_base.html' %} {% load helpers %} -{% block content %} -{% include 'ipam/inc/vlan_header.html' with active_tab='vlan' %} -
-
-
-
- VLAN -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Site - {% if vlan.site %} - {% if vlan.site.region %} - {{ vlan.site.region }} - - {% endif %} - {{ vlan.site }} - {% else %} - None - {% endif %} -
Group - {% if vlan.group %} - {{ vlan.group }} - {% else %} - None - {% endif %} -
VLAN ID{{ vlan.vid }}
Name{{ vlan.name }}
Tenant - {% if vlan.tenant %} - {% if vlan.tenant.group %} - {{ vlan.tenant.group }} - - {% endif %} - {{ vlan.tenant }} - {% else %} - None - {% endif %} -
Status - {{ vlan.get_status_display }} -
Role - {% if vlan.role %} - {{ vlan.role }} - {% else %} - None - {% endif %} -
Description - {% if vlan.description %} - {{ vlan.description }} - {% else %} - N/A - {% endif %} -
Tags - {% for tag in vlan.tags.all %} - {% tag 'ipam:vlan_list' tag %} - {% empty %} - N/A - {% endfor %} -
+{% block header %} +
+
+
- {% with vlan.get_custom_fields as custom_fields %} - {% include 'inc/custom_fields_panel.html' %} - {% endwith %} -
-
-
-
- Prefixes +
+
+
+ + + +
- {% include 'responsive_table.html' with table=prefix_table %} - {% if perms.ipam.add_prefix %} - - {% endif %} +
-
-
+
+
+ {% if perms.ipam.change_vlan %} + + + Edit this VLAN + + {% endif %} + {% if perms.ipam.delete_vlan %} + + + Delete this VLAN + + {% endif %} +
+

{% block title %}VLAN {{ vlan.display_name }}{% endblock %}

+ {% include 'inc/created_updated.html' with obj=vlan %} + +{% endblock %} + +{% block content %} +
+
+
+
+ VLAN +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Site + {% if vlan.site %} + {% if vlan.site.region %} + {{ vlan.site.region }} + + {% endif %} + {{ vlan.site }} + {% else %} + None + {% endif %} +
Group + {% if vlan.group %} + {{ vlan.group }} + {% else %} + None + {% endif %} +
VLAN ID{{ vlan.vid }}
Name{{ vlan.name }}
Tenant + {% if vlan.tenant %} + {% if vlan.tenant.group %} + {{ vlan.tenant.group }} + + {% endif %} + {{ vlan.tenant }} + {% else %} + None + {% endif %} +
Status + {{ vlan.get_status_display }} +
Role + {% if vlan.role %} + {{ vlan.role }} + {% else %} + None + {% endif %} +
Description + {% if vlan.description %} + {{ vlan.description }} + {% else %} + N/A + {% endif %} +
Tags + {% for tag in vlan.tags.all %} + {% tag 'ipam:vlan_list' tag %} + {% empty %} + N/A + {% endfor %} +
+
+ {% with vlan.get_custom_fields as custom_fields %} + {% include 'inc/custom_fields_panel.html' %} + {% endwith %} +
+
+
+
+ Prefixes +
+ {% include 'responsive_table.html' with table=prefix_table %} + {% if perms.ipam.add_prefix %} + + {% endif %} +
+
+
{% endblock %} diff --git a/netbox/templates/ipam/vlan_members.html b/netbox/templates/ipam/vlan_members.html index 27d5d50f7..9fc803e09 100644 --- a/netbox/templates/ipam/vlan_members.html +++ b/netbox/templates/ipam/vlan_members.html @@ -1,9 +1,8 @@ -{% extends '_base.html' %} +{% extends 'ipam/vlan.html' %} -{% block title %}{{ vlan }} - Members{% endblock %} +{% block title %}{{ block.super }} - Members{% endblock %} {% block content %} - {% include 'ipam/inc/vlan_header.html' with active_tab='members' %}
{% include 'utilities/obj_table.html' with table=members_table table_template='panel_table.html' heading='VLAN Members' parent=vlan %} From 3c2e0b0b173dc50ad50fd504a1e923dd1e3c3063 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 14 Jun 2018 16:15:14 -0400 Subject: [PATCH 04/22] Added changelog views --- netbox/circuits/urls.py | 5 + netbox/dcim/urls.py | 19 +- netbox/extras/admin.py | 4 +- netbox/extras/models.py | 5 + netbox/extras/views.py | 39 +++- netbox/ipam/urls.py | 11 ++ netbox/secrets/urls.py | 4 + netbox/templates/circuits/circuit.html | 87 +++++---- netbox/templates/circuits/provider.html | 97 +++++----- netbox/templates/dcim/device.html | 11 +- netbox/templates/dcim/devicetype.html | 67 ++++--- netbox/templates/dcim/rack.html | 93 +++++----- netbox/templates/dcim/site.html | 106 ++++++----- netbox/templates/extras/changelog.html | 36 ++++ netbox/templates/ipam/aggregate.html | 85 +++++---- netbox/templates/ipam/ipaddress.html | 89 +++++---- netbox/templates/ipam/prefix.html | 15 +- netbox/templates/ipam/vlan.html | 11 +- netbox/templates/ipam/vrf.html | 83 +++++---- netbox/templates/secrets/secret.html | 61 +++--- netbox/templates/tenancy/tenant.html | 89 +++++---- netbox/templates/virtualization/cluster.html | 89 +++++---- .../virtualization/virtualmachine.html | 173 ++++++++++-------- netbox/tenancy/urls.py | 4 + netbox/virtualization/urls.py | 6 + netbox/virtualization/views.py | 8 +- 26 files changed, 790 insertions(+), 507 deletions(-) create mode 100644 netbox/templates/extras/changelog.html diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index 569c1eb9a..6b5529a42 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -2,7 +2,9 @@ from __future__ import unicode_literals from django.conf.urls import url +from extras.views import ChangeLogView from . import views +from .models import Circuit, CircuitType, Provider app_name = 'circuits' urlpatterns = [ @@ -16,6 +18,7 @@ urlpatterns = [ url(r'^providers/(?P[\w-]+)/$', views.ProviderView.as_view(), name='provider'), url(r'^providers/(?P[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'), url(r'^providers/(?P[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'), + url(r'^providers/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), # Circuit types url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'), @@ -23,6 +26,7 @@ urlpatterns = [ url(r'^circuit-types/import/$', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), url(r'^circuit-types/(?P[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), + url(r'^circuit-types/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), # Circuits url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'), @@ -33,6 +37,7 @@ urlpatterns = [ url(r'^circuits/(?P\d+)/$', views.CircuitView.as_view(), name='circuit'), url(r'^circuits/(?P\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'), url(r'^circuits/(?P\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'), + url(r'^circuits/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), url(r'^circuits/(?P\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'), # Circuit terminations diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 5682bd8e7..67e957f9b 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -2,11 +2,14 @@ from __future__ import unicode_literals from django.conf.urls import url -from extras.views import ImageAttachmentEditView +from extras.views import ChangeLogView, ImageAttachmentEditView from ipam.views import ServiceCreateView from secrets.views import secret_add from . import views -from .models import Device, Rack, Site +from .models import ( + Device, DeviceRole, DeviceType, Manufacturer, Platform, Rack, RackGroup, RackReservation, RackRole, Region, Site, + VirtualChassis, +) app_name = 'dcim' urlpatterns = [ @@ -17,6 +20,7 @@ urlpatterns = [ url(r'^regions/import/$', views.RegionBulkImportView.as_view(), name='region_import'), url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), url(r'^regions/(?P\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'), + url(r'^regions/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), # Sites url(r'^sites/$', views.SiteListView.as_view(), name='site_list'), @@ -26,6 +30,7 @@ urlpatterns = [ url(r'^sites/(?P[\w-]+)/$', views.SiteView.as_view(), name='site'), url(r'^sites/(?P[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'), url(r'^sites/(?P[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'), + url(r'^sites/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), url(r'^sites/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), # Rack groups @@ -34,6 +39,7 @@ urlpatterns = [ url(r'^rack-groups/import/$', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'), url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), url(r'^rack-groups/(?P\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'), + url(r'^rack-groups/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}), # Rack roles url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'), @@ -41,6 +47,7 @@ urlpatterns = [ url(r'^rack-roles/import/$', views.RackRoleBulkImportView.as_view(), name='rackrole_import'), url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), url(r'^rack-roles/(?P\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'), + url(r'^rack-roles/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), # Rack reservations url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'), @@ -48,6 +55,7 @@ urlpatterns = [ url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), url(r'^rack-reservations/(?P\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'), url(r'^rack-reservations/(?P\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), + url(r'^rack-reservations/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), # Racks url(r'^racks/$', views.RackListView.as_view(), name='rack_list'), @@ -59,6 +67,7 @@ urlpatterns = [ url(r'^racks/(?P\d+)/$', views.RackView.as_view(), name='rack'), url(r'^racks/(?P\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'), url(r'^racks/(?P\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'), + url(r'^racks/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), url(r'^racks/(?P\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'), url(r'^racks/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), @@ -68,6 +77,7 @@ urlpatterns = [ url(r'^manufacturers/import/$', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'), url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), url(r'^manufacturers/(?P[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), + url(r'^manufacturers/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), # Device types url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'), @@ -78,6 +88,7 @@ urlpatterns = [ url(r'^device-types/(?P\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'), url(r'^device-types/(?P\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), url(r'^device-types/(?P\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), + url(r'^device-types/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), # Console port templates url(r'^device-types/(?P\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'), @@ -110,6 +121,7 @@ urlpatterns = [ url(r'^device-roles/import/$', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'), url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), url(r'^device-roles/(?P[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), + url(r'^device-roles/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), # Platforms url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'), @@ -117,6 +129,7 @@ urlpatterns = [ url(r'^platforms/import/$', views.PlatformBulkImportView.as_view(), name='platform_import'), url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), url(r'^platforms/(?P[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'), + url(r'^platforms/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), # Devices url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'), @@ -128,6 +141,7 @@ urlpatterns = [ url(r'^devices/(?P\d+)/$', views.DeviceView.as_view(), name='device'), url(r'^devices/(?P\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'), url(r'^devices/(?P\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), + url(r'^devices/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), url(r'^devices/(?P\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), url(r'^devices/(?P\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'), url(r'^devices/(?P\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), @@ -221,6 +235,7 @@ urlpatterns = [ url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), url(r'^virtual-chassis/(?P\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), url(r'^virtual-chassis/(?P\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), + url(r'^virtual-chassis/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), url(r'^virtual-chassis/(?P\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), url(r'^virtual-chassis-members/(?P\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 200387f88..3da723b7d 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -132,7 +132,7 @@ class TopologyMapAdmin(admin.ModelAdmin): @admin.register(ObjectChange) class ObjectChangeAdmin(admin.ModelAdmin): actions = None - fields = ['time', 'content_type', 'display_object', 'action', 'display_user'] + fields = ['time', 'content_type', 'display_object', 'action', 'display_user', 'object_data'] list_display = ['time', 'content_type', 'display_object', 'display_action', 'display_user'] list_filter = ['time', 'action', 'user__username'] list_select_related = ['content_type', 'user'] @@ -156,7 +156,7 @@ class ObjectChangeAdmin(admin.ModelAdmin): OBJECTCHANGE_ACTION_DELETE: 'deletelink', } return mark_safe('{}'.format(icon[obj.action], obj.get_action_display())) - display_user.short_description = 'action' + display_action.short_description = 'action' def display_object(self, obj): if hasattr(obj.changed_object, 'get_absolute_url'): diff --git a/netbox/extras/models.py b/netbox/extras/models.py index e9fb2d543..759f90b4b 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from collections import OrderedDict from datetime import date +import json import graphviz from django.contrib.auth.models import User @@ -720,6 +721,10 @@ class ObjectChange(models.Model): return super(ObjectChange, self).save(*args, **kwargs) + @property + def object_data_pretty(self): + return json.dumps(self.object_data, indent=4, sort_keys=True) + # # User actions diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 130437356..d3dd2c3d2 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals +from django import template from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin +from django.contrib.contenttypes.models import ContentType from django.db.models import Count from django.http import Http404 from django.shortcuts import get_object_or_404, redirect, render, reverse @@ -12,7 +14,7 @@ from taggit.models import Tag from utilities.forms import ConfirmationForm from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView from .forms import ImageAttachmentForm, TagForm -from .models import ImageAttachment, ReportResult, UserAction +from .models import ImageAttachment, ObjectChange, ReportResult, UserAction from .reports import get_report, get_reports from .tables import TagTable @@ -50,6 +52,41 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): default_return_url = 'extras:tag_list' +# +# Change logging +# + +class ChangeLogView(View): + """ + Present a history of changes made to an object. + """ + + def get(self, request, model, **kwargs): + + # Get object my model and kwargs (e.g. slug='foo') + obj = get_object_or_404(model, **kwargs) + + # Gather all changes for this object + content_type = ContentType.objects.get_for_model(model) + changes = ObjectChange.objects.filter(content_type=content_type, object_id=obj.pk) + + # Check whether a header template exists for this model + base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name) + try: + template.loader.get_template(base_template) + object_var = model._meta.model_name + except template.TemplateDoesNotExist: + base_template = '_base.html' + object_var = 'obj' + + return render(request, 'extras/changelog.html', { + object_var: obj, + 'changes': changes, + 'base_template': base_template, + 'active_tab': 'changelog', + }) + + # # Image attachments # diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index aa7c17a5c..89c30a6db 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -2,7 +2,9 @@ from __future__ import unicode_literals from django.conf.urls import url +from extras.views import ChangeLogView from . import views +from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF app_name = 'ipam' @@ -17,6 +19,7 @@ urlpatterns = [ url(r'^vrfs/(?P\d+)/$', views.VRFView.as_view(), name='vrf'), url(r'^vrfs/(?P\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'), url(r'^vrfs/(?P\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'), + url(r'^vrfs/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}), # RIRs url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'), @@ -24,6 +27,7 @@ urlpatterns = [ url(r'^rirs/import/$', views.RIRBulkImportView.as_view(), name='rir_import'), url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'), url(r'^rirs/(?P[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'), + url(r'^vrfs/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}), # Aggregates url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'), @@ -34,6 +38,7 @@ urlpatterns = [ url(r'^aggregates/(?P\d+)/$', views.AggregateView.as_view(), name='aggregate'), url(r'^aggregates/(?P\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'), url(r'^aggregates/(?P\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'), + url(r'^aggregates/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}), # Roles url(r'^roles/$', views.RoleListView.as_view(), name='role_list'), @@ -41,6 +46,7 @@ urlpatterns = [ url(r'^roles/import/$', views.RoleBulkImportView.as_view(), name='role_import'), url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'), url(r'^roles/(?P[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'), + url(r'^roles/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}), # Prefixes url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'), @@ -51,6 +57,7 @@ urlpatterns = [ url(r'^prefixes/(?P\d+)/$', views.PrefixView.as_view(), name='prefix'), url(r'^prefixes/(?P\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'), url(r'^prefixes/(?P\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'), + url(r'^prefixes/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}), url(r'^prefixes/(?P\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'), url(r'^prefixes/(?P\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), @@ -61,6 +68,7 @@ urlpatterns = [ url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), + url(r'^ip-addresses/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}), url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'), url(r'^ip-addresses/(?P\d+)/$', views.IPAddressView.as_view(), name='ipaddress'), url(r'^ip-addresses/(?P\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'), @@ -72,6 +80,7 @@ urlpatterns = [ url(r'^vlan-groups/import/$', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'), url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'), url(r'^vlan-groups/(?P\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), + url(r'^vlan-groups/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), # VLANs url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'), @@ -83,9 +92,11 @@ urlpatterns = [ url(r'^vlans/(?P\d+)/members/$', views.VLANMembersView.as_view(), name='vlan_members'), url(r'^vlans/(?P\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'), url(r'^vlans/(?P\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'), + url(r'^vlans/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}), # Services url(r'^services/(?P\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'), url(r'^services/(?P\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'), + url(r'^services/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}), ] diff --git a/netbox/secrets/urls.py b/netbox/secrets/urls.py index cd6415719..9c9324c4a 100644 --- a/netbox/secrets/urls.py +++ b/netbox/secrets/urls.py @@ -2,7 +2,9 @@ from __future__ import unicode_literals from django.conf.urls import url +from extras.views import ChangeLogView from . import views +from .models import Secret, SecretRole app_name = 'secrets' urlpatterns = [ @@ -13,6 +15,7 @@ urlpatterns = [ url(r'^secret-roles/import/$', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'), url(r'^secret-roles/delete/$', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'), url(r'^secret-roles/(?P[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'), + url(r'^secret-roles/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}), # Secrets url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'), @@ -22,5 +25,6 @@ urlpatterns = [ url(r'^secrets/(?P\d+)/$', views.SecretView.as_view(), name='secret'), url(r'^secrets/(?P\d+)/edit/$', views.secret_edit, name='secret_edit'), url(r'^secrets/(?P\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'), + url(r'^secrets/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}), ] diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 509c6da89..048c16862 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -1,44 +1,57 @@ {% extends '_base.html' %} {% load helpers %} +{% block title %}{{ circuit }}{% endblock %} + +{% block header %} +
+
+ +
+
+
+
+ + + + +
+
+
+
+
+ {% if perms.circuits.change_circuit %} + + + Edit this circuit + + {% endif %} + {% if perms.circuits.delete_circuit %} + + + Delete this circuit + + {% endif %} +
+

{{ circuit }}

+ {% include 'inc/created_updated.html' with obj=circuit %} + +{% endblock %} + {% block content %} -
-
- -
-
-
-
- - - - -
-
-
-
-
- {% if perms.circuits.change_circuit %} - - - Edit this circuit - - {% endif %} - {% if perms.circuits.delete_circuit %} - - - Delete this circuit - - {% endif %} -
-

{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}

-{% include 'inc/created_updated.html' with obj=circuit %}
diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index e19175c7f..d2fed8647 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -2,49 +2,62 @@ {% load static from staticfiles %} {% load helpers %} +{% block title %}{{ provider }}{% endblock %} + +{% block header %} +
+
+ +
+
+
+
+ + + + +
+
+
+
+
+ {% if show_graphs %} + + {% endif %} + {% if perms.circuits.change_provider %} + + + Edit this provider + + {% endif %} + {% if perms.circuits.delete_provider %} + + + Delete this provider + + {% endif %} +
+

{{ provider }}

+ {% include 'inc/created_updated.html' with obj=provider %} + +{% endblock %} + {% block content %} -
-
- -
-
-
-
- - - - -
-
-
-
-
- {% if show_graphs %} - - {% endif %} - {% if perms.circuits.change_provider %} - - - Edit this provider - - {% endif %} - {% if perms.circuits.delete_provider %} - - - Delete this provider - - {% endif %} -
-

{% block title %}{{ provider }}{% endblock %}

-{% include 'inc/created_updated.html' with obj=provider %}
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 0cc1e4cf8..1c1539f89 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -2,6 +2,8 @@ {% load static from staticfiles %} {% load helpers %} +{% block title %}{{ device }}{% endblock %} + {% block header %}
@@ -45,11 +47,11 @@ {% endif %}
-

{% block title %}{{ device }}{% endblock %}

+

{{ device }}

{% include 'inc/created_updated.html' with obj=device %} -