From c13e4858d7c1704ada3d4e1c941bd2b1a6336c88 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Jun 2018 16:02:34 -0400 Subject: [PATCH] Initial work on config contexts --- netbox/dcim/api/views.py | 6 +- netbox/dcim/models.py | 4 +- netbox/dcim/urls.py | 1 + netbox/dcim/views.py | 12 ++ netbox/extras/admin.py | 12 +- netbox/extras/api/serializers.py | 26 ++- netbox/extras/api/urls.py | 3 + netbox/extras/api/views.py | 12 +- netbox/extras/forms.py | 20 ++- .../extras/migrations/0014_config-contexts.py | 44 +++++ netbox/extras/models.py | 88 ++++++++++ netbox/extras/tables.py | 27 ++- netbox/extras/urls.py | 8 + netbox/extras/views.py | 53 +++++- netbox/templates/dcim/device.html | 3 + .../templates/dcim/device_configcontext.html | 18 ++ netbox/templates/extras/configcontext.html | 154 ++++++++++++++++++ .../templates/extras/configcontext_edit.html | 24 +++ .../templates/extras/configcontext_list.html | 16 ++ netbox/templates/inc/nav_menu.html | 3 + .../virtualization/virtualmachine.html | 3 + .../virtualmachine_configcontext.html | 18 ++ netbox/virtualization/api/views.py | 9 + netbox/virtualization/models.py | 4 +- netbox/virtualization/urls.py | 1 + netbox/virtualization/views.py | 12 ++ 26 files changed, 565 insertions(+), 16 deletions(-) create mode 100644 netbox/extras/migrations/0014_config-contexts.py create mode 100644 netbox/templates/dcim/device_configcontext.html create mode 100644 netbox/templates/extras/configcontext.html create mode 100644 netbox/templates/extras/configcontext_edit.html create mode 100644 netbox/templates/extras/configcontext_list.html create mode 100644 netbox/templates/virtualization/virtualmachine_configcontext.html diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 5ef4b1de7..b99d98477 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -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): """ diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index ecb83ecaa..6963ddc7f 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -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. diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index de1cbd4cc..6824f620a 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -141,6 +141,7 @@ urlpatterns = [ url(r'^devices/(?P\d+)/$', views.DeviceView.as_view(), name='device'), url(r'^devices/(?P\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'), url(r'^devices/(?P\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), + url(r'^devices/(?P\d+)/config-context/$', views.DeviceConfigContextView.as_view(), name='device_configcontext'), url(r'^devices/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), url(r'^devices/(?P\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), url(r'^devices/(?P\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 0e783d39c..be5e3276b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -994,6 +994,18 @@ class DeviceConfigView(PermissionRequiredMixin, View): }) +class DeviceConfigContextView(View): + + def get(self, request, pk): + + device = get_object_or_404(Device, pk=pk) + + return render(request, 'dcim/device_configcontext.html', { + 'device': device, + 'active_tab': 'config-context', + }) + + class DeviceCreateView(PermissionRequiredMixin, ObjectEditView): permission_required = 'dcim.add_device' model = Device diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 7d30cff34..20a8f6f4e 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -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 # diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 10afee954..a64acc1a4 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -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,22 @@ 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', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenants', 'data'] + + # # Reports # diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 3b4e59ef2..cf61841dd 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -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') diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index d65a099ad..55e4457ce 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -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 # diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index a39814eb6..bd7ace840 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -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,21 @@ 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', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenants', 'data'] + + # # Image attachments # diff --git a/netbox/extras/migrations/0014_config-contexts.py b/netbox/extras/migrations/0014_config-contexts.py new file mode 100644 index 000000000..5d8d46d6f --- /dev/null +++ b/netbox/extras/migrations/0014_config-contexts.py @@ -0,0 +1,44 @@ +# Generated by Django 2.0.6 on 2018-06-27 17:45 + +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)), + ('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'), + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 4b41a523a..ff9977160 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -629,6 +629,94 @@ 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 + ) + 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() + + 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. + """ + + # `device_role` for Device; `role` for VirtualMachine + role = getattr(self, 'device_role', None) or self.role + + # Gather all ConfigContexts orders by weight, name + contexts = ConfigContext.objects.filter( + Q(regions=self.site.region) | Q(regions=None), + Q(sites=self.site) | Q(sites=None), + Q(roles=role) | Q(roles=None), + Q(tenants=self.tenant) | Q(tenants=None), + Q(platforms=self.platform) | Q(platforms=None), + is_active=True, + ).order_by('weight', 'name') + + # Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs + data = {} + for context in contexts: + data.update(context.data) + + return data + + # # Report results # diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index bd190c7e5..be5d748e3 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -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 %} + +{% endif %} +{% if perms.extras.delete_configcontext %} + +{% endif %} +""" + OBJECTCHANGE_ACTION = """ {% if record.action == 1 %} Created @@ -44,7 +53,21 @@ 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() + actions = tables.TemplateColumn( + template_code=CONFIGCONTEXT_ACTIONS, + attrs={'td': {'class': 'text-right'}}, + verbose_name='' + ) + + class Meta(BaseTable.Meta): + model = ConfigContext + fields = ('pk', 'name', 'weight', 'active') class ObjectChangeTable(BaseTable): diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index d92303264..e56652280 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -13,6 +13,14 @@ urlpatterns = [ url(r'^tags/(?P[\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\d+)/$', views.ConfigContextView.as_view(), name='configcontext'), + url(r'^config-contexts/(?P\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'), + url(r'^config-contexts/(?P\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\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'), url(r'^image-attachments/(?P\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 46cddabf4..8670fdcdb 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -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,53 @@ 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' + + # # Change logging # diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 1c1539f89..4984dad95 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -69,6 +69,9 @@ {% include 'dcim/inc/device_napalm_tabs.html' %} {% endif %} {% endif %} + diff --git a/netbox/templates/dcim/device_configcontext.html b/netbox/templates/dcim/device_configcontext.html new file mode 100644 index 000000000..adb00291d --- /dev/null +++ b/netbox/templates/dcim/device_configcontext.html @@ -0,0 +1,18 @@ +{% extends 'dcim/device.html' %} + +{% block title %}{{ device }} - Config Context{% endblock %} + +{% block content %} +
+
+
+
+ Config Context +
+
+
{{ device.get_config_context }}
+
+
+
+
+{% endblock %} diff --git a/netbox/templates/extras/configcontext.html b/netbox/templates/extras/configcontext.html new file mode 100644 index 000000000..08d257b5f --- /dev/null +++ b/netbox/templates/extras/configcontext.html @@ -0,0 +1,154 @@ +{% extends '_base.html' %} +{% load helpers %} + +{% block header %} +
+
+ +
+
+
+
+ + + + +
+
+
+
+
+ {% if perms.extras.change_configcontext %} + + + Edit this config context + + {% endif %} +
+

