mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Merge pull request #2198 from digitalocean/1898-activity-logging
Closes #1898: Change logging
This commit is contained in:
commit
ffcbc54522
@ -44,6 +44,14 @@ BASE_PATH = 'netbox/'
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG_RETENTION
|
||||
|
||||
Default: 90
|
||||
|
||||
The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain changes in the database indefinitely. (Warning: This will greatly increase database size over time.)
|
||||
|
||||
---
|
||||
|
||||
## CORS_ORIGIN_ALLOW_ALL
|
||||
|
||||
Default: False
|
||||
|
45
netbox/circuits/migrations/0012_change_logging.py
Normal file
45
netbox/circuits/migrations/0012_change_logging.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
@ -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']
|
||||
|
@ -9,6 +9,9 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
CIRCUITTYPE_ACTIONS = """
|
||||
<a href="{% url 'circuits:circuittype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.circuit.change_circuittype %}
|
||||
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
|
@ -2,7 +2,9 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from extras.views import ObjectChangeLogView
|
||||
from . import views
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
app_name = 'circuits'
|
||||
urlpatterns = [
|
||||
@ -16,6 +18,7 @@ urlpatterns = [
|
||||
url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.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<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||
url(r'^circuit-types/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.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<pk>\d+)/$', views.CircuitView.as_view(), name='circuit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
|
||||
url(r'^circuits/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
|
||||
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
||||
|
||||
# Circuit terminations
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
@ -8,7 +8,7 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0059_devicetype_add_created_updated_times'),
|
||||
('dcim', '0058_relax_rack_naming_constraints'),
|
||||
]
|
||||
|
||||
operations = [
|
135
netbox/dcim/migrations/0060_change_logging.py
Normal file
135
netbox/dcim/migrations/0060_change_logging.py
Normal file
@ -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', '0059_site_latitude_longitude'),
|
||||
]
|
||||
|
||||
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),
|
||||
),
|
||||
]
|
@ -18,22 +18,48 @@ from taggit.managers import TaggableManager
|
||||
from timezone_field import TimeZoneField
|
||||
|
||||
from circuits.models import Circuit
|
||||
from extras.models import CustomFieldModel
|
||||
from extras.constants import OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||
from extras.models import CustomFieldModel, ObjectChange
|
||||
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 utilities.utils import serialize_object
|
||||
from .constants import *
|
||||
from .fields import ASNField, MACAddressField
|
||||
from .querysets import InterfaceQuerySet
|
||||
|
||||
|
||||
class ComponentModel(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get_component_parent(self):
|
||||
raise NotImplementedError(
|
||||
"ComponentModel must implement get_component_parent()"
|
||||
)
|
||||
|
||||
def log_change(self, user, request_id, action):
|
||||
"""
|
||||
Log an ObjectChange including the parent Device/VM.
|
||||
"""
|
||||
ObjectChange(
|
||||
user=user,
|
||||
request_id=request_id,
|
||||
changed_object=self,
|
||||
related_object=self.get_component_parent(),
|
||||
action=action,
|
||||
object_data=serialize_object(self)
|
||||
).save()
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Region(MPTTModel):
|
||||
class Region(MPTTModel, ChangeLoggedModel):
|
||||
"""
|
||||
Sites can be grouped within geographic Regions.
|
||||
"""
|
||||
@ -53,6 +79,7 @@ class Region(MPTTModel):
|
||||
unique=True
|
||||
)
|
||||
|
||||
serializer = 'dcim.api.serializers.RegionSerializer'
|
||||
csv_headers = ['name', 'slug', 'parent']
|
||||
|
||||
class MPTTMeta:
|
||||
@ -81,7 +108,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).
|
||||
@ -174,13 +201,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', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
]
|
||||
|
||||
serializer = 'dcim.api.serializers.SiteSerializer'
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -245,7 +271,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
|
||||
@ -261,9 +287,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']
|
||||
@ -287,7 +312,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.
|
||||
"""
|
||||
@ -300,6 +325,7 @@ class RackRole(models.Model):
|
||||
)
|
||||
color = ColorField()
|
||||
|
||||
serializer = 'dcim.api.serializers.RackRoleSerializer'
|
||||
csv_headers = ['name', 'slug', 'color']
|
||||
|
||||
class Meta:
|
||||
@ -324,7 +350,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.
|
||||
@ -406,13 +432,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 = [
|
||||
@ -584,7 +609,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class RackReservation(models.Model):
|
||||
class RackReservation(ChangeLoggedModel):
|
||||
"""
|
||||
One or more reserved units within a Rack.
|
||||
"""
|
||||
@ -596,9 +621,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,
|
||||
@ -614,6 +636,8 @@ class RackReservation(models.Model):
|
||||
max_length=100
|
||||
)
|
||||
|
||||
serializer = 'dcim.api.serializers.RackReservationSerializer'
|
||||
|
||||
class Meta:
|
||||
ordering = ['created']
|
||||
|
||||
@ -661,7 +685,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.
|
||||
"""
|
||||
@ -673,6 +697,7 @@ class Manufacturer(models.Model):
|
||||
unique=True
|
||||
)
|
||||
|
||||
serializer = 'dcim.api.serializers.ManufacturerSerializer'
|
||||
csv_headers = ['name', 'slug']
|
||||
|
||||
class Meta:
|
||||
@ -692,7 +717,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).
|
||||
@ -767,6 +792,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',
|
||||
@ -866,7 +892,7 @@ class DeviceType(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ConsolePortTemplate(models.Model):
|
||||
class ConsolePortTemplate(ComponentModel):
|
||||
"""
|
||||
A template for a ConsolePort to be created for a new Device.
|
||||
"""
|
||||
@ -886,9 +912,12 @@ class ConsolePortTemplate(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device_type
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ConsoleServerPortTemplate(models.Model):
|
||||
class ConsoleServerPortTemplate(ComponentModel):
|
||||
"""
|
||||
A template for a ConsoleServerPort to be created for a new Device.
|
||||
"""
|
||||
@ -908,9 +937,12 @@ class ConsoleServerPortTemplate(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device_type
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class PowerPortTemplate(models.Model):
|
||||
class PowerPortTemplate(ComponentModel):
|
||||
"""
|
||||
A template for a PowerPort to be created for a new Device.
|
||||
"""
|
||||
@ -930,9 +962,12 @@ class PowerPortTemplate(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device_type
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class PowerOutletTemplate(models.Model):
|
||||
class PowerOutletTemplate(ComponentModel):
|
||||
"""
|
||||
A template for a PowerOutlet to be created for a new Device.
|
||||
"""
|
||||
@ -952,9 +987,12 @@ class PowerOutletTemplate(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device_type
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class InterfaceTemplate(models.Model):
|
||||
class InterfaceTemplate(ComponentModel):
|
||||
"""
|
||||
A template for a physical data interface on a new Device.
|
||||
"""
|
||||
@ -984,9 +1022,12 @@ class InterfaceTemplate(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device_type
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DeviceBayTemplate(models.Model):
|
||||
class DeviceBayTemplate(ComponentModel):
|
||||
"""
|
||||
A template for a DeviceBay to be created for a new parent Device.
|
||||
"""
|
||||
@ -1006,13 +1047,16 @@ class DeviceBayTemplate(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device_type
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
@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
|
||||
@ -1032,6 +1076,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:
|
||||
@ -1053,7 +1098,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
|
||||
@ -1087,6 +1132,7 @@ class Platform(models.Model):
|
||||
verbose_name='Legacy RPC client'
|
||||
)
|
||||
|
||||
serializer = 'dcim.api.serializers.PlatformSerializer'
|
||||
csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver']
|
||||
|
||||
class Meta:
|
||||
@ -1112,7 +1158,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.
|
||||
@ -1252,13 +1298,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 = [
|
||||
@ -1501,7 +1546,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ConsolePort(models.Model):
|
||||
class ConsolePort(ComponentModel):
|
||||
"""
|
||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||
"""
|
||||
@ -1538,6 +1583,9 @@ class ConsolePort(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.cs_port.device.identifier if self.cs_port else None,
|
||||
@ -1563,7 +1611,7 @@ class ConsoleServerPortManager(models.Manager):
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ConsoleServerPort(models.Model):
|
||||
class ConsoleServerPort(ComponentModel):
|
||||
"""
|
||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||
"""
|
||||
@ -1587,6 +1635,9 @@ class ConsoleServerPort(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Check that the parent device's DeviceType is a console server
|
||||
@ -1604,7 +1655,7 @@ class ConsoleServerPort(models.Model):
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class PowerPort(models.Model):
|
||||
class PowerPort(ComponentModel):
|
||||
"""
|
||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||
"""
|
||||
@ -1640,6 +1691,9 @@ class PowerPort(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.power_outlet.device.identifier if self.power_outlet else None,
|
||||
@ -1665,7 +1719,7 @@ class PowerOutletManager(models.Manager):
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class PowerOutlet(models.Model):
|
||||
class PowerOutlet(ComponentModel):
|
||||
"""
|
||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||
"""
|
||||
@ -1689,6 +1743,9 @@ class PowerOutlet(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Check that the parent device's DeviceType is a PDU
|
||||
@ -1706,7 +1763,7 @@ class PowerOutlet(models.Model):
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Interface(models.Model):
|
||||
class Interface(ComponentModel):
|
||||
"""
|
||||
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
|
||||
Interface via the creation of an InterfaceConnection.
|
||||
@ -1796,6 +1853,9 @@ class Interface(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return self.parent.get_absolute_url()
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device or self.virtual_machine
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Check that the parent device's DeviceType is a network device
|
||||
@ -1866,6 +1926,23 @@ class Interface(models.Model):
|
||||
|
||||
return super(Interface, self).save(*args, **kwargs)
|
||||
|
||||
def log_change(self, user, request_id, action):
|
||||
"""
|
||||
Include the connected Interface (if any).
|
||||
"""
|
||||
ObjectChange(
|
||||
user=user,
|
||||
request_id=request_id,
|
||||
changed_object=self,
|
||||
related_object=self.get_component_parent(),
|
||||
action=action,
|
||||
object_data=serialize_object(self, extra={
|
||||
'connected_interface': self.connected_interface.pk if self.connection else None,
|
||||
'connection_status': self.connection.connection_status if self.connection else None,
|
||||
})
|
||||
).save()
|
||||
|
||||
# TODO: Replace `parent` with get_component_parent() (from ComponentModel)
|
||||
@property
|
||||
def parent(self):
|
||||
return self.device or self.virtual_machine
|
||||
@ -1970,13 +2047,40 @@ class InterfaceConnection(models.Model):
|
||||
self.get_connection_status_display(),
|
||||
)
|
||||
|
||||
def log_change(self, user, request_id, action):
|
||||
"""
|
||||
Create a new ObjectChange for each of the two affected Interfaces.
|
||||
"""
|
||||
interfaces = (
|
||||
(self.interface_a, self.interface_b),
|
||||
(self.interface_b, self.interface_a),
|
||||
)
|
||||
for interface, peer_interface in interfaces:
|
||||
if action == OBJECTCHANGE_ACTION_DELETE:
|
||||
connection_data = {
|
||||
'connected_interface': None,
|
||||
}
|
||||
else:
|
||||
connection_data = {
|
||||
'connected_interface': peer_interface.pk,
|
||||
'connection_status': self.connection_status
|
||||
}
|
||||
ObjectChange(
|
||||
user=user,
|
||||
request_id=request_id,
|
||||
changed_object=interface,
|
||||
related_object=interface.parent,
|
||||
action=OBJECTCHANGE_ACTION_UPDATE,
|
||||
object_data=serialize_object(interface, extra=connection_data)
|
||||
).save()
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DeviceBay(models.Model):
|
||||
class DeviceBay(ComponentModel):
|
||||
"""
|
||||
An empty space within a Device which can house a child device
|
||||
"""
|
||||
@ -2007,6 +2111,9 @@ class DeviceBay(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate that the parent Device can have DeviceBays
|
||||
@ -2025,7 +2132,7 @@ class DeviceBay(models.Model):
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class InventoryItem(models.Model):
|
||||
class InventoryItem(ComponentModel):
|
||||
"""
|
||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||
InventoryItems are used only for inventory purposes.
|
||||
@ -2094,6 +2201,9 @@ class InventoryItem(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
||||
def get_component_parent(self):
|
||||
return self.device
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.name or '{' + self.device.pk + '}',
|
||||
@ -2112,7 +2222,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).
|
||||
"""
|
||||
@ -2126,6 +2236,8 @@ class VirtualChassis(models.Model):
|
||||
blank=True
|
||||
)
|
||||
|
||||
serializer = 'dcim.api.serializers.VirtualChassisSerializer'
|
||||
|
||||
class Meta:
|
||||
ordering = ['master']
|
||||
verbose_name_plural = 'virtual chassis'
|
||||
|
@ -41,12 +41,18 @@ DEVICE_LINK = """
|
||||
"""
|
||||
|
||||
REGION_ACTIONS = """
|
||||
<a href="{% url 'dcim:region_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_region %}
|
||||
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACKGROUP_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackgroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
@ -58,6 +64,9 @@ RACKGROUP_ACTIONS = """
|
||||
"""
|
||||
|
||||
RACKROLE_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackrole_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackrole %}
|
||||
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
@ -76,20 +85,29 @@ RACK_DEVICE_COUNT = """
|
||||
"""
|
||||
|
||||
RACKRESERVATION_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackreservation_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_ACTIONS = """
|
||||
{% if perms.dcim.change_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
MANUFACTURER_ACTIONS = """
|
||||
<a href="{% url 'dcim:manufacturer_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_manufacturer %}
|
||||
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
MANUFACTURER_ACTIONS = """
|
||||
{% if perms.dcim.change_manufacturer %}
|
||||
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
DEVICEROLE_ACTIONS = """
|
||||
<a href="{% url 'dcim:devicerole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -110,6 +128,9 @@ PLATFORM_VM_COUNT = """
|
||||
"""
|
||||
|
||||
PLATFORM_ACTIONS = """
|
||||
<a href="{% url 'dcim:platform_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_platform %}
|
||||
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
@ -143,6 +164,9 @@ UTILIZATION_GRAPH = """
|
||||
"""
|
||||
|
||||
VIRTUALCHASSIS_ACTIONS = """
|
||||
<a href="{% url 'dcim:virtualchassis_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_virtualchassis %}
|
||||
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
|
@ -2,11 +2,14 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from extras.views import ImageAttachmentEditView
|
||||
from extras.views import ObjectChangeLogView, 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<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
|
||||
url(r'^regions/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.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<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
|
||||
url(r'^sites/(?P<object_id>\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<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||
url(r'^rack-groups/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.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<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
url(r'^rack-roles/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.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<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.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<pk>\d+)/$', views.RackView.as_view(), name='rack'),
|
||||
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
||||
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||
url(r'^racks/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
||||
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
|
||||
url(r'^racks/(?P<object_id>\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<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||
url(r'^manufacturers/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.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<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
||||
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||
url(r'^device-types/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
|
||||
|
||||
# Console port templates
|
||||
url(r'^device-types/(?P<pk>\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<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||
url(r'^device-roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.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<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||
url(r'^platforms/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.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<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
|
||||
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
|
||||
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||
url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
|
||||
url(r'^devices/(?P<pk>\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<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
|
||||
url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
|
||||
url(r'^virtual-chassis/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
|
||||
url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
|
||||
url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
|
||||
|
||||
|
@ -18,7 +18,7 @@ from django.views.generic import View
|
||||
from natsort import natsorted
|
||||
|
||||
from circuits.models import Circuit
|
||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
|
||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from ipam.models import Prefix, Service, VLAN
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
@ -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',
|
||||
})
|
||||
|
||||
|
||||
@ -1104,7 +1108,6 @@ class ConsolePortConnectView(PermissionRequiredMixin, View):
|
||||
escape(consoleport.cs_port.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
@ -1155,7 +1158,6 @@ class ConsolePortDisconnectView(PermissionRequiredMixin, View):
|
||||
escape(cs_port.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
@ -1244,7 +1246,6 @@ class ConsoleServerPortConnectView(PermissionRequiredMixin, View):
|
||||
escape(consoleserverport.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
@ -1296,7 +1297,6 @@ class ConsoleServerPortDisconnectView(PermissionRequiredMixin, View):
|
||||
escape(consoleserverport.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
@ -1390,7 +1390,6 @@ class PowerPortConnectView(PermissionRequiredMixin, View):
|
||||
escape(powerport.power_outlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
@ -1441,7 +1440,6 @@ class PowerPortDisconnectView(PermissionRequiredMixin, View):
|
||||
escape(power_outlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
@ -1529,7 +1527,6 @@ class PowerOutletConnectView(PermissionRequiredMixin, View):
|
||||
escape(poweroutlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
@ -1580,7 +1577,6 @@ class PowerOutletDisconnectView(PermissionRequiredMixin, View):
|
||||
escape(poweroutlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
@ -1910,7 +1906,6 @@ class InterfaceConnectionAddView(PermissionRequiredMixin, GetReturnURLMixin, Vie
|
||||
escape(interfaceconnection.interface_b.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, interfaceconnection, msg)
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
|
||||
@ -1961,7 +1956,6 @@ class InterfaceConnectionDeleteView(PermissionRequiredMixin, GetReturnURLMixin,
|
||||
escape(interfaceconnection.interface_b.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, interfaceconnection, msg)
|
||||
|
||||
return redirect(self.get_return_url(request, interfaceconnection))
|
||||
|
||||
@ -2241,7 +2235,6 @@ class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, Vi
|
||||
membership_form.save()
|
||||
msg = 'Added member <a href="{}">{}</a>'.format(device.get_absolute_url(), escape(device))
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, device, msg)
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.get_full_path())
|
||||
@ -2296,7 +2289,6 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
|
||||
|
||||
msg = 'Removed {} from virtual chassis {}'.format(device, device.virtual_chassis)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_edit(request.user, device, msg)
|
||||
|
||||
return redirect(self.get_return_url(request, device))
|
||||
|
||||
|
@ -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', 'changed_object_type', 'display_object', 'action', 'display_user', 'request_id', 'object_data']
|
||||
list_display = ['time', 'changed_object_type', 'display_object', 'display_action', 'display_user', 'request_id']
|
||||
list_filter = ['time', 'action', 'user__username']
|
||||
list_select_related = ['changed_object_type', 'user']
|
||||
readonly_fields = fields
|
||||
search_fields = ['user_name', 'object_repr', 'request_id']
|
||||
|
||||
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('<span class="{}">{}</span>'.format(icon[obj.action], obj.get_action_display()))
|
||||
display_action.short_description = 'action'
|
||||
|
||||
def display_object(self, obj):
|
||||
if hasattr(obj.changed_object, 'get_absolute_url'):
|
||||
return mark_safe('<a href="{}">{}</a>'.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
|
||||
#
|
||||
|
@ -6,11 +6,12 @@ from taggit.models import Tag
|
||||
|
||||
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
|
||||
from dcim.models import Device, Rack, Site
|
||||
from extras.constants import ACTION_CHOICES, GRAPH_TYPE_CHOICES
|
||||
from extras.models import ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
|
||||
from extras.models import ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, UserAction
|
||||
from extras.constants import *
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from utilities.api import (
|
||||
ChoiceFieldSerializer, ContentTypeFieldSerializer, get_serializer_for_model, ValidatedModelSerializer,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -155,6 +156,35 @@ class ReportDetailSerializer(ReportSerializer):
|
||||
result = ReportResultSerializer()
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
user = NestedUserSerializer(read_only=True)
|
||||
content_type = ContentTypeFieldSerializer(read_only=True)
|
||||
changed_object = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'content_type', 'changed_object', 'object_data',
|
||||
]
|
||||
|
||||
def get_changed_object(self, obj):
|
||||
"""
|
||||
Serialize a nested representation of the changed object.
|
||||
"""
|
||||
if obj.changed_object is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
|
||||
if serializer is None:
|
||||
return obj.object_repr
|
||||
context = {'request': self.context['request']}
|
||||
data = serializer(obj.changed_object, context=context).data
|
||||
return data
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
#
|
||||
|
@ -37,6 +37,9 @@ router.register(r'image-attachments', views.ImageAttachmentViewSet)
|
||||
# Reports
|
||||
router.register(r'reports', views.ReportViewSet, base_name='report')
|
||||
|
||||
# Change logging
|
||||
router.register(r'object-changes', views.ObjectChangeViewSet)
|
||||
|
||||
# Recent activity
|
||||
router.register(r'recent-activity', views.RecentActivityViewSet)
|
||||
|
||||
|
@ -11,7 +11,9 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||
from taggit.models import Tag
|
||||
|
||||
from extras import filters
|
||||
from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
|
||||
from extras.models import (
|
||||
CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, UserAction,
|
||||
)
|
||||
from extras.reports import get_report, get_reports
|
||||
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
||||
from . import serializers
|
||||
@ -206,6 +208,19 @@ class ReportViewSet(ViewSet):
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
Retrieve a list of recent changes.
|
||||
"""
|
||||
queryset = ObjectChange.objects.select_related('user')
|
||||
serializer_class = serializers.ObjectChangeSerializer
|
||||
filter_class = filters.ObjectChangeFilter
|
||||
|
||||
|
||||
#
|
||||
# User activity
|
||||
#
|
||||
|
@ -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
|
||||
|
@ -8,7 +8,7 @@ from taggit.models import Tag
|
||||
|
||||
from dcim.models import Site
|
||||
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
||||
from .models import CustomField, Graph, ExportTemplate, TopologyMap, UserAction
|
||||
from .models import CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
|
||||
|
||||
|
||||
class CustomFieldFilter(django_filters.Filter):
|
||||
@ -124,6 +124,26 @@ class TopologyMapFilter(django_filters.FilterSet):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class ObjectChangeFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = ['user', 'user_name', 'request_id', 'action', 'changed_object_type', 'object_repr']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(user_name__icontains=value) |
|
||||
Q(object_repr__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class UserActionFilter(django_filters.FilterSet):
|
||||
username = django_filters.ModelMultipleChoiceFilter(
|
||||
name='user__username',
|
||||
|
@ -3,12 +3,16 @@ from __future__ import unicode_literals
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from taggit.models import Tag
|
||||
|
||||
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField, SlugField
|
||||
from .constants import CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
|
||||
from .models import CustomField, CustomFieldValue, ImageAttachment
|
||||
from utilities.forms import add_blank_choice, BootstrapMixin, BulkEditForm, LaxURLField, SlugField
|
||||
from .constants import (
|
||||
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||
OBJECTCHANGE_ACTION_CHOICES,
|
||||
)
|
||||
from .models import CustomField, CustomFieldValue, ImageAttachment, ObjectChange
|
||||
|
||||
|
||||
#
|
||||
@ -189,3 +193,38 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = ['name', 'image']
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = ObjectChange
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
# TODO: Change time_0 and time_1 to time_after and time_before for django-filter==2.0
|
||||
time_0 = forms.DateTimeField(
|
||||
label='After',
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
|
||||
)
|
||||
)
|
||||
time_1 = forms.DateTimeField(
|
||||
label='Before',
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
|
||||
)
|
||||
)
|
||||
action = forms.ChoiceField(
|
||||
choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES),
|
||||
required=False
|
||||
)
|
||||
user = forms.ModelChoiceField(
|
||||
queryset=User.objects.order_by('username'),
|
||||
required=False
|
||||
)
|
||||
|
65
netbox/extras/middleware.py
Normal file
65
netbox/extras/middleware.py
Normal file
@ -0,0 +1,65 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import timedelta
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import curry, SimpleLazyObject
|
||||
|
||||
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||
from .models import ObjectChange
|
||||
|
||||
|
||||
def record_object_change(user, request_id, instance, **kwargs):
|
||||
"""
|
||||
Create an ObjectChange in response to an object being created or deleted.
|
||||
"""
|
||||
if not hasattr(instance, 'log_change'):
|
||||
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
|
||||
|
||||
instance.log_change(user, request_id, action)
|
||||
|
||||
# 1% chance of clearing out expired ObjectChanges
|
||||
if settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:
|
||||
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
|
||||
purged_count, _ = ObjectChange.objects.filter(
|
||||
time__lt=cutoff
|
||||
).delete()
|
||||
|
||||
|
||||
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))
|
||||
|
||||
request_id = uuid.uuid4()
|
||||
|
||||
# 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, request_id)
|
||||
|
||||
post_save.connect(_record_object_change, dispatch_uid='record_object_saved')
|
||||
post_delete.connect(_record_object_change, dispatch_uid='record_object_deleted')
|
||||
|
||||
return self.get_response(request)
|
40
netbox/extras/migrations/0013_objectchange.py
Normal file
40
netbox/extras/migrations/0013_objectchange.py
Normal file
@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.12 on 2018-06-22 18:13
|
||||
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 = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('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)),
|
||||
('request_id', models.UUIDField(editable=False)),
|
||||
('action', models.PositiveSmallIntegerField(choices=[(1, 'Created'), (2, 'Updated'), (3, 'Deleted')])),
|
||||
('changed_object_id', models.PositiveIntegerField()),
|
||||
('related_object_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('object_repr', models.CharField(editable=False, max_length=200)),
|
||||
('object_data', django.contrib.postgres.fields.jsonb.JSONField(editable=False)),
|
||||
('changed_object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
|
||||
('related_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', 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'],
|
||||
},
|
||||
),
|
||||
]
|
@ -2,12 +2,14 @@ from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
import json
|
||||
|
||||
import graphviz
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.urls import reverse
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
@ -656,6 +658,119 @@ 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. A change record may optionally
|
||||
indicate an object related to the one being changed. For example, a change to an interface may also indicate the
|
||||
parent device. This will ensure changes made to component models appear in the parent model's changelog.
|
||||
"""
|
||||
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
|
||||
)
|
||||
request_id = models.UUIDField(
|
||||
editable=False
|
||||
)
|
||||
action = models.PositiveSmallIntegerField(
|
||||
choices=OBJECTCHANGE_ACTION_CHOICES
|
||||
)
|
||||
changed_object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+'
|
||||
)
|
||||
changed_object_id = models.PositiveIntegerField()
|
||||
changed_object = GenericForeignKey(
|
||||
ct_field='changed_object_type',
|
||||
fk_field='changed_object_id'
|
||||
)
|
||||
related_object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
related_object_id = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
related_object = GenericForeignKey(
|
||||
ct_field='related_object_type',
|
||||
fk_field='related_object_id'
|
||||
)
|
||||
object_repr = models.CharField(
|
||||
max_length=200,
|
||||
editable=False
|
||||
)
|
||||
object_data = JSONField(
|
||||
editable=False
|
||||
)
|
||||
|
||||
serializer = 'extras.api.serializers.ObjectChangeSerializer'
|
||||
csv_headers = [
|
||||
'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
|
||||
'related_object_type', 'related_object_id', 'object_repr', 'object_data',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['-time']
|
||||
|
||||
def __str__(self):
|
||||
return '{} {} {} by {}'.format(
|
||||
self.changed_object_type,
|
||||
self.object_repr,
|
||||
self.get_action_display().lower(),
|
||||
self.user_name
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Record the user's name and the object's representation as static strings
|
||||
self.user_name = self.user.username
|
||||
self.object_repr = str(self.changed_object)
|
||||
|
||||
return super(ObjectChange, self).save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:objectchange', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.time,
|
||||
self.user,
|
||||
self.user_name,
|
||||
self.request_id,
|
||||
self.get_action_display(),
|
||||
self.changed_object_type,
|
||||
self.changed_object_id,
|
||||
self.related_object_type,
|
||||
self.related_object_id,
|
||||
self.object_repr,
|
||||
self.object_data,
|
||||
)
|
||||
|
||||
@property
|
||||
def object_data_pretty(self):
|
||||
return json.dumps(self.object_data, indent=4, sort_keys=True)
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
#
|
||||
|
@ -4,6 +4,7 @@ import django_tables2 as tables
|
||||
from taggit.models import Tag
|
||||
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import ObjectChange
|
||||
|
||||
TAG_ACTIONS = """
|
||||
{% if perms.taggit.change_tag %}
|
||||
@ -14,6 +15,24 @@ TAG_ACTIONS = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
OBJECTCHANGE_ACTION = """
|
||||
{% if record.action == 1 %}
|
||||
<span class="label label-success">Created</span>
|
||||
{% elif record.action == 2 %}
|
||||
<span class="label label-primary">Updated</span>
|
||||
{% elif record.action == 3 %}
|
||||
<span class="label label-danger">Deleted</span>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
OBJECTCHANGE_OBJECT = """
|
||||
{% if record.action != 3 and record.changed_object.get_absolute_url %}
|
||||
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
|
||||
{% else %}
|
||||
{{ record.object_repr }}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
class TagTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
@ -26,3 +45,24 @@ class TagTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Tag
|
||||
fields = ('pk', 'name', 'items')
|
||||
|
||||
|
||||
class ObjectChangeTable(BaseTable):
|
||||
time = tables.LinkColumn()
|
||||
action = tables.TemplateColumn(
|
||||
template_code=OBJECTCHANGE_ACTION
|
||||
)
|
||||
changed_object_type = tables.Column(
|
||||
verbose_name='Type'
|
||||
)
|
||||
object_repr = tables.TemplateColumn(
|
||||
template_code=OBJECTCHANGE_OBJECT,
|
||||
verbose_name='Object'
|
||||
)
|
||||
request_id = tables.Column(
|
||||
verbose_name='Request ID'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ObjectChange
|
||||
fields = ('time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')
|
||||
|
@ -22,4 +22,8 @@ urlpatterns = [
|
||||
url(r'^reports/(?P<name>[^/]+\.[^/]+)/$', views.ReportView.as_view(), name='report'),
|
||||
url(r'^reports/(?P<name>[^/]+\.[^/]+)/run/$', views.ReportRunView.as_view(), name='report_run'),
|
||||
|
||||
# Change logging
|
||||
url(r'^changelog/$', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
url(r'^changelog/(?P<pk>\d+)/$', views.ObjectChangeView.as_view(), name='objectchange'),
|
||||
|
||||
]
|
||||
|
@ -1,8 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import template
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models import Count
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Count, Q
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect, render, reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
@ -11,10 +13,11 @@ 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 . import filters
|
||||
from .forms import ObjectChangeFilterForm, ImageAttachmentForm, TagForm
|
||||
from .models import ImageAttachment, ObjectChange, ReportResult
|
||||
from .reports import get_report, get_reports
|
||||
from .tables import TagTable
|
||||
from .tables import ObjectChangeTable, TagTable
|
||||
|
||||
|
||||
#
|
||||
@ -50,6 +53,77 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
class ObjectChangeListView(ObjectListView):
|
||||
queryset = ObjectChange.objects.select_related('user', 'changed_object_type')
|
||||
filter = filters.ObjectChangeFilter
|
||||
filter_form = ObjectChangeFilterForm
|
||||
table = ObjectChangeTable
|
||||
template_name = 'extras/objectchange_list.html'
|
||||
|
||||
|
||||
class ObjectChangeView(View):
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
objectchange = get_object_or_404(ObjectChange, pk=pk)
|
||||
|
||||
related_changes = ObjectChange.objects.filter(request_id=objectchange.request_id).exclude(pk=objectchange.pk)
|
||||
related_changes_table = ObjectChangeTable(
|
||||
data=related_changes[:50],
|
||||
orderable=False
|
||||
)
|
||||
|
||||
return render(request, 'extras/objectchange.html', {
|
||||
'objectchange': objectchange,
|
||||
'related_changes_table': related_changes_table,
|
||||
'related_changes_count': related_changes.count()
|
||||
})
|
||||
|
||||
|
||||
class ObjectChangeLogView(View):
|
||||
"""
|
||||
Present a history of changes made to a particular 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 (and its related objects)
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
objectchanges = ObjectChange.objects.select_related(
|
||||
'user', 'changed_object_type'
|
||||
).filter(
|
||||
Q(changed_object_type=content_type, changed_object_id=obj.pk) |
|
||||
Q(related_object_type=content_type, related_object_id=obj.pk)
|
||||
)
|
||||
objectchanges_table = ObjectChangeTable(
|
||||
data=objectchanges,
|
||||
orderable=False
|
||||
)
|
||||
|
||||
# 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/object_changelog.html', {
|
||||
object_var: obj,
|
||||
'objectchanges_table': objectchanges_table,
|
||||
'base_template': base_template,
|
||||
'active_tab': 'changelog',
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# Image attachments
|
||||
#
|
||||
@ -149,6 +223,5 @@ class ReportRunView(PermissionRequiredMixin, View):
|
||||
result = 'failed' if report.failed else 'passed'
|
||||
msg = "Ran report {} ({})".format(report.full_name, result)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_create(request.user, report.result, msg)
|
||||
|
||||
return redirect('extras:report', name=report.full_name)
|
||||
|
105
netbox/ipam/migrations/0023_change_logging.py
Normal file
105
netbox/ipam/migrations/0023_change_logging.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
@ -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, CustomFieldModel):
|
||||
class Service(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
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.
|
||||
|
@ -28,6 +28,9 @@ RIR_UTILIZATION = """
|
||||
"""
|
||||
|
||||
RIR_ACTIONS = """
|
||||
<a href="{% url 'ipam:rir_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_rir %}
|
||||
<a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
@ -47,6 +50,9 @@ ROLE_VLAN_COUNT = """
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
<a href="{% url 'ipam:role_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_role %}
|
||||
<a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
@ -127,6 +133,9 @@ VLAN_ROLE_LINK = """
|
||||
"""
|
||||
|
||||
VLANGROUP_ACTIONS = """
|
||||
<a href="{% url 'ipam:vlangroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% with next_vid=record.get_next_available_vid %}
|
||||
{% if next_vid and perms.ipam.add_vlan %}
|
||||
<a href="{% url 'ipam:vlan_add' %}?site={{ record.site_id }}&group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
|
||||
|
@ -2,7 +2,9 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from extras.views import ObjectChangeLogView
|
||||
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<pk>\d+)/$', views.VRFView.as_view(), name='vrf'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.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<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
|
||||
url(r'^vrfs/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.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<pk>\d+)/$', views.AggregateView.as_view(), name='aggregate'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.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<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
|
||||
url(r'^roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.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<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}),
|
||||
url(r'^prefixes/(?P<pk>\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
|
||||
url(r'^prefixes/(?P<pk>\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<pk>\d+)/changelog/$', ObjectChangeLogView.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<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
|
||||
url(r'^ip-addresses/(?P<pk>\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<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||
url(r'^vlan-groups/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
|
||||
|
||||
# VLANs
|
||||
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
|
||||
@ -83,6 +92,7 @@ urlpatterns = [
|
||||
url(r'^vlans/(?P<pk>\d+)/members/$', views.VLANMembersView.as_view(), name='vlan_members'),
|
||||
url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
|
||||
url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),
|
||||
url(r'^vlans/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
|
||||
|
||||
# Services
|
||||
url(r'^services/$', views.ServiceListView.as_view(), name='service_list'),
|
||||
@ -91,5 +101,6 @@ urlpatterns = [
|
||||
url(r'^services/(?P<pk>\d+)/$', views.ServiceView.as_view(), name='service'),
|
||||
url(r'^services/(?P<pk>\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'),
|
||||
url(r'^services/(?P<pk>\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'),
|
||||
url(r'^services/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
|
||||
|
||||
]
|
||||
|
@ -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',
|
||||
})
|
||||
|
||||
|
||||
|
@ -50,6 +50,9 @@ BANNER_LOGIN = ''
|
||||
# BASE_PATH = 'netbox/'
|
||||
BASE_PATH = ''
|
||||
|
||||
# Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90)
|
||||
CHANGELOG_RETENTION = 90
|
||||
|
||||
# API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be
|
||||
# allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or
|
||||
# CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers
|
||||
|
@ -44,6 +44,7 @@ BANNER_TOP = getattr(configuration, 'BANNER_TOP', '')
|
||||
BASE_PATH = getattr(configuration, 'BASE_PATH', '')
|
||||
if BASE_PATH:
|
||||
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
|
||||
CHANGELOG_RETENTION = getattr(configuration, 'CHANGELOG_RETENTION', 90)
|
||||
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
|
||||
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
|
||||
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
|
||||
@ -174,6 +175,7 @@ MIDDLEWARE = (
|
||||
'utilities.middleware.ExceptionHandlingMiddleware',
|
||||
'utilities.middleware.LoginRequiredMiddleware',
|
||||
'utilities.middleware.APIVersionMiddleware',
|
||||
'extras.middleware.ChangeLoggingMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'netbox.urls'
|
||||
|
35
netbox/secrets/migrations/0005_change_logging.py
Normal file
35
netbox/secrets/migrations/0005_change_logging.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
@ -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:
|
||||
|
@ -6,6 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import SecretRole, Secret
|
||||
|
||||
SECRETROLE_ACTIONS = """
|
||||
<a href="{% url 'secrets:secretrole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.secrets.change_secretrole %}
|
||||
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
|
@ -2,7 +2,9 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from extras.views import ObjectChangeLogView
|
||||
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<slug>[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
|
||||
url(r'^secret-roles/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.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<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
|
||||
url(r'^secrets/(?P<pk>\d+)/edit/$', views.secret_edit, name='secret_edit'),
|
||||
url(r'^secrets/(?P<pk>\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'),
|
||||
url(r'^secrets/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}),
|
||||
|
||||
]
|
||||
|
@ -34,6 +34,7 @@
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% block header %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
<div class="push"></div>
|
||||
{% if settings.BANNER_BOTTOM %}
|
||||
|
@ -1,8 +1,10 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% block title %}{{ circuit }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
|
||||
@ -22,8 +24,8 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.circuits.change_circuit %}
|
||||
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
@ -36,9 +38,20 @@
|
||||
Delete this circuit
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=circuit %}
|
||||
</div>
|
||||
<h1>{{ circuit }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=circuit %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ circuit.get_absolute_url }}">Circuit</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'circuits:circuit_changelog' pk=circuit.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
|
@ -2,8 +2,10 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% block title %}{{ provider }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
|
||||
@ -22,8 +24,8 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if show_graphs %}
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider-graphs' pk=provider.pk %}" title="Show graphs">
|
||||
<i class="fa fa-signal" aria-hidden="true"></i>
|
||||
@ -42,9 +44,20 @@
|
||||
Delete this provider
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ provider }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=provider %}
|
||||
</div>
|
||||
<h1>{{ provider }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=provider %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ provider.get_absolute_url }}">Provider</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'circuits:provider_changelog' slug=provider.slug %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
|
@ -4,9 +4,79 @@
|
||||
|
||||
{% block title %}{{ device }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a></li>
|
||||
{% if device.rack %}
|
||||
<li><a href="{% url 'dcim:rack_list' %}?site={{ device.site.slug }}">Racks</a></li>
|
||||
<li><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a></li>
|
||||
{% endif %}
|
||||
{% if device.parent_bay %}
|
||||
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
|
||||
<li>{{ device.parent_bay }}</li>
|
||||
{% endif %}
|
||||
<li>{{ device }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search devices" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.change_device %}
|
||||
<a href="{% url 'dcim:device_edit' pk=device.pk %}" class="btn btn-warning">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||
Edit this device
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_device %}
|
||||
<a href="{% url 'dcim:device_delete' pk=device.pk %}" class="btn btn-danger">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
||||
Delete this device
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ device }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=device %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">Device</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a>
|
||||
</li>
|
||||
{% if perms.dcim.napalm_read %}
|
||||
{% if device.status != 1 %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='Device must be in active status' %}
|
||||
{% elif not device.platform %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No platform assigned to this device' %}
|
||||
{% elif not device.platform.napalm_driver %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No NAPALM driver assigned for this platform' %}
|
||||
{% elif not device.primary_ip %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No primary IP address assigned to this device' %}
|
||||
{% else %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device_changelog' pk=device.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'dcim/inc/device_header.html' with active_tab='info' %}
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
@ -378,8 +448,8 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% if device_bays or device.device_type.is_parent_device %}
|
||||
{% if perms.dcim.delete_devicebay %}
|
||||
@ -626,7 +696,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/graphs_modal.html' %}
|
||||
{% include 'secrets/inc/private_key_modal.html' %}
|
||||
{% endblock %}
|
||||
|
@ -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' %}
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="panel panel-default">
|
||||
|
@ -1,10 +1,9 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'dcim/device.html' %}
|
||||
|
||||
{% block title %}{{ device }} - Inventory{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'dcim/inc/device_header.html' with active_tab='inventory' %}
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
@ -73,5 +72,5 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -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' %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>LLDP Neighbors</strong>
|
||||
|
@ -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' %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
|
@ -1,8 +1,10 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:devicetype_list' %}">Device Types</a></li>
|
||||
@ -10,8 +12,8 @@
|
||||
<li>{{ devicetype.model }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.change_devicetype or perms.dcim.delete_devicetype %}
|
||||
</div>
|
||||
{% if perms.dcim.change_devicetype or perms.dcim.delete_devicetype %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.change_devicetype %}
|
||||
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
|
||||
@ -26,9 +28,20 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<h1>{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=devicetype %}
|
||||
{% endif %}
|
||||
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=devicetype %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ devicetype.get_absolute_url }}">Device Type</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:devicetype_changelog' pk=devicetype.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
|
@ -1,65 +0,0 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a></li>
|
||||
{% if device.rack %}
|
||||
<li><a href="{% url 'dcim:rack_list' %}?site={{ device.site.slug }}">Racks</a></li>
|
||||
<li><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a></li>
|
||||
{% endif %}
|
||||
{% if device.parent_bay %}
|
||||
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
|
||||
<li>{{ device.parent_bay }}</li>
|
||||
{% endif %}
|
||||
<li>{{ device }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search devices" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.change_device %}
|
||||
<a href="{% url 'dcim:device_edit' pk=device.pk %}" class="btn btn-warning">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||
Edit this device
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_device %}
|
||||
<a href="{% url 'dcim:device_delete' pk=device.pk %}" class="btn btn-danger">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
||||
Delete this device
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ device }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=device %}
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if active_tab == 'info' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">Info</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a>
|
||||
</li>
|
||||
{% if perms.dcim.napalm_read %}
|
||||
{% if device.status != 1 %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='Device must be in active status' %}
|
||||
{% elif not device.platform %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No platform assigned to this device' %}
|
||||
{% elif not device.platform.napalm_driver %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No NAPALM driver assigned for this platform' %}
|
||||
{% elif not device.primary_ip %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No primary IP address assigned to this device' %}
|
||||
{% else %}
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
@ -1,8 +1,8 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
|
||||
@ -22,8 +22,8 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
|
||||
<span class="fa fa-chevron-left" aria-hidden="true"></span> Previous Rack
|
||||
</a>
|
||||
@ -40,9 +40,20 @@
|
||||
<span class="fa fa-trash" aria-hidden="true"></span> Delete this rack
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}Rack {{ rack }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=rack %}
|
||||
</div>
|
||||
<h1>{% block title %}Rack {{ rack }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=rack %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ rack.get_absolute_url }}">Rack</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:rack_changelog' pk=rack.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
|
@ -3,10 +3,11 @@
|
||||
{% load tz %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
|
||||
{% if site.region %}
|
||||
{% for region in site.region.get_ancestors %}
|
||||
<li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
|
||||
@ -28,8 +29,8 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if show_graphs %}
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site-graphs' pk=site.pk %}" title="Show graphs">
|
||||
<i class="fa fa-signal" aria-hidden="true"></i>
|
||||
@ -48,9 +49,20 @@
|
||||
Delete this site
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ site }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=site %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ site }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=site %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ site.get_absolute_url }}">Site</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:site_changelog' slug=site.slug %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="panel panel-default">
|
||||
|
8
netbox/templates/extras/object_changelog.html
Normal file
8
netbox/templates/extras/object_changelog.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends base_template %}
|
||||
|
||||
{% block title %}{% if obj %}{{ obj }}{% else %}{{ block.super }}{% endif %} - Changelog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if obj %}<h1>{{ obj }}</h1>{% endif %}
|
||||
{% include 'panel_table.html' with table=objectchanges_table %}
|
||||
{% endblock %}
|
101
netbox/templates/extras/objectchange.html
Normal file
101
netbox/templates/extras/objectchange.html
Normal file
@ -0,0 +1,101 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}{{ objectchange }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'extras:objectchange_list' %}">Changelog</a></li>
|
||||
<li>{{ objectchange }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'extras:objectchange_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Change</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Time</td>
|
||||
<td>
|
||||
{{ objectchange.time }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>User</td>
|
||||
<td>
|
||||
{{ objectchange.user|default:objectchange.user_name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Action</td>
|
||||
<td>
|
||||
{{ objectchange.get_action_display }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Object Type</td>
|
||||
<td>
|
||||
{{ objectchange.changed_object_type }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Object</td>
|
||||
<td>
|
||||
{% if objectchange.changed_object.get_absolute_url %}
|
||||
<a href="{{ objectchange.changed_object.get_absolute_url }}">{{ objectchange.changed_object }}</a>
|
||||
{% else %}
|
||||
{{ objectchange.object_repr }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Request ID</td>
|
||||
<td>
|
||||
{{ objectchange.request_id }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Object Data</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<pre>{{ objectchange.object_data_pretty }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'panel_table.html' with table=related_changes_table heading='Related Changes' %}
|
||||
{% if related_changes_count > related_changes_table.rows|length %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'extras:objectchange_list' %}?request_id={{ objectchange.request_id }}" class="btn btn-primary">See all {{ related_changes_count|add:"1" }} changes</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
17
netbox/templates/extras/objectchange_list.html
Normal file
17
netbox/templates/extras/objectchange_list.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Changelog{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -66,6 +66,9 @@
|
||||
<li>
|
||||
<a href="{% url 'extras:report_list' %}">Reports</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'extras:objectchange_list' %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/rack' %} active{% endif %}">
|
||||
|
@ -1,8 +1,8 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></li>
|
||||
@ -22,8 +22,8 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.change_aggregate %}
|
||||
<a href="{% url 'ipam:aggregate_edit' pk=aggregate.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
@ -36,9 +36,20 @@
|
||||
Delete this aggregate
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ aggregate }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=aggregate %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ aggregate }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=aggregate %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ aggregate.get_absolute_url }}">Aggregate</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:aggregate_changelog' pk=aggregate.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
|
@ -1,55 +0,0 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
|
||||
{% if prefix.vrf %}
|
||||
<li><a href="{% url 'ipam:vrf' pk=prefix.vrf.pk %}">{{ prefix.vrf }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ prefix }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'ipam:prefix_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search prefixes" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %}
|
||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ prefix.vrf.pk }}&site={{ prefix.site.pk }}&tenant_group={{ prefix.tenant.group.pk }}&tenant={{ prefix.tenant.pk }}" class="btn btn-success">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> Add Child Prefix
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}&vrf={{ prefix.vrf.pk }}&tenant_group={{ prefix.tenant.group.pk }}&tenant={{ prefix.tenant.pk }}" class="btn btn-success">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add an IP Address
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.change_prefix %}
|
||||
<a href="{% url 'ipam:prefix_edit' pk=prefix.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
Edit this prefix
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_prefix %}
|
||||
<a href="{% url 'ipam:prefix_delete' pk=prefix.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
Delete this prefix
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ prefix }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=prefix %}
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if active_tab == 'prefix' %} class="active"{% endif %}><a href="{% url 'ipam:prefix' pk=prefix.pk %}">Prefix</a></li>
|
||||
<li role="presentation"{% if active_tab == 'prefixes' %} class="active"{% endif %}><a href="{% url 'ipam:prefix_prefixes' pk=prefix.pk %}">Child Prefixes <span class="badge">{{ prefix.get_child_prefixes.count }}</span></a></li>
|
||||
<li role="presentation"{% if active_tab == 'ip-addresses' %} class="active"{% endif %}><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">IP Addresses <span class="badge">{{ prefix.get_child_ips.count }}</span></a></li>
|
||||
</ul>
|
@ -14,6 +14,9 @@
|
||||
</td>
|
||||
<td>{{ service.description }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'ipam:service_changelog' pk=service.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_service %}
|
||||
<a href="{% url 'ipam:service_edit' pk=service.pk %}" class="btn btn-info btn-xs" title="Edit service">
|
||||
<i class="glyphicon glyphicon-pencil"></i>
|
||||
|
@ -1,46 +0,0 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
|
||||
{% if vlan.site %}
|
||||
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
|
||||
{% endif %}
|
||||
{% if vlan.group %}
|
||||
<li><a href="{% url 'ipam:vlan_list' %}?group={{ vlan.group.slug }}">{{ vlan.group }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ vlan }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'ipam:vlan_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search VLANs" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.change_vlan %}
|
||||
<a href="{% url 'ipam:vlan_edit' pk=vlan.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
Edit this VLAN
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_vlan %}
|
||||
<a href="{% url 'ipam:vlan_delete' pk=vlan.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
Delete this VLAN
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}VLAN {{ vlan.display_name }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=vlan %}
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if active_tab == 'vlan' %} class="active"{% endif %}><a href="{% url 'ipam:vlan' pk=vlan.pk %}">VLAN</a></li>
|
||||
<li role="presentation"{% if active_tab == 'members' %} class="active"{% endif %}><a href="{% url 'ipam:vlan_members' pk=vlan.pk %}">Members <span class="badge">{{ vlan.get_members.count }}</span></a></li>
|
||||
</ul>
|
@ -1,8 +1,8 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
|
||||
@ -24,8 +24,8 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=ipaddress.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
@ -38,9 +38,20 @@
|
||||
Delete this IP
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ ipaddress }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=ipaddress %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ ipaddress }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=ipaddress %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ ipaddress.get_absolute_url }}">IP Address</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:ipaddress_changelog' pk=ipaddress.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
|
@ -1,11 +1,75 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}{{ prefix }}{% endblock %}
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
|
||||
{% if prefix.vrf %}
|
||||
<li><a href="{% url 'ipam:vrf' pk=prefix.vrf.pk %}">{{ prefix.vrf }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ prefix }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'ipam:prefix_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search prefixes" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %}
|
||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ prefix.vrf.pk }}&site={{ prefix.site.pk }}&tenant_group={{ prefix.tenant.group.pk }}&tenant={{ prefix.tenant.pk }}" class="btn btn-success">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> Add Child Prefix
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}&vrf={{ prefix.vrf.pk }}&tenant_group={{ prefix.tenant.group.pk }}&tenant={{ prefix.tenant.pk }}" class="btn btn-success">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add an IP Address
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.change_prefix %}
|
||||
<a href="{% url 'ipam:prefix_edit' pk=prefix.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
Edit this prefix
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_prefix %}
|
||||
<a href="{% url 'ipam:prefix_delete' pk=prefix.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
Delete this prefix
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ prefix }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=prefix %}
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">Prefix</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'prefixes' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:prefix_prefixes' pk=prefix.pk %}">Child Prefixes <span class="badge">{{ prefix.get_child_prefixes.count }}</span></a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'ip-addresses' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">IP Addresses <span class="badge">{{ prefix.get_child_ips.count }}</span></a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:prefix_changelog' pk=prefix.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'ipam/inc/prefix_header.html' with active_tab='prefix' %}
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
@ -148,5 +212,5 @@
|
||||
{% endif %}
|
||||
{% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' panel_class='default' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -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' %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% 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' %}
|
||||
|
@ -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' %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% 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 %}
|
||||
|
@ -1,9 +1,64 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
|
||||
{% if vlan.site %}
|
||||
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
|
||||
{% endif %}
|
||||
{% if vlan.group %}
|
||||
<li><a href="{% url 'ipam:vlan_list' %}?group={{ vlan.group.slug }}">{{ vlan.group }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ vlan }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'ipam:vlan_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search VLANs" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.change_vlan %}
|
||||
<a href="{% url 'ipam:vlan_edit' pk=vlan.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
Edit this VLAN
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_vlan %}
|
||||
<a href="{% url 'ipam:vlan_delete' pk=vlan.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
Delete this VLAN
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}VLAN {{ vlan.display_name }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=vlan %}
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:vlan' pk=vlan.pk %}">VLAN</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'members' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:vlan_members' pk=vlan.pk %}">Members <span class="badge">{{ vlan.get_members.count }}</span></a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:vlan_changelog' pk=vlan.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'ipam/inc/vlan_header.html' with active_tab='vlan' %}
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
@ -114,5 +169,5 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -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' %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with table=members_table table_template='panel_table.html' heading='VLAN Members' parent=vlan %}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:vrf_list' %}">VRFs</a></li>
|
||||
@ -21,8 +21,8 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.change_vrf %}
|
||||
<a href="{% url 'ipam:vrf_edit' pk=vrf.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
@ -35,9 +35,20 @@
|
||||
Delete this VRF
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}VRF {{ vrf }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=vrf %}
|
||||
</div>
|
||||
<h1>{% block title %}VRF {{ vrf }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=vrf %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ aggregate.get_absolute_url }}">VRF</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:vrf_changelog' pk=vrf.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
|
@ -3,8 +3,8 @@
|
||||
{% load helpers %}
|
||||
{% load secret_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
|
||||
@ -12,8 +12,8 @@
|
||||
<li>{{ secret.device }}{% if secret.name %} ({{ secret.name }}){% endif %}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.secrets.change_secret %}
|
||||
<a href="{% url 'secrets:secret_edit' pk=secret.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
@ -26,9 +26,20 @@
|
||||
Delete this secret
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ secret }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=secret %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ secret }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=secret %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ secret.get_absolute_url }}">Secret</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'secrets:secret_changelog' pk=secret.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
|
@ -1,8 +1,8 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></li>
|
||||
@ -24,8 +24,8 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.tenancy.change_tenant %}
|
||||
<a href="{% url 'tenancy:tenant_edit' slug=tenant.slug %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
@ -38,9 +38,20 @@
|
||||
Delete this tenant
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ tenant }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=tenant %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ tenant }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=tenant %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ tenant.get_absolute_url }}">Tenant</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'tenancy:tenant_changelog' slug=tenant.slug %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="panel panel-default">
|
||||
|
@ -1,8 +1,8 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row" xmlns="http://www.w3.org/1999/html">
|
||||
{% block header %}
|
||||
<div class="row" xmlns="http://www.w3.org/1999/html">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ cluster.type.get_absolute_url }}">{{ cluster.type }}</a></li>
|
||||
@ -24,8 +24,8 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.virtualization.change_cluster %}
|
||||
<a href="{% url 'virtualization:cluster_edit' pk=cluster.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
@ -38,9 +38,20 @@
|
||||
Delete this cluster
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ cluster }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=cluster %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ cluster }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=cluster %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ cluster.get_absolute_url }}">Cluster</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'virtualization:cluster_changelog' pk=cluster.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
|
@ -1,14 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
{% if vm.cluster %}
|
||||
<li><a href="{{ vm.cluster.get_absolute_url }}">{{ vm.cluster }}</a></li>
|
||||
{% if virtualmachine.cluster %}
|
||||
<li><a href="{{ virtualmachine.cluster.get_absolute_url }}">{{ virtualmachine.cluster }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ vm }}</li>
|
||||
<li>{{ virtualmachine }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
@ -23,23 +23,34 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.virtualization.change_virtualmachine %}
|
||||
<a href="{% url 'virtualization:virtualmachine_edit' pk=vm.pk %}" class="btn btn-warning">
|
||||
<a href="{% url 'virtualization:virtualmachine_edit' pk=virtualmachine.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil"></span>
|
||||
Edit this VM
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.delete_virtualmachine %}
|
||||
<a href="{% url 'virtualization:virtualmachine_delete' pk=vm.pk %}" class="btn btn-danger">
|
||||
<a href="{% url 'virtualization:virtualmachine_delete' pk=virtualmachine.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash"></span>
|
||||
Delete this VM
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ vm }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=vm %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ virtualmachine }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=virtualmachine %}
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ virtualmachine.get_absolute_url }}">Virtual Machine</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'virtualization:virtualmachine_changelog' pk=virtualmachine.pk %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
@ -49,19 +60,19 @@
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ vm }}</td>
|
||||
<td>{{ virtualmachine }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
<span class="label label-{{ vm.get_status_class }}">{{ vm.get_status_display }}</span>
|
||||
<span class="label label-{{ virtualmachine.get_status_class }}">{{ virtualmachine.get_status_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>
|
||||
{% if vm.role %}
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?role={{ vm.role.slug }}">{{ vm.role }}</a>
|
||||
{% if virtualmachine.role %}
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?role={{ virtualmachine.role.slug }}">{{ virtualmachine.role }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
@ -70,8 +81,8 @@
|
||||
<tr>
|
||||
<td>Platform</td>
|
||||
<td>
|
||||
{% if vm.platform %}
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ vm.platform.slug }}">{{ vm.platform }}</a>
|
||||
{% if virtualmachine.platform %}
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ virtualmachine.platform.slug }}">{{ virtualmachine.platform }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
@ -80,12 +91,12 @@
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
{% if vm.tenant %}
|
||||
{% if vm.tenant.group %}
|
||||
<a href="{{ vm.tenant.group.get_absolute_url }}">{{ vm.tenant.group }}</a>
|
||||
{% if virtualmachine.tenant %}
|
||||
{% if virtualmachine.tenant.group %}
|
||||
<a href="{{ virtualmachine.tenant.group.get_absolute_url }}">{{ virtualmachine.tenant.group }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ vm.tenant.get_absolute_url }}">{{ vm.tenant }}</a>
|
||||
<a href="{{ virtualmachine.tenant.get_absolute_url }}">{{ virtualmachine.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
@ -94,12 +105,12 @@
|
||||
<tr>
|
||||
<td>Primary IPv4</td>
|
||||
<td>
|
||||
{% if vm.primary_ip4 %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=vm.primary_ip4.pk %}">{{ vm.primary_ip4.address.ip }}</a>
|
||||
{% if vm.primary_ip4.nat_inside %}
|
||||
<span>(NAT for {{ vm.primary_ip4.nat_inside.address.ip }})</span>
|
||||
{% elif vm.primary_ip4.nat_outside %}
|
||||
<span>(NAT: {{ vm.primary_ip4.nat_outside.address.ip }})</span>
|
||||
{% if virtualmachine.primary_ip4 %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=virtualmachine.primary_ip4.pk %}">{{ virtualmachine.primary_ip4.address.ip }}</a>
|
||||
{% if virtualmachine.primary_ip4.nat_inside %}
|
||||
<span>(NAT for {{ virtualmachine.primary_ip4.nat_inside.address.ip }})</span>
|
||||
{% elif virtualmachine.primary_ip4.nat_outside %}
|
||||
<span>(NAT: {{ virtualmachine.primary_ip4.nat_outside.address.ip }})</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
@ -109,12 +120,12 @@
|
||||
<tr>
|
||||
<td>Primary IPv6</td>
|
||||
<td>
|
||||
{% if vm.primary_ip6 %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=vm.primary_ip6.pk %}">{{ vm.primary_ip6.address.ip }}</a>
|
||||
{% if vm.primary_ip6.nat_inside %}
|
||||
<span>(NAT for {{ vm.primary_ip6.nat_inside.address.ip }})</span>
|
||||
{% elif vm.primary_ip6.nat_outside %}
|
||||
<span>(NAT: {{ vm.primary_ip6.nat_outside.address.ip }})</span>
|
||||
{% if virtualmachine.primary_ip6 %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=virtualmachine.primary_ip6.pk %}">{{ virtualmachine.primary_ip6.address.ip }}</a>
|
||||
{% if virtualmachine.primary_ip6.nat_inside %}
|
||||
<span>(NAT for {{ virtualmachine.primary_ip6.nat_inside.address.ip }})</span>
|
||||
{% elif virtualmachine.primary_ip6.nat_outside %}
|
||||
<span>(NAT: {{ virtualmachine.primary_ip6.nat_outside.address.ip }})</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
@ -124,7 +135,7 @@
|
||||
<tr>
|
||||
<td>Tags</td>
|
||||
<td>
|
||||
{% for tag in vm.tags.all %}
|
||||
{% for tag in virtualmachine.tags.all %}
|
||||
{% tag 'virtualization:virtualmachine_list' tag %}
|
||||
{% empty %}
|
||||
<span class="text-muted">N/A</span>
|
||||
@ -133,14 +144,14 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' with custom_fields=virtualmachine.get_custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if vm.comments %}
|
||||
{{ vm.comments|gfm }}
|
||||
{% if virtualmachine.comments %}
|
||||
{{ virtualmachine.comments|gfm }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
@ -156,16 +167,16 @@
|
||||
<tr>
|
||||
<td>Cluster</td>
|
||||
<td>
|
||||
{% if vm.cluster.group %}
|
||||
<a href="{{ vm.cluster.group.get_absolute_url }}">{{ vm.cluster.group }}</a>
|
||||
{% if virtualmachine.cluster.group %}
|
||||
<a href="{{ virtualmachine.cluster.group.get_absolute_url }}">{{ virtualmachine.cluster.group }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ vm.cluster.get_absolute_url }}">{{ vm.cluster }}</a>
|
||||
<a href="{{ virtualmachine.cluster.get_absolute_url }}">{{ virtualmachine.cluster }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cluster Type</td>
|
||||
<td>{{ vm.cluster.type }}</td>
|
||||
<td>{{ virtualmachine.cluster.type }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@ -177,8 +188,8 @@
|
||||
<tr>
|
||||
<td><i class="fa fa-tachometer"></i> Virtual CPUs</td>
|
||||
<td>
|
||||
{% if vm.vcpus %}
|
||||
{{ vm.vcpus }}
|
||||
{% if virtualmachine.vcpus %}
|
||||
{{ virtualmachine.vcpus }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
@ -187,8 +198,8 @@
|
||||
<tr>
|
||||
<td><i class="fa fa-microchip"></i> Memory</td>
|
||||
<td>
|
||||
{% if vm.memory %}
|
||||
{{ vm.memory }} MB
|
||||
{% if virtualmachine.memory %}
|
||||
{{ virtualmachine.memory }} MB
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
@ -197,8 +208,8 @@
|
||||
<tr>
|
||||
<td><i class="fa fa-hdd-o"></i> Disk Space</td>
|
||||
<td>
|
||||
{% if vm.disk %}
|
||||
{{ vm.disk }} GB
|
||||
{% if virtualmachine.disk %}
|
||||
{{ virtualmachine.disk }} GB
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
@ -223,7 +234,7 @@
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_service %}
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'virtualization:virtualmachine_service_assign' virtualmachine=vm.pk %}" class="btn btn-xs btn-primary">
|
||||
<a href="{% url 'virtualization:virtualmachine_service_assign' virtualmachine=virtualmachine.pk %}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
|
||||
</a>
|
||||
</div>
|
||||
@ -236,7 +247,7 @@
|
||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="virtual_machine" value="{{ vm.pk }}" />
|
||||
<input type="hidden" name="virtual_machine" value="{{ virtualmachine.pk }}" />
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
@ -264,7 +275,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for iface in interfaces %}
|
||||
{% include 'dcim/inc/interface.html' with device=vm %}
|
||||
{% include 'dcim/inc/interface.html' with device=virtualmachine %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted">— No interfaces defined —</td>
|
||||
@ -275,21 +286,21 @@
|
||||
{% if perms.dcim.add_interface or perms.dcim.delete_interface %}
|
||||
<div class="panel-footer">
|
||||
{% if interfaces and perms.dcim.change_interface %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ vm.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=vm.pk %}" class="btn btn-warning btn-xs">
|
||||
<button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=virtualmachine.pk %}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if interfaces and perms.dcim.delete_interface %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=vm.pk %}" class="btn btn-danger btn-xs">
|
||||
<button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=virtualmachine.pk %}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'virtualization:interface_add' pk=vm.pk %}" class="btn btn-primary btn-xs">
|
||||
<a href="{% url 'virtualization:interface_add' pk=virtualmachine.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
||||
</a>
|
||||
</div>
|
||||
|
35
netbox/tenancy/migrations/0005_change_logging.py
Normal file
35
netbox/tenancy/migrations/0005_change_logging.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
@ -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']
|
||||
|
@ -6,6 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
TENANTGROUP_ACTIONS = """
|
||||
<a href="{% url 'tenancy:tenantgroup_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.tenancy.change_tenantgroup %}
|
||||
<a href="{% url 'tenancy:tenantgroup_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
|
@ -2,7 +2,9 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from extras.views import ObjectChangeLogView
|
||||
from . import views
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
app_name = 'tenancy'
|
||||
urlpatterns = [
|
||||
@ -13,6 +15,7 @@ urlpatterns = [
|
||||
url(r'^tenant-groups/import/$', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'),
|
||||
url(r'^tenant-groups/delete/$', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'),
|
||||
url(r'^tenant-groups/(?P<slug>[\w-]+)/edit/$', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'),
|
||||
url(r'^tenant-groups/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}),
|
||||
|
||||
# Tenants
|
||||
url(r'^tenants/$', views.TenantListView.as_view(), name='tenant_list'),
|
||||
@ -23,5 +26,6 @@ urlpatterns = [
|
||||
url(r'^tenants/(?P<slug>[\w-]+)/$', views.TenantView.as_view(), name='tenant'),
|
||||
url(r'^tenants/(?P<slug>[\w-]+)/edit/$', views.TenantEditView.as_view(), name='tenant_edit'),
|
||||
url(r'^tenants/(?P<slug>[\w-]+)/delete/$', views.TenantDeleteView.as_view(), name='tenant_delete'),
|
||||
url(r'^tenants/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}),
|
||||
|
||||
]
|
||||
|
@ -16,6 +16,8 @@ from rest_framework.response import Response
|
||||
from rest_framework.serializers import Field, ModelSerializer, RelatedField, ValidationError
|
||||
from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||
|
||||
from .utils import dynamic_import
|
||||
|
||||
WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
|
||||
|
||||
|
||||
@ -24,6 +26,20 @@ class ServiceUnavailable(APIException):
|
||||
default_detail = "Service temporarily unavailable, please try again later."
|
||||
|
||||
|
||||
def get_serializer_for_model(model, prefix=''):
|
||||
"""
|
||||
Dynamically resolve and return the appropriate serializer for a model.
|
||||
"""
|
||||
app_name, model_name = model._meta.label.split('.')
|
||||
serializer_name = '{}.api.serializers.{}{}Serializer'.format(
|
||||
app_name, prefix, model_name
|
||||
)
|
||||
try:
|
||||
return dynamic_import(serializer_name)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
#
|
||||
# Authentication
|
||||
#
|
||||
|
@ -2,10 +2,38 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.db import models
|
||||
|
||||
from extras.models import ObjectChange
|
||||
from utilities.utils import serialize_object
|
||||
|
||||
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
|
||||
|
||||
def log_change(self, user, request_id, action):
|
||||
"""
|
||||
Create a new ObjectChange representing a change made to this object. This will typically be called automatically
|
||||
by extras.middleware.ChangeLoggingMiddleware.
|
||||
"""
|
||||
ObjectChange(
|
||||
user=user,
|
||||
request_id=request_id,
|
||||
changed_object=self,
|
||||
action=action,
|
||||
object_data=serialize_object(self)
|
||||
).save()
|
||||
|
@ -1,8 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import six
|
||||
|
||||
from django.core.serializers import serialize
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
@ -82,3 +84,15 @@ def dynamic_import(name):
|
||||
for comp in components[1:]:
|
||||
mod = getattr(mod, comp)
|
||||
return mod
|
||||
|
||||
|
||||
def serialize_object(obj, extra=None):
|
||||
"""
|
||||
Return a generic JSON representation of an object using Django's built-in serializer. (This is used for things like
|
||||
change logging, not the REST API.) Optionally include a dictionary to supplement the object data.
|
||||
"""
|
||||
json_str = serialize('json', [obj])
|
||||
data = json.loads(json_str)[0]['fields']
|
||||
if extra is not None:
|
||||
data.update(extra)
|
||||
return data
|
||||
|
@ -19,7 +19,7 @@ from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
|
||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate
|
||||
from extras.webhooks import bulk_operation_signal
|
||||
from utilities.utils import queryset_to_csv
|
||||
from utilities.forms import BootstrapMixin, CSVDataField
|
||||
@ -213,11 +213,6 @@ class ObjectEditView(GetReturnURLMixin, View):
|
||||
msg = '{} {}'.format(msg, escape(obj))
|
||||
messages.success(request, mark_safe(msg))
|
||||
|
||||
if obj_created:
|
||||
UserAction.objects.log_create(request.user, obj, msg)
|
||||
else:
|
||||
UserAction.objects.log_edit(request.user, obj, msg)
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
@ -279,7 +274,6 @@ class ObjectDeleteView(GetReturnURLMixin, View):
|
||||
|
||||
msg = 'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_delete(request.user, obj, msg)
|
||||
|
||||
return_url = form.cleaned_data.get('return_url')
|
||||
if return_url is not None and is_safe_url(url=return_url, host=request.get_host()):
|
||||
@ -365,7 +359,6 @@ class BulkCreateView(View):
|
||||
# If we make it to this point, validation has succeeded on all new objects.
|
||||
msg = "Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(model), msg)
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
@ -450,7 +443,6 @@ class BulkImportView(View):
|
||||
if new_objs:
|
||||
msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_import(request.user, ContentType.objects.get_for_model(new_objs[0]), msg)
|
||||
|
||||
return render(request, "import_success.html", {
|
||||
'table': obj_table,
|
||||
@ -566,7 +558,6 @@ class BulkEditView(View):
|
||||
if updated_count:
|
||||
msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
|
||||
messages.success(self.request, msg)
|
||||
UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg)
|
||||
|
||||
return redirect(return_url)
|
||||
|
||||
@ -661,7 +652,6 @@ class BulkDeleteView(View):
|
||||
|
||||
msg = 'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_bulk_delete(request.user, ContentType.objects.get_for_model(self.cls), msg)
|
||||
return redirect(return_url)
|
||||
|
||||
else:
|
||||
|
55
netbox/virtualization/migrations/0007_change_logging.py
Normal file
55
netbox/virtualization/migrations/0007_change_logging.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
@ -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']
|
||||
|
||||
|
@ -9,12 +9,18 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
CLUSTERTYPE_ACTIONS = """
|
||||
<a href="{% url 'virtualization:clustertype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.virtualization.change_clustertype %}
|
||||
<a href="{% url 'virtualization:clustertype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
CLUSTERGROUP_ACTIONS = """
|
||||
<a href="{% url 'virtualization:clustergroup_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.virtualization.change_clustergroup %}
|
||||
<a href="{% url 'virtualization:clustergroup_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
|
@ -2,8 +2,10 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from extras.views import ObjectChangeLogView
|
||||
from ipam.views import ServiceCreateView
|
||||
from . import views
|
||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
app_name = 'virtualization'
|
||||
urlpatterns = [
|
||||
@ -14,6 +16,7 @@ urlpatterns = [
|
||||
url(r'^cluster-types/import/$', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'),
|
||||
url(r'^cluster-types/delete/$', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'),
|
||||
url(r'^cluster-types/(?P<slug>[\w-]+)/edit/$', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
|
||||
url(r'^cluster-types/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}),
|
||||
|
||||
# Cluster groups
|
||||
url(r'^cluster-groups/$', views.ClusterGroupListView.as_view(), name='clustergroup_list'),
|
||||
@ -21,6 +24,7 @@ urlpatterns = [
|
||||
url(r'^cluster-groups/import/$', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'),
|
||||
url(r'^cluster-groups/delete/$', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'),
|
||||
url(r'^cluster-groups/(?P<slug>[\w-]+)/edit/$', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
|
||||
url(r'^cluster-groups/(?P<slug>[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}),
|
||||
|
||||
# Clusters
|
||||
url(r'^clusters/$', views.ClusterListView.as_view(), name='cluster_list'),
|
||||
@ -31,6 +35,7 @@ urlpatterns = [
|
||||
url(r'^clusters/(?P<pk>\d+)/$', views.ClusterView.as_view(), name='cluster'),
|
||||
url(r'^clusters/(?P<pk>\d+)/edit/$', views.ClusterEditView.as_view(), name='cluster_edit'),
|
||||
url(r'^clusters/(?P<pk>\d+)/delete/$', views.ClusterDeleteView.as_view(), name='cluster_delete'),
|
||||
url(r'^clusters/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}),
|
||||
url(r'^clusters/(?P<pk>\d+)/devices/add/$', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'),
|
||||
url(r'^clusters/(?P<pk>\d+)/devices/remove/$', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'),
|
||||
|
||||
@ -43,6 +48,7 @@ urlpatterns = [
|
||||
url(r'^virtual-machines/(?P<pk>\d+)/$', views.VirtualMachineView.as_view(), name='virtualmachine'),
|
||||
url(r'^virtual-machines/(?P<pk>\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'),
|
||||
url(r'^virtual-machines/(?P<pk>\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
|
||||
url(r'^virtual-machines/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}),
|
||||
url(r'^virtual-machines/(?P<virtualmachine>\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'),
|
||||
|
||||
# VM interfaces
|
||||
|
@ -258,12 +258,12 @@ class VirtualMachineView(View):
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
vm = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
|
||||
interfaces = Interface.objects.filter(virtual_machine=vm)
|
||||
services = Service.objects.filter(virtual_machine=vm)
|
||||
virtualmachine = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
|
||||
interfaces = Interface.objects.filter(virtual_machine=virtualmachine)
|
||||
services = Service.objects.filter(virtual_machine=virtualmachine)
|
||||
|
||||
return render(request, 'virtualization/virtualmachine.html', {
|
||||
'vm': vm,
|
||||
'virtualmachine': virtualmachine,
|
||||
'interfaces': interfaces,
|
||||
'services': services,
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user