소스 검색

Merge pull request #2445 from digitalocean/local-config-context

Local config context
Jeremy Stretch 7 년 전
부모
커밋
7616bcad3d

+ 2 - 0
docs/additional-features/context-data.md

@@ -1,3 +1,5 @@
 # Contextual Configuration Data
 # Contextual Configuration Data
 
 
 Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. For example, you might want to associate a set of syslog servers for all devices at a particular site. Context data enables the association of arbitrary data to devices and virtual machines grouped by region, site, role, platform, and/or tenant. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
 Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. For example, you might want to associate a set of syslog servers for all devices at a particular site. Context data enables the association of arbitrary data to devices and virtual machines grouped by region, site, role, platform, and/or tenant. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
+
+Devices and Virtual Machines may also have a local config context defined. This local context will always overwrite the rendered config context objects for the Device/VM. This is useful in situations were the device requires a one-off value different from the rest of the environment.

+ 2 - 2
netbox/dcim/api/serializers.py

@@ -412,7 +412,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
             'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
             'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
             'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
             'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
             'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created',
             'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created',
-            'last_updated',
+            'last_updated', 'local_context_data',
         ]
         ]
         validators = []
         validators = []
 
 
@@ -448,7 +448,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
             'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
             'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
             'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
             'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
             'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields',
             'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields',
-            'config_context', 'created', 'last_updated',
+            'config_context', 'created', 'last_updated', 'local_context_data',
         ]
         ]
 
 
     def get_config_context(self, obj):
     def get_config_context(self, obj):

+ 4 - 1
netbox/dcim/forms.py

@@ -18,7 +18,7 @@ from utilities.forms import (
     AnnotatedMultipleChoiceField, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
     AnnotatedMultipleChoiceField, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
     BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm,
     BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm,
     ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField,
     ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField,
-    FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField,
+    FlexibleModelChoiceField, JSONField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField,
 )
 )
 from virtualization.models import Cluster
 from virtualization.models import Cluster
 from .constants import (
 from .constants import (
@@ -822,16 +822,19 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
     )
     )
     comments = CommentField()
     comments = CommentField()
     tags = TagField(required=False)
     tags = TagField(required=False)
+    local_context_data = JSONField(required=False)
 
 
     class Meta:
     class Meta:
         model = Device
         model = Device
         fields = [
         fields = [
             'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
             'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
             'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags',
             'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags',
+            'local_context_data'
         ]
         ]
         help_texts = {
         help_texts = {
             'device_role': "The function this device serves",
             'device_role': "The function this device serves",
             'serial': "Chassis serial number",
             'serial': "Chassis serial number",
+            'local_context_data': "Local config context data overwrites all sources contexts in the final rendered config context"
         }
         }
         widgets = {
         widgets = {
             'face': forms.Select(attrs={'filter-for': 'position'}),
             'face': forms.Select(attrs={'filter-for': 'position'}),

+ 19 - 0
netbox/dcim/migrations/0063_device_local_context_data.py

@@ -0,0 +1,19 @@
+# Generated by Django 2.0.8 on 2018-09-16 02:01
+
+import django.contrib.postgres.fields.jsonb
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0062_interface_mtu'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='device',
+            name='local_context_data',
+            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
+        ),
+    ]

+ 9 - 0
netbox/extras/models.py

@@ -711,6 +711,11 @@ class ConfigContext(models.Model):
 
 
 class ConfigContextModel(models.Model):
 class ConfigContextModel(models.Model):
 
 
+    local_context_data = JSONField(
+        blank=True,
+        null=True,
+    )
+
     class Meta:
     class Meta:
         abstract = True
         abstract = True
 
 
@@ -724,6 +729,10 @@ class ConfigContextModel(models.Model):
         for context in ConfigContext.objects.get_for_object(self):
         for context in ConfigContext.objects.get_for_object(self):
             data.update(context.data)
             data.update(context.data)
 
 
+        # If the object has local config context data defined, that data overwrites all rendered data
+        if self.local_context_data is not None:
+            data.update(self.local_context_data)
+
         return data
         return data
 
 
 
 

+ 3 - 1
netbox/extras/views.py

@@ -106,9 +106,11 @@ class ObjectConfigContextView(View):
 
 
         obj = get_object_or_404(self.object_class, pk=pk)
         obj = get_object_or_404(self.object_class, pk=pk)
         source_contexts = ConfigContext.objects.get_for_object(obj)
         source_contexts = ConfigContext.objects.get_for_object(obj)
