mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge pull request #2208 from digitalocean/1349-config-contexts
1349 config contexts
This commit is contained in:
commit
06dab9c468
@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
@ -233,6 +232,11 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
serializer_class = serializers.DeviceSerializer
|
||||
filter_class = filters.DeviceFilter
|
||||
|
||||
@detail_route(url_path='config-context')
|
||||
def config_context(self, request, pk):
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
return Response(device.get_config_context())
|
||||
|
||||
@detail_route(url_path='napalm')
|
||||
def napalm(self, request, pk):
|
||||
"""
|
||||
|
@ -19,7 +19,7 @@ from timezone_field import TimeZoneField
|
||||
|
||||
from circuits.models import Circuit
|
||||
from extras.constants import OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||
from extras.models import CustomFieldModel, ObjectChange
|
||||
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange
|
||||
from extras.rpc import RPC_CLIENTS
|
||||
from utilities.fields import ColorField, NullableCharField
|
||||
from utilities.managers import NaturalOrderByManager
|
||||
@ -1158,7 +1158,7 @@ class DeviceManager(NaturalOrderByManager):
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Device(ChangeLoggedModel, CustomFieldModel):
|
||||
class Device(ChangeLoggedModel, ConfigContextModel, 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.
|
||||
|
@ -141,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+)/config-context/$', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
|
||||
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'),
|
||||
|
@ -19,6 +19,7 @@ from natsort import natsorted
|
||||
|
||||
from circuits.models import Circuit
|
||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from extras.views import ObjectConfigContextView
|
||||
from ipam.models import Prefix, Service, VLAN
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
@ -994,6 +995,11 @@ class DeviceConfigView(PermissionRequiredMixin, View):
|
||||
})
|
||||
|
||||
|
||||
class DeviceConfigContextView(ObjectConfigContextView):
|
||||
object_class = Device
|
||||
base_template = 'dcim/device.html'
|
||||
|
||||
|
||||
class DeviceCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.add_device'
|
||||
model = Device
|
||||
|
@ -7,7 +7,8 @@ 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, ObjectChange, TopologyMap, UserAction, Webhook,
|
||||
ConfigContext, CustomField, CustomFieldChoice, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction,
|
||||
Webhook,
|
||||
)
|
||||
|
||||
|
||||
@ -125,6 +126,15 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
|
||||
@admin.register(ConfigContext)
|
||||
class ConfigContextAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'weight']
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
@ -4,10 +4,16 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from rest_framework import serializers
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
|
||||
from dcim.api.serializers import (
|
||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
|
||||
NestedRegionSerializer, NestedSiteSerializer,
|
||||
)
|
||||
from dcim.models import Device, Rack, Site
|
||||
from extras.models import ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, UserAction
|
||||
from extras.models import (
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, UserAction,
|
||||
)
|
||||
from extras.constants import *
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from utilities.api import (
|
||||
ChoiceFieldSerializer, ContentTypeFieldSerializer, get_serializer_for_model, ValidatedModelSerializer,
|
||||
@ -121,6 +127,24 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
|
||||
class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
regions = NestedRegionSerializer(many=True)
|
||||
sites = NestedSiteSerializer(many=True)
|
||||
roles = NestedDeviceRoleSerializer(many=True)
|
||||
platforms = NestedPlatformSerializer(many=True)
|
||||
tenants = NestedTenantSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = [
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenants', 'data',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Reports
|
||||
#
|
||||
|
@ -34,6 +34,9 @@ router.register(r'tags', views.TagViewSet)
|
||||
# Image attachments
|
||||
router.register(r'image-attachments', views.ImageAttachmentViewSet)
|
||||
|
||||
# Config contexts
|
||||
router.register(r'config-contexts', views.ConfigContextViewSet)
|
||||
|
||||
# Reports
|
||||
router.register(r'reports', views.ReportViewSet, base_name='report')
|
||||
|
||||
|
@ -12,7 +12,8 @@ from taggit.models import Tag
|
||||
|
||||
from extras import filters
|
||||
from extras.models import (
|
||||
CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, UserAction,
|
||||
ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||
UserAction,
|
||||
)
|
||||
from extras.reports import get_report, get_reports
|
||||
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
||||
@ -132,6 +133,15 @@ class ImageAttachmentViewSet(ModelViewSet):
|
||||
serializer_class = serializers.ImageAttachmentSerializer
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
|
||||
class ConfigContextViewSet(ModelViewSet):
|
||||
queryset = ConfigContext.objects.prefetch_related('regions', 'sites', 'roles', 'platforms', 'tenants')
|
||||
serializer_class = serializers.ConfigContextSerializer
|
||||
|
||||
|
||||
#
|
||||
# Reports
|
||||
#
|
||||
|
@ -5,14 +5,16 @@ from collections import OrderedDict
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from mptt.forms import TreeNodeMultipleChoiceField
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.models import Region
|
||||
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
|
||||
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange
|
||||
|
||||
|
||||
#
|
||||
@ -174,7 +176,6 @@ class CustomFieldFilterForm(forms.Form):
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
#
|
||||
|
||||
class TagForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
@ -184,6 +185,23 @@ class TagForm(BootstrapMixin, forms.ModelForm):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
|
||||
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
regions = TreeNodeMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = [
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenants', 'data',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Image attachments
|
||||
#
|
||||
|
45
netbox/extras/migrations/0014_configcontexts.py
Normal file
45
netbox/extras/migrations/0014_configcontexts.py
Normal file
@ -0,0 +1,45 @@
|
||||
# Generated by Django 2.0.6 on 2018-06-29 13:34
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0005_change_logging'),
|
||||
('dcim', '0060_change_logging'),
|
||||
('extras', '0013_objectchange'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ConfigContext',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('weight', models.PositiveSmallIntegerField(default=1000)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||
('platforms', models.ManyToManyField(blank=True, related_name='_configcontext_platforms_+', to='dcim.Platform')),
|
||||
('regions', models.ManyToManyField(blank=True, related_name='_configcontext_regions_+', to='dcim.Region')),
|
||||
('roles', models.ManyToManyField(blank=True, related_name='_configcontext_roles_+', to='dcim.DeviceRole')),
|
||||
('sites', models.ManyToManyField(blank=True, related_name='_configcontext_sites_+', to='dcim.Site')),
|
||||
('tenants', models.ManyToManyField(blank=True, related_name='_configcontext_tenants_+', to='tenancy.Tenant')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['weight', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='obj_type',
|
||||
field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='obj_type',
|
||||
field=models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'rackgroup', 'device', 'interface', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vlangroup', 'vrf', 'service', 'tenant', 'tenantgroup', 'cluster', 'clustergroup', 'virtualmachine')}, related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types'),
|
||||
),
|
||||
]
|
@ -2,7 +2,6 @@ from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
import json
|
||||
|
||||
import graphviz
|
||||
from django.contrib.auth.models import User
|
||||
@ -21,6 +20,7 @@ from django.utils.safestring import mark_safe
|
||||
from dcim.constants import CONNECTION_STATUS_CONNECTED
|
||||
from utilities.utils import foreground_color
|
||||
from .constants import *
|
||||
from .querysets import ConfigContextQuerySet
|
||||
|
||||
|
||||
#
|
||||
@ -629,6 +629,87 @@ class ImageAttachment(models.Model):
|
||||
return None
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
|
||||
class ConfigContext(models.Model):
|
||||
"""
|
||||
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
|
||||
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
|
||||
will be available to a Device in site A assigned to tenant B. Data is stored in JSON format.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
default=1000
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
)
|
||||
regions = models.ManyToManyField(
|
||||
to='dcim.Region',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
sites = models.ManyToManyField(
|
||||
to='dcim.Site',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
roles = models.ManyToManyField(
|
||||
to='dcim.DeviceRole',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
platforms = models.ManyToManyField(
|
||||
to='dcim.Platform',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
tenants = models.ManyToManyField(
|
||||
to='tenancy.Tenant',
|
||||
related_name='+',
|
||||
blank=True
|
||||
)
|
||||
data = JSONField()
|
||||
|
||||
objects = ConfigContextQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['weight', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:configcontext', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
class ConfigContextModel(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get_config_context(self):
|
||||
"""
|
||||
Return the rendered configuration context for a device or VM.
|
||||
"""
|
||||
|
||||
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
|
||||
data = OrderedDict()
|
||||
for context in ConfigContext.objects.get_for_object(self):
|
||||
data.update(context.data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
#
|
||||
# Report results
|
||||
#
|
||||
@ -766,10 +847,6 @@ class ObjectChange(models.Model):
|
||||
self.object_data,
|
||||
)
|
||||
|
||||
@property
|
||||
def object_data_pretty(self):
|
||||
return json.dumps(self.object_data, indent=4, sort_keys=True)
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
|
23
netbox/extras/querysets.py
Normal file
23
netbox/extras/querysets.py
Normal file
@ -0,0 +1,23 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import Q, QuerySet
|
||||
|
||||
|
||||
class ConfigContextQuerySet(QuerySet):
|
||||
|
||||
def get_for_object(self, obj):
|
||||
"""
|
||||
Return all applicable ConfigContexts for a given object. Only active ConfigContexts will be included.
|
||||
"""
|
||||
|
||||
# `device_role` for Device; `role` for VirtualMachine
|
||||
role = getattr(obj, 'device_role', None) or obj.role
|
||||
|
||||
return self.filter(
|
||||
Q(regions=obj.site.region) | Q(regions=None),
|
||||
Q(sites=obj.site) | Q(sites=None),
|
||||
Q(roles=role) | Q(roles=None),
|
||||
Q(tenants=obj.tenant) | Q(tenants=None),
|
||||
Q(platforms=obj.platform) | Q(platforms=None),
|
||||
is_active=True,
|
||||
).order_by('weight', 'name')
|
@ -4,7 +4,7 @@ import django_tables2 as tables
|
||||
from taggit.models import Tag
|
||||
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import ObjectChange
|
||||
from .models import ConfigContext, ObjectChange
|
||||
|
||||
TAG_ACTIONS = """
|
||||
{% if perms.taggit.change_tag %}
|
||||
@ -15,6 +15,15 @@ TAG_ACTIONS = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
CONFIGCONTEXT_ACTIONS = """
|
||||
{% if perms.extras.change_configcontext %}
|
||||
<a href="{% url 'extras:configcontext_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% if perms.extras.delete_configcontext %}
|
||||
<a href="{% url 'extras:configcontext_delete' pk=record.pk %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-trash" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
OBJECTCHANGE_ACTION = """
|
||||
{% if record.action == 1 %}
|
||||
<span class="label label-success">Created</span>
|
||||
@ -44,7 +53,24 @@ class TagTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Tag
|
||||
fields = ('pk', 'name', 'items')
|
||||
fields = ('pk', 'name', 'weight')
|
||||
|
||||
|
||||
class ConfigContextTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
is_active = tables.BooleanColumn(
|
||||
verbose_name='Active'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=CONFIGCONTEXT_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConfigContext
|
||||
fields = ('pk', 'name', 'weight', 'is_active', 'description', 'actions')
|
||||
|
||||
|
||||
class ObjectChangeTable(BaseTable):
|
||||
|
@ -13,6 +13,14 @@ urlpatterns = [
|
||||
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
|
||||
# Config contexts
|
||||
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
|
||||
|
||||
# Image attachments
|
||||
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||
url(r'^image-attachments/(?P<pk>\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
|
||||
|
@ -14,10 +14,10 @@ from taggit.models import Tag
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||
from . import filters
|
||||
from .forms import ObjectChangeFilterForm, ImageAttachmentForm, TagForm
|
||||
from .models import ImageAttachment, ObjectChange, ReportResult
|
||||
from .forms import ConfigContextForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
||||
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
||||
from .reports import get_report, get_reports
|
||||
from .tables import ObjectChangeTable, TagTable
|
||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
||||
|
||||
|
||||
#
|
||||
@ -53,6 +53,71 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
|
||||
class ConfigContextListView(ObjectListView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
table = ConfigContextTable
|
||||
template_name = 'extras/configcontext_list.html'
|
||||
|
||||
|
||||
class ConfigContextView(View):
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
configcontext = get_object_or_404(ConfigContext, pk=pk)
|
||||
|
||||
return render(request, 'extras/configcontext.html', {
|
||||
'configcontext': configcontext,
|
||||
})
|
||||
|
||||
|
||||
class ConfigContextCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'extras.add_configcontext'
|
||||
model = ConfigContext
|
||||
model_form = ConfigContextForm
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
template_name = 'extras/configcontext_edit.html'
|
||||
|
||||
|
||||
class ConfigContextEditView(ConfigContextCreateView):
|
||||
permission_required = 'extras.change_configcontext'
|
||||
|
||||
|
||||
class ConfigContextDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'extras.delete_configcontext'
|
||||
model = ConfigContext
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
|
||||
class ConfigContextBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'extras.delete_cconfigcontext'
|
||||
cls = ConfigContext
|
||||
queryset = ConfigContext.objects.all()
|
||||
table = ConfigContextTable
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
|
||||
class ObjectConfigContextView(View):
|
||||
object_class = None
|
||||
base_template = None
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
obj = get_object_or_404(self.object_class, pk=pk)
|
||||
source_contexts = ConfigContext.objects.get_for_object(obj)
|
||||
|
||||
return render(request, 'extras/object_configcontext.html', {
|
||||
self.object_class._meta.model_name: obj,
|
||||
'rendered_context': obj.get_config_context(),
|
||||
'source_contexts': source_contexts,
|
||||
'base_template': self.base_template,
|
||||
'active_tab': 'config-context',
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
@ -69,6 +69,9 @@
|
||||
{% include 'dcim/inc/device_napalm_tabs.html' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<li role="presentation"{% if active_tab == 'config-context' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device_configcontext' pk=device.pk %}">Config Context</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device_changelog' pk=device.pk %}">Changelog</a>
|
||||
</li>
|
||||
|
171
netbox/templates/extras/configcontext.html
Normal file
171
netbox/templates/extras/configcontext.html
Normal file
@ -0,0 +1,171 @@
|
||||
{% 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 'extras:configcontext_list' %}">Config Contexts</a></li>
|
||||
<li>{{ configcontext }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'extras:configcontext_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>
|
||||
<div class="pull-right">
|
||||
{% if perms.extras.change_configcontext %}
|
||||
<a href="{% url 'extras:configcontext_edit' pk=configcontext.pk %}" class="btn btn-warning">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||
Edit this config context
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ configcontext }}{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Config Context</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
{{ configcontext.name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Weight</td>
|
||||
<td>
|
||||
{{ configcontext.weight }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>
|
||||
{% if configcontext.description %}
|
||||
{{ configcontext.description }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Active</td>
|
||||
<td>
|
||||
{% if configcontext.is_active %}
|
||||
<span class="text-success">
|
||||
<i class="fa fa-check"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-danger">
|
||||
<i class="fa fa-close"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Assignment</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Regions</td>
|
||||
<td>
|
||||
{% if configcontext.regions.all %}
|
||||
<ul>
|
||||
{% for region in configcontext.regions.all %}
|
||||
<li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sites</td>
|
||||
<td>
|
||||
{% if configcontext.sites.all %}
|
||||
<ul>
|
||||
{% for site in configcontext.sites.all %}
|
||||
<li><a href="{{ site.get_absolute_url }}">{{ site }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Roles</td>
|
||||
<td>
|
||||
{% if configcontext.roles.all %}
|
||||
<ul>
|
||||
{% for role in configcontext.roles.all %}
|
||||
<li><a href="{{ role.get_absolute_url }}">{{ role }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Platforms</td>
|
||||
<td>
|
||||
{% if configcontext.platforms.all %}
|
||||
<ul>
|
||||
{% for platform in configcontext.platforms.all %}
|
||||
<li><a href="{{ platform.get_absolute_url }}">{{ platform }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenants</td>
|
||||
<td>
|
||||
{% if configcontext.tenants.all %}
|
||||
<ul>
|
||||
{% for tenant in configcontext.tenants.all %}
|
||||
<li><a href="{{ tenant.get_absolute_url }}">{{ tenant }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Data</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<pre>{{ configcontext.data|render_json }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
30
netbox/templates/extras/configcontext_edit.html
Normal file
30
netbox/templates/extras/configcontext_edit.html
Normal file
@ -0,0 +1,30 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Config Context</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.name %}
|
||||
{% render_field form.weight %}
|
||||
{% render_field form.description %}
|
||||
{% render_field form.is_active %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Assignment</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.regions %}
|
||||
{% render_field form.sites %}
|
||||
{% render_field form.roles %}
|
||||
{% render_field form.platforms %}
|
||||
{% render_field form.tenants %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Data</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.data %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
16
netbox/templates/extras/configcontext_list.html
Normal file
16
netbox/templates/extras/configcontext_list.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.extras.add_configcontext %}
|
||||
{% add_button 'extras:configcontext_add' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}Config Contexts{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:configcontext_bulk_delete' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
38
netbox/templates/extras/object_configcontext.html
Normal file
38
netbox/templates/extras/object_configcontext.html
Normal file
@ -0,0 +1,38 @@
|
||||
{% extends base_template %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}{{ block.super }} - Config Context{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Rendered Context</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<pre>{{ rendered_context|render_json }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Source Contexts</strong>
|
||||
</div>
|
||||
{% for context in source_contexts %}
|
||||
<div class="panel-body">
|
||||
<div class="pull-right">
|
||||
<span class="text-muted">{{ context.weight }}</span>
|
||||
</div>
|
||||
<a href="{{ context.get_absolute_url }}"><strong>{{ context.name }}</strong></a>
|
||||
{% if context.description %}
|
||||
<br /><small>{{ context.description }}</small>
|
||||
{% endif %}
|
||||
<pre>{{ context.data|render_json }}</pre>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -83,7 +83,7 @@
|
||||
<strong>Object Data</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<pre>{{ objectchange.object_data_pretty }}</pre>
|
||||
<pre>{{ objectchange.object_data|render_json }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -63,6 +63,9 @@
|
||||
<li>
|
||||
<a href="{% url 'extras:tag_list' %}">Tags</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'extras:configcontext_list' %}">Config Contexts</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'extras:report_list' %}">Reports</a>
|
||||
</li>
|
||||
|
@ -44,6 +44,9 @@
|
||||
<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 == 'config-context' %} class="active"{% endif %}>
|
||||
<a href="{% url 'virtualization:virtualmachine_configcontext' pk=virtualmachine.pk %}">Config Context</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'virtualization:virtualmachine_changelog' pk=virtualmachine.pk %}">Changelog</a>
|
||||
</li>
|
||||
|
@ -1,6 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
@ -46,6 +47,14 @@ def gfm(value):
|
||||
return mark_safe(html)
|
||||
|
||||
|
||||
@register.filter()
|
||||
def render_json(value):
|
||||
"""
|
||||
Render a dictionary as formatted JSON.
|
||||
"""
|
||||
return json.dumps(value, indent=4, sort_keys=True)
|
||||
|
||||
|
||||
@register.filter()
|
||||
def model_name(obj):
|
||||
"""
|
||||
|
@ -1,5 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.response import Response
|
||||
|
||||
from dcim.models import Interface
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from utilities.api import FieldChoicesViewSet, ModelViewSet
|
||||
@ -49,6 +53,11 @@ class VirtualMachineViewSet(CustomFieldModelViewSet):
|
||||
serializer_class = serializers.VirtualMachineSerializer
|
||||
filter_class = filters.VirtualMachineFilter
|
||||
|
||||
@detail_route(url_path='config-context')
|
||||
def config_context(self, request, pk):
|
||||
device = get_object_or_404(VirtualMachine, pk=pk)
|
||||
return Response(device.get_config_context())
|
||||
|
||||
|
||||
class InterfaceViewSet(ModelViewSet):
|
||||
queryset = Interface.objects.filter(virtual_machine__isnull=False).select_related('virtual_machine')
|
||||
|
@ -9,7 +9,7 @@ from django.utils.encoding import python_2_unicode_compatible
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.models import Device
|
||||
from extras.models import CustomFieldModel
|
||||
from extras.models import ConfigContextModel, CustomFieldModel
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
|
||||
|
||||
@ -168,7 +168,7 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class VirtualMachine(ChangeLoggedModel, CustomFieldModel):
|
||||
class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
"""
|
||||
A virtual machine which runs inside a Cluster.
|
||||
"""
|
||||
|
@ -48,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+)/config-context/$', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'),
|
||||
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'),
|
||||
|
||||
|
@ -9,6 +9,7 @@ from django.views.generic import View
|
||||
|
||||
from dcim.models import Device, Interface
|
||||
from dcim.tables import DeviceTable
|
||||
from extras.views import ObjectConfigContextView
|
||||
from ipam.models import Service
|
||||
from utilities.views import (
|
||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ObjectDeleteView,
|
||||
@ -269,6 +270,11 @@ class VirtualMachineView(View):
|
||||
})
|
||||
|
||||
|
||||
class VirtualMachineConfigContextView(ObjectConfigContextView):
|
||||
object_class = VirtualMachine
|
||||
base_template = 'virtualization/virtualmachine.html'
|
||||
|
||||
|
||||
class VirtualMachineCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'virtualization.add_virtualmachine'
|
||||
model = VirtualMachine
|
||||
|
Loading…
Reference in New Issue
Block a user