{% block title %}{{ configcontext }}{% endblock %}

+{% endblock %} + +{% block content %} +
+
+
+
+ Config Context +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + {{ configcontext.name }} +
Weight + {{ configcontext.weight }} +
Active + {% if configcontext.is_active %} + + + + {% else %} + + + + {% endif %} +
Regions + {% if configcontext.regions.all %} +
    + {% for region in configcontext.regions.all %} +
  • {{ region }}
  • + {% endfor %} +
+ {% else %} + None + {% endif %} +
Sites + {% if configcontext.sites.all %} +
    + {% for site in configcontext.sites.all %} +
  • {{ site }}
  • + {% endfor %} +
+ {% else %} + None + {% endif %} +
Roles + {% if configcontext.roles.all %} +
    + {% for role in configcontext.roles.all %} +
  • {{ role }}
  • + {% endfor %} +
+ {% else %} + None + {% endif %} +
Platforms + {% if configcontext.platforms.all %} +
    + {% for platform in configcontext.platforms.all %} +
  • {{ platform }}
  • + {% endfor %} +
+ {% else %} + None + {% endif %} +
Tenants + {% if configcontext.tenants.all %} +
    + {% for tenant in configcontext.tenants.all %} +
  • {{ tenant }}
  • + {% endfor %} +