+        model_name = self.object_class._meta.model_name
 
 
         return render(request, 'extras/object_configcontext.html', {
         return render(request, 'extras/object_configcontext.html', {
-            self.object_class._meta.model_name: obj,
+            model_name: obj,
+            'obj': obj,
             'rendered_context': obj.get_config_context(),
             'rendered_context': obj.get_config_context(),
             'source_contexts': source_contexts,
             'source_contexts': source_contexts,
             'base_template': self.base_template,
             'base_template': self.base_template,

+ 6 - 0
netbox/templates/dcim/device_edit.html

@@ -77,6 +77,12 @@
             </div>
             </div>
         </div>
         </div>
     {% endif %}
     {% endif %}
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Local Config Context Data</strong></div>
+        <div class="panel-body">
+            {% render_field form.local_context_data %}
+        </div>
+    </div>
     <div class="panel panel-default">
     <div class="panel panel-default">
         <div class="panel-heading"><strong>Tags</strong></div>
         <div class="panel-heading"><strong>Tags</strong></div>
         <div class="panel-body">
         <div class="panel-body">

+ 18 - 0
netbox/templates/extras/object_configcontext.html

@@ -16,6 +16,24 @@
             </div>
             </div>
         </div>
         </div>
         <div class="col-md-6">
         <div class="col-md-6">
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Local Context</strong>
+                </div>
+                <div class="panel-body">
+                    {% if obj.local_context_data %}
+                        <pre>{{ obj.local_context_data|render_json }}</pre>
+                    {% else %}
+                        <span class="text-muted">None</span>
+                    {% endif %}
+                </div>
+                <div class="panel-footer">
+                    <span class="help-block">
+                        <i class="fa fa-info-circle"></i>
+                        The local config context overwrites all source contexts.
+                    </span>
+                </div>
+            </div>
             <div class="panel panel-default">
             <div class="panel panel-default">
                 <div class="panel-heading">
                 <div class="panel-heading">
                     <strong>Source Contexts</strong>
                     <strong>Source Contexts</strong>

+ 6 - 0
netbox/templates/virtualization/virtualmachine_edit.html

@@ -48,6 +48,12 @@
             </div>
             </div>
         </div>
         </div>
     {% endif %}
     {% endif %}
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Local Config Context Data</strong></div>
+        <div class="panel-body">
+            {% render_field form.local_context_data %}
+        </div>
+    </div>
     <div class="panel panel-default">
     <div class="panel panel-default">
         <div class="panel-heading"><strong>Tags</strong></div>
         <div class="panel-heading"><strong>Tags</strong></div>
         <div class="panel-body">
         <div class="panel-body">

+ 2 - 0
netbox/virtualization/api/serializers.py

@@ -107,6 +107,7 @@ class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
         fields = [
         fields = [
             'id', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4',
             'id', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4',
             'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
             'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+            'local_context_data',
         ]
         ]
 
 
 
 
@@ -117,6 +118,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
         fields = [
         fields = [
             'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6',
             'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6',
             'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
             'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
+            'local_context_data',
         ]
         ]
 
 
     def get_config_context(self, obj):
     def get_config_context(self, obj):

+ 6 - 1
netbox/virtualization/forms.py

@@ -16,7 +16,8 @@ from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
     AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
     AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
     ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm,
     ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm,
-    ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, SlugField, SmallTextarea, add_blank_choice
+    ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, SmallTextarea,
+    add_blank_choice
 )
 )
 from .constants import VM_STATUS_CHOICES
 from .constants import VM_STATUS_CHOICES
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@@ -246,6 +247,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
         )
         )
     )
     )
     tags = TagField(required=False)
     tags = TagField(required=False)
+    local_context_data = JSONField(required=False)
 
 
     class Meta:
     class Meta:
         model = VirtualMachine
         model = VirtualMachine
@@ -253,6 +255,9 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
             'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
             'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
             'vcpus', 'memory', 'disk', 'comments', 'tags',
             'vcpus', 'memory', 'disk', 'comments', 'tags',
         ]
         ]
+        help_texts = {
+            'local_context_data': "Local config context data overwrites all sources contexts in the final rendered config context",
+        }
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
 
 

+ 19 - 0
netbox/virtualization/migrations/0008_virtualmachine_local_context_data.py

@@ -0,0 +1,19 @@
+# Generated by Django 2.0.8 on 2018-09-16 02:01
+
+import django.contrib.postgres.fields.jsonb
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('virtualization', '0007_change_logging'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='virtualmachine',
+            name='local_context_data',
+            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
+        ),
+    ]

+ 1 - 0
netbox/virtualization/models.py

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 
 from django.conf import settings
 from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
 from django.contrib.contenttypes.fields import GenericRelation
+from django.contrib.postgres.fields import JSONField
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.db import models
 from django.urls import reverse
 from django.urls import reverse