mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
WIP
This commit is contained in:
parent
d6c41fb45f
commit
f5e09c6f48
@ -383,6 +383,27 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Config templates
|
||||||
|
#
|
||||||
|
|
||||||
|
class ConfigTemplateSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
|
||||||
|
data_source = NestedDataSourceSerializer(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
data_file = NestedDataFileSerializer(
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConfigTemplate
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source',
|
||||||
|
'data_path', 'data_file', 'data_synced', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Job Results
|
# Job Results
|
||||||
#
|
#
|
||||||
|
@ -14,6 +14,7 @@ router.register('tags', views.TagViewSet)
|
|||||||
router.register('image-attachments', views.ImageAttachmentViewSet)
|
router.register('image-attachments', views.ImageAttachmentViewSet)
|
||||||
router.register('journal-entries', views.JournalEntryViewSet)
|
router.register('journal-entries', views.JournalEntryViewSet)
|
||||||
router.register('config-contexts', views.ConfigContextViewSet)
|
router.register('config-contexts', views.ConfigContextViewSet)
|
||||||
|
router.register('config-templates', views.ConfigTemplateViewSet)
|
||||||
router.register('reports', views.ReportViewSet, basename='report')
|
router.register('reports', views.ReportViewSet, basename='report')
|
||||||
router.register('scripts', views.ScriptViewSet, basename='script')
|
router.register('scripts', views.ScriptViewSet, basename='script')
|
||||||
router.register('object-changes', views.ObjectChangeViewSet)
|
router.register('object-changes', views.ObjectChangeViewSet)
|
||||||
|
@ -157,6 +157,27 @@ class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
|
|||||||
filterset_class = filtersets.ConfigContextFilterSet
|
filterset_class = filtersets.ConfigContextFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Config templates
|
||||||
|
#
|
||||||
|
|
||||||
|
class ConfigTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
|
||||||
|
queryset = ConfigTemplate.objects.prefetch_related('data_source', 'data_file')
|
||||||
|
serializer_class = serializers.ConfigTemplateSerializer
|
||||||
|
filterset_class = filtersets.ConfigTemplateFilterSet
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def render(self, request, pk):
|
||||||
|
"""
|
||||||
|
Render a ConfigTemplate using the context data provided (if any).
|
||||||
|
"""
|
||||||
|
configtemplate = self.get_object()
|
||||||
|
output = configtemplate.render(context=request.data)
|
||||||
|
|
||||||
|
# TODO: Create a proper serializer
|
||||||
|
return Response({"output": output})
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Reports
|
# Reports
|
||||||
#
|
#
|
||||||
|
@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from core.models import DataFile, DataSource
|
from core.models import DataSource
|
||||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
@ -13,9 +13,9 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType
|
|||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextFilterSet',
|
'ConfigContextFilterSet',
|
||||||
|
'ConfigTemplateFilterSet',
|
||||||
'ContentTypeFilterSet',
|
'ContentTypeFilterSet',
|
||||||
'CustomFieldFilterSet',
|
'CustomFieldFilterSet',
|
||||||
'CustomLinkFilterSet',
|
'CustomLinkFilterSet',
|
||||||
@ -454,6 +454,33 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateFilterSet(BaseFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label=_('Search'),
|
||||||
|
)
|
||||||
|
data_source_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=DataSource.objects.all(),
|
||||||
|
label=_('Data source (ID)'),
|
||||||
|
)
|
||||||
|
data_file_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=DataSource.objects.all(),
|
||||||
|
label=_('Data file (ID)'),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConfigTemplate
|
||||||
|
fields = ['id', 'name', 'description', 'data_synced']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Filter for Local Config Context Data
|
# Filter for Local Config Context Data
|
||||||
#
|
#
|
||||||
|
@ -9,6 +9,7 @@ from utilities.forms import (
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextBulkEditForm',
|
'ConfigContextBulkEditForm',
|
||||||
|
'ConfigTemplateBulkEditForm',
|
||||||
'CustomFieldBulkEditForm',
|
'CustomFieldBulkEditForm',
|
||||||
'CustomLinkBulkEditForm',
|
'CustomLinkBulkEditForm',
|
||||||
'ExportTemplateBulkEditForm',
|
'ExportTemplateBulkEditForm',
|
||||||
@ -201,6 +202,19 @@ class ConfigContextBulkEditForm(BulkEditForm):
|
|||||||
nullable_fields = ('description',)
|
nullable_fields = ('description',)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateBulkEditForm(BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=ConfigTemplate.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
nullable_fields = ('description',)
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryBulkEditForm(BulkEditForm):
|
class JournalEntryBulkEditForm(BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=JournalEntry.objects.all(),
|
queryset=JournalEntry.objects.all(),
|
||||||
|
@ -10,6 +10,7 @@ from extras.utils import FeatureQuery
|
|||||||
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField
|
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'ConfigTemplateImportForm',
|
||||||
'CustomFieldImportForm',
|
'CustomFieldImportForm',
|
||||||
'CustomLinkImportForm',
|
'CustomLinkImportForm',
|
||||||
'ExportTemplateImportForm',
|
'ExportTemplateImportForm',
|
||||||
@ -83,6 +84,15 @@ class ExportTemplateImportForm(CSVModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateImportForm(CSVModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConfigTemplate
|
||||||
|
fields = (
|
||||||
|
'name', 'description', 'environment_params', 'template_code',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SavedFilterImportForm(CSVModelForm):
|
class SavedFilterImportForm(CSVModelForm):
|
||||||
content_types = CSVMultipleContentTypeField(
|
content_types = CSVMultipleContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
|
@ -20,6 +20,7 @@ from .mixins import SavedFiltersMixin
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextFilterForm',
|
'ConfigContextFilterForm',
|
||||||
|
'ConfigTemplateFilterForm',
|
||||||
'CustomFieldFilterForm',
|
'CustomFieldFilterForm',
|
||||||
'CustomLinkFilterForm',
|
'CustomLinkFilterForm',
|
||||||
'ExportTemplateFilterForm',
|
'ExportTemplateFilterForm',
|
||||||
@ -364,6 +365,26 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('q', 'filter_id')),
|
||||||
|
('Data', ('data_source_id', 'data_file_id')),
|
||||||
|
)
|
||||||
|
data_source_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=DataSource.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Data source')
|
||||||
|
)
|
||||||
|
data_file_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=DataFile.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Data file'),
|
||||||
|
query_params={
|
||||||
|
'source_id': '$data_source_id'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LocalConfigContextFilterForm(forms.Form):
|
class LocalConfigContextFilterForm(forms.Form):
|
||||||
local_context_data = forms.NullBooleanField(
|
local_context_data = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -18,6 +18,7 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextForm',
|
'ConfigContextForm',
|
||||||
|
'ConfigTemplateForm',
|
||||||
'CustomFieldForm',
|
'CustomFieldForm',
|
||||||
'CustomLinkForm',
|
'CustomLinkForm',
|
||||||
'ExportTemplateForm',
|
'ExportTemplateForm',
|
||||||
@ -269,6 +270,34 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
|
|||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
template_code = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.Textarea(attrs={'class': 'font-monospace'})
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Export Template', ('name', 'description', 'environment_params', 'tags')),
|
||||||
|
('Content', ('data_source', 'data_file', 'template_code',)),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConfigTemplate
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
|
||||||
|
raise forms.ValidationError("Must specify either local content or a data file")
|
||||||
|
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -8,6 +8,9 @@ class ExtrasQuery(graphene.ObjectType):
|
|||||||
config_context = ObjectField(ConfigContextType)
|
config_context = ObjectField(ConfigContextType)
|
||||||
config_context_list = ObjectListField(ConfigContextType)
|
config_context_list = ObjectListField(ConfigContextType)
|
||||||
|
|
||||||
|
config_template = ObjectField(ConfigTemplateType)
|
||||||
|
config_template_list = ObjectListField(ConfigTemplateType)
|
||||||
|
|
||||||
custom_field = ObjectField(CustomFieldType)
|
custom_field = ObjectField(CustomFieldType)
|
||||||
custom_field_list = ObjectListField(CustomFieldType)
|
custom_field_list = ObjectListField(CustomFieldType)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ from netbox.graphql.types import BaseObjectType, ObjectType
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextType',
|
'ConfigContextType',
|
||||||
|
'ConfigTemplateType',
|
||||||
'CustomFieldType',
|
'CustomFieldType',
|
||||||
'CustomLinkType',
|
'CustomLinkType',
|
||||||
'ExportTemplateType',
|
'ExportTemplateType',
|
||||||
@ -24,6 +25,14 @@ class ConfigContextType(ObjectType):
|
|||||||
filterset_class = filtersets.ConfigContextFilterSet
|
filterset_class = filtersets.ConfigContextFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateType(ObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.ConfigTemplate
|
||||||
|
fields = '__all__'
|
||||||
|
filterset_class = filtersets.ConfigTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldType(ObjectType):
|
class CustomFieldType(ObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
34
netbox/extras/migrations/0086_configtemplate.py
Normal file
34
netbox/extras/migrations/0086_configtemplate.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 4.1.6 on 2023-02-09 19:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
('extras', '0085_synced_data'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ConfigTemplate',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('data_path', models.CharField(blank=True, editable=False, max_length=1000)),
|
||||||
|
('data_synced', models.DateTimeField(blank=True, editable=False, null=True)),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('template_code', models.TextField()),
|
||||||
|
('environment_params', models.JSONField(blank=True, null=True)),
|
||||||
|
('data_file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile')),
|
||||||
|
('data_source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('name',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -1,5 +1,5 @@
|
|||||||
from .change_logging import ObjectChange
|
from .change_logging import ObjectChange
|
||||||
from .configcontexts import ConfigContext, ConfigContextModel
|
from .configs import *
|
||||||
from .customfields import CustomField
|
from .customfields import CustomField
|
||||||
from .models import *
|
from .models import *
|
||||||
from .search import *
|
from .search import *
|
||||||
@ -12,6 +12,7 @@ __all__ = (
|
|||||||
'ConfigContext',
|
'ConfigContext',
|
||||||
'ConfigContextModel',
|
'ConfigContextModel',
|
||||||
'ConfigRevision',
|
'ConfigRevision',
|
||||||
|
'ConfigTemplate',
|
||||||
'CustomField',
|
'CustomField',
|
||||||
'CustomLink',
|
'CustomLink',
|
||||||
'ExportTemplate',
|
'ExportTemplate',
|
||||||
|
@ -3,15 +3,20 @@ from django.core.validators import ValidationError
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
|
|
||||||
from extras.querysets import ConfigContextQuerySet
|
from extras.querysets import ConfigContextQuerySet
|
||||||
|
from netbox.config import get_config
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
from netbox.models.features import SyncedDataMixin
|
from netbox.models.features import ExportTemplatesMixin, SyncedDataMixin
|
||||||
|
from utilities.jinja2 import ConfigTemplateLoader
|
||||||
from utilities.utils import deepmerge
|
from utilities.utils import deepmerge
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContext',
|
'ConfigContext',
|
||||||
'ConfigContextModel',
|
'ConfigContextModel',
|
||||||
|
'ConfigTemplate',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -182,3 +187,59 @@ class ConfigContextModel(models.Model):
|
|||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{'local_context_data': 'JSON data must be in object form. Example: {"foo": 123}'}
|
{'local_context_data': 'JSON data must be in object form. Example: {"foo": 123}'}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Config templates
|
||||||
|
#
|
||||||
|
|
||||||
|
class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
template_code = models.TextField(
|
||||||
|
help_text=_('Jinja2 template code.')
|
||||||
|
)
|
||||||
|
environment_params = models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('name',)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('extras:configtemplate', args=[self.pk])
|
||||||
|
|
||||||
|
def sync_data(self):
|
||||||
|
"""
|
||||||
|
Synchronize template content from the designated DataFile (if any).
|
||||||
|
"""
|
||||||
|
self.template_code = self.data_file.data_as_string
|
||||||
|
self.data_synced = timezone.now()
|
||||||
|
|
||||||
|
def render(self, context=None):
|
||||||
|
"""
|
||||||
|
Render the contents of the template.
|
||||||
|
"""
|
||||||
|
context = context or {}
|
||||||
|
template_code = self.template_code
|
||||||
|
# output = render_jinja2(template_code, context)
|
||||||
|
|
||||||
|
environment = SandboxedEnvironment(
|
||||||
|
loader=ConfigTemplateLoader(data_source=self.data_source)
|
||||||
|
)
|
||||||
|
environment.filters.update(get_config().JINJA2_FILTERS)
|
||||||
|
output = environment.from_string(source=template_code).render(**context)
|
||||||
|
|
||||||
|
# Replace CRLF-style line terminators
|
||||||
|
output = output.replace('\r\n', '\n')
|
||||||
|
|
||||||
|
return output
|
@ -8,6 +8,7 @@ from .template_code import *
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextTable',
|
'ConfigContextTable',
|
||||||
|
'ConfigTemplateTable',
|
||||||
'CustomFieldTable',
|
'CustomFieldTable',
|
||||||
'CustomLinkTable',
|
'CustomLinkTable',
|
||||||
'ExportTemplateTable',
|
'ExportTemplateTable',
|
||||||
@ -223,6 +224,30 @@ class ConfigContextTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'weight', 'is_active', 'is_synced', 'description')
|
default_columns = ('pk', 'name', 'weight', 'is_active', 'is_synced', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateTable(NetBoxTable):
|
||||||
|
name = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
data_source = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
data_file = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
is_synced = columns.BooleanColumn(
|
||||||
|
verbose_name='Synced'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = ConfigTemplate
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'name', 'description', 'data_source', 'data_file', 'data_synced', 'created', 'last_updated',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'name', 'description', 'is_synced',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeTable(NetBoxTable):
|
class ObjectChangeTable(NetBoxTable):
|
||||||
time = tables.DateTimeColumn(
|
time = tables.DateTimeColumn(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
|
@ -64,6 +64,14 @@ urlpatterns = [
|
|||||||
path('config-contexts/sync/', views.ConfigContextBulkSyncDataView.as_view(), name='configcontext_bulk_sync'),
|
path('config-contexts/sync/', views.ConfigContextBulkSyncDataView.as_view(), name='configcontext_bulk_sync'),
|
||||||
path('config-contexts/<int:pk>/', include(get_model_urls('extras', 'configcontext'))),
|
path('config-contexts/<int:pk>/', include(get_model_urls('extras', 'configcontext'))),
|
||||||
|
|
||||||
|
# Config templates
|
||||||
|
path('config-templates/', views.ConfigTemplateListView.as_view(), name='configtemplate_list'),
|
||||||
|
path('config-templates/add/', views.ConfigTemplateEditView.as_view(), name='configtemplate_add'),
|
||||||
|
path('config-templates/edit/', views.ConfigTemplateBulkEditView.as_view(), name='configtemplate_bulk_edit'),
|
||||||
|
path('config-templates/delete/', views.ConfigTemplateBulkDeleteView.as_view(), name='configtemplate_bulk_delete'),
|
||||||
|
path('config-templates/sync/', views.ConfigTemplateBulkSyncDataView.as_view(), name='configtemplate_bulk_sync'),
|
||||||
|
path('config-templates/<int:pk>/', include(get_model_urls('extras', 'configtemplate'))),
|
||||||
|
|
||||||
# Image attachments
|
# Image attachments
|
||||||
path('image-attachments/add/', views.ImageAttachmentEditView.as_view(), name='imageattachment_add'),
|
path('image-attachments/add/', views.ImageAttachmentEditView.as_view(), name='imageattachment_add'),
|
||||||
path('image-attachments/<int:pk>/', include(get_model_urls('extras', 'imageattachment'))),
|
path('image-attachments/<int:pk>/', include(get_model_urls('extras', 'imageattachment'))),
|
||||||
|
@ -452,6 +452,58 @@ class ObjectConfigContextView(generic.ObjectView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Config templates
|
||||||
|
#
|
||||||
|
|
||||||
|
class ConfigTemplateListView(generic.ObjectListView):
|
||||||
|
queryset = ConfigTemplate.objects.all()
|
||||||
|
filterset = filtersets.ConfigTemplateFilterSet
|
||||||
|
filterset_form = forms.ConfigTemplateFilterForm
|
||||||
|
table = tables.ConfigTemplateTable
|
||||||
|
template_name = 'extras/configtemplate_list.html'
|
||||||
|
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_sync')
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(ConfigTemplate)
|
||||||
|
class ConfigTemplateView(generic.ObjectView):
|
||||||
|
queryset = ConfigTemplate.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(ConfigTemplate, 'edit')
|
||||||
|
class ConfigTemplateEditView(generic.ObjectEditView):
|
||||||
|
queryset = ConfigTemplate.objects.all()
|
||||||
|
form = forms.ConfigTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(ConfigTemplate, 'delete')
|
||||||
|
class ConfigTemplateDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = ConfigTemplate.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = ConfigTemplate.objects.all()
|
||||||
|
model_form = forms.ConfigTemplateImportForm
|
||||||
|
table = tables.ConfigTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = ConfigTemplate.objects.all()
|
||||||
|
filterset = filtersets.ConfigTemplateFilterSet
|
||||||
|
table = tables.ConfigTemplateTable
|
||||||
|
form = forms.ConfigTemplateBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = ConfigTemplate.objects.all()
|
||||||
|
filterset = filtersets.ConfigTemplateFilterSet
|
||||||
|
table = tables.ConfigTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateBulkSyncDataView(generic.BulkSyncDataView):
|
||||||
|
queryset = ConfigTemplate.objects.all()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Change logging
|
# Change logging
|
||||||
#
|
#
|
||||||
|
@ -311,6 +311,7 @@ OTHER_MENU = Menu(
|
|||||||
items=(
|
items=(
|
||||||
get_model_item('extras', 'tag', 'Tags'),
|
get_model_item('extras', 'tag', 'Tags'),
|
||||||
get_model_item('extras', 'configcontext', _('Config Contexts'), actions=['add']),
|
get_model_item('extras', 'configcontext', _('Config Contexts'), actions=['add']),
|
||||||
|
get_model_item('extras', 'configtemplate', _('Config Templates'), actions=['add']),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
76
netbox/templates/extras/configtemplate.html
Normal file
76
netbox/templates/extras/configtemplate.html
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-5">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Config Template</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Data Source</th>
|
||||||
|
<td>
|
||||||
|
{% if object.data_source %}
|
||||||
|
<a href="{{ object.data_source.get_absolute_url }}">{{ object.data_source }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Data File</th>
|
||||||
|
<td>
|
||||||
|
{% if object.data_file %}
|
||||||
|
<a href="{{ object.data_file.get_absolute_url }}">{{ object.data_file }}</a>
|
||||||
|
{% elif object.data_path %}
|
||||||
|
<div class="float-end text-warning">
|
||||||
|
<i class="mdi mdi-alert" title="The data file associated with this object has been deleted."></i>
|
||||||
|
</div>
|
||||||
|
{{ object.data_path }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Data Synced</th>
|
||||||
|
<td>{{ object.data_synced|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-7">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Environment Parameters</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre>{{ object.environment_params }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Template</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'inc/sync_warning.html' %}
|
||||||
|
<pre>{{ object.template_code }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
10
netbox/templates/extras/configtemplate_list.html
Normal file
10
netbox/templates/extras/configtemplate_list.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends 'generic/object_list.html' %}
|
||||||
|
|
||||||
|
{% block bulk_buttons %}
|
||||||
|
{% if perms.extras.sync_configtemplate %}
|
||||||
|
<button type="submit" name="_sync" formaction="{% url 'extras:configtemplate_bulk_sync' %}" class="btn btn-primary btn-sm">
|
||||||
|
<i class="mdi mdi-sync" aria-hidden="true"></i> Sync Data
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
27
netbox/utilities/jinja2.py
Normal file
27
netbox/utilities/jinja2.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from jinja2 import BaseLoader, TemplateNotFound
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ConfigTemplateLoader',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigTemplateLoader(BaseLoader):
|
||||||
|
"""
|
||||||
|
Custom Jinja2 loader to facilitate populating template content from DataFiles.
|
||||||
|
"""
|
||||||
|
def __init__(self, data_source):
|
||||||
|
self.data_source = data_source
|
||||||
|
|
||||||
|
def get_source(self, environment, template):
|
||||||
|
DataFile = apps.get_model('core', 'DataFile')
|
||||||
|
|
||||||
|
try:
|
||||||
|
datafile = DataFile.objects.get(source=self.data_source, path=template)
|
||||||
|
template_source = datafile.data_as_string
|
||||||
|
except DataFile.DoesNotExist:
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
return template_source, template, True
|
Loading…
Reference in New Issue
Block a user