+ {% else %} + None + {% endif %} +
+
+
+
+
+
+ Data +
+
+
{{ configcontext.data }}
+
+
+
+
+{% endblock %} diff --git a/netbox/templates/extras/configcontext_edit.html b/netbox/templates/extras/configcontext_edit.html new file mode 100644 index 000000000..a0f428131 --- /dev/null +++ b/netbox/templates/extras/configcontext_edit.html @@ -0,0 +1,24 @@ +{% extends 'utilities/obj_edit.html' %} +{% load form_helpers %} + +{% block form %} +
+
Config Context
+
+ {% render_field form.name %} + {% render_field form.weight %} + {% render_field form.is_active %} + {% render_field form.regions %} + {% render_field form.sites %} + {% render_field form.roles %} + {% render_field form.platforms %} + {% render_field form.tenants %} +
+
+
+
Data
+
+ {% render_field form.data %} +
+
+{% endblock %} diff --git a/netbox/templates/extras/configcontext_list.html b/netbox/templates/extras/configcontext_list.html new file mode 100644 index 000000000..98913d987 --- /dev/null +++ b/netbox/templates/extras/configcontext_list.html @@ -0,0 +1,16 @@ +{% extends '_base.html' %} +{% load buttons %} + +{% block content %} +
+ {% if perms.extras.add_configcontext %} + {% add_button 'extras:configcontext_add' %} + {% endif %} +
+

{% block title %}Config Contexts{% endblock %}

+
+
+ {% include 'utilities/obj_table.html' with bulk_delete_url='extras:configcontext_bulk_delete' %} +
+
+{% endblock %} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index ced87768e..aeddf1969 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -63,6 +63,9 @@
  • Tags
  • +
  • + Config Contexts +
  • Reports
  • diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index d6d594b55..9b5dbc471 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -44,6 +44,9 @@ + diff --git a/netbox/templates/virtualization/virtualmachine_configcontext.html b/netbox/templates/virtualization/virtualmachine_configcontext.html new file mode 100644 index 000000000..4218a4161 --- /dev/null +++ b/netbox/templates/virtualization/virtualmachine_configcontext.html @@ -0,0 +1,18 @@ +{% extends 'virtualization/virtualmachine.html' %} + +{% block title %}{{ virtualmachine }} - Config Context{% endblock %} + +{% block content %} +
    +
    +
    +
    + Config Context +
    +
    +
    {{ virtualmachine.get_config_context }}
    +
    +
    +
    +
    +{% endblock %} diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index fae8b9232..b04248f87 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -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') diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 70a73dc05..904d04634 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -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. """ diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py index d123e7cfa..b03b3bc0a 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -48,6 +48,7 @@ urlpatterns = [ url(r'^virtual-machines/(?P\d+)/$', views.VirtualMachineView.as_view(), name='virtualmachine'), url(r'^virtual-machines/(?P\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), url(r'^virtual-machines/(?P\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), + url(r'^virtual-machines/(?P\d+)/config-context/$', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), url(r'^virtual-machines/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}), url(r'^virtual-machines/(?P\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'), diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index c3fa97f2f..6286ba1a5 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -269,6 +269,18 @@ class VirtualMachineView(View): }) +class VirtualMachineConfigContextView(View): + + def get(self, request, pk): + + virtualmachine = get_object_or_404(VirtualMachine, pk=pk) + + return render(request, 'virtualization/virtualmachine_configcontext.html', { + 'virtualmachine': virtualmachine, + 'active_tab': 'config-context', + }) + + class VirtualMachineCreateView(PermissionRequiredMixin, ObjectEditView): permission_required = 'virtualization.add_virtualmachine' model = VirtualMachine