| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- from django.conf import settings
- from django.core.validators import ValidationError
- from django.db import models
- from django.urls import reverse
- from django.utils.translation import gettext_lazy as _
- from jinja2.loaders import BaseLoader
- from jinja2.sandbox import SandboxedEnvironment
- from extras.querysets import ConfigContextQuerySet
- from netbox.config import get_config
- from netbox.models import ChangeLoggedModel
- from netbox.models.features import CloningMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
- from utilities.jinja2 import ConfigTemplateLoader
- from utilities.utils import deepmerge
- __all__ = (
- 'ConfigContext',
- 'ConfigContextModel',
- 'ConfigTemplate',
- )
- #
- # Config contexts
- #
- class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
- """
- 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(
- verbose_name=_('name'),
- max_length=100,
- unique=True
- )
- weight = models.PositiveSmallIntegerField(
- verbose_name=_('weight'),
- default=1000
- )
- description = models.CharField(
- verbose_name=_('description'),
- max_length=200,
- blank=True
- )
- is_active = models.BooleanField(
- verbose_name=_('is active'),
- default=True,
- )
- regions = models.ManyToManyField(
- to='dcim.Region',
- related_name='+',
- blank=True
- )
- site_groups = models.ManyToManyField(
- to='dcim.SiteGroup',
- related_name='+',
- blank=True
- )
- sites = models.ManyToManyField(
- to='dcim.Site',
- related_name='+',
- blank=True
- )
- locations = models.ManyToManyField(
- to='dcim.Location',
- related_name='+',
- blank=True
- )
- device_types = models.ManyToManyField(
- to='dcim.DeviceType',
- related_name='+',
- blank=True
- )
- roles = models.ManyToManyField(
- to='dcim.DeviceRole',
- related_name='+',
- blank=True
- )
- platforms = models.ManyToManyField(
- to='dcim.Platform',
- related_name='+',
- blank=True
- )
- cluster_types = models.ManyToManyField(
- to='virtualization.ClusterType',
- related_name='+',
- blank=True
- )
- cluster_groups = models.ManyToManyField(
- to='virtualization.ClusterGroup',
- related_name='+',
- blank=True
- )
- clusters = models.ManyToManyField(
- to='virtualization.Cluster',
- related_name='+',
- blank=True
- )
- tenant_groups = models.ManyToManyField(
- to='tenancy.TenantGroup',
- related_name='+',
- blank=True
- )
- tenants = models.ManyToManyField(
- to='tenancy.Tenant',
- related_name='+',
- blank=True
- )
- tags = models.ManyToManyField(
- to='extras.Tag',
- related_name='+',
- blank=True
- )
- data = models.JSONField()
- objects = ConfigContextQuerySet.as_manager()
- clone_fields = (
- 'weight', 'is_active', 'regions', 'site_groups', 'sites', 'locations', 'device_types',
- 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
- 'tenants', 'tags', 'data',
- )
- class Meta:
- ordering = ['weight', 'name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return reverse('extras:configcontext', kwargs={'pk': self.pk})
- @property
- def docs_url(self):
- return f'{settings.STATIC_URL}docs/models/extras/configcontext/'
- def clean(self):
- super().clean()
- # Verify that JSON data is provided as an object
- if type(self.data) is not dict:
- raise ValidationError(
- {'data': _('JSON data must be in object form. Example: {"foo": 123}')}
- )
- def sync_data(self):
- """
- Synchronize context data from the designated DataFile (if any).
- """
- self.data = self.data_file.get_data()
- sync_data.alters_data = True
- class ConfigContextModel(models.Model):
- """
- A model which includes local configuration context data. This local data will override any inherited data from
- ConfigContexts.
- """
- local_context_data = models.JSONField(
- blank=True,
- null=True,
- help_text=_(
- "Local config context data takes precedence over source contexts in the final rendered config context"
- )
- )
- class Meta:
- abstract = True
- def get_config_context(self):
- """
- Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs.
- Return the rendered configuration context for a device or VM.
- """
- data = {}
- if not hasattr(self, 'config_context_data'):
- # The annotation is not available, so we fall back to manually querying for the config context objects
- config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True)
- else:
- # The attribute may exist, but the annotated value could be None if there is no config context data
- config_context_data = self.config_context_data or []
- for context in config_context_data:
- data = deepmerge(data, context)
- # If the object has local config context data defined, merge it last
- if self.local_context_data:
- data = deepmerge(data, self.local_context_data)
- return data
- def clean(self):
- super().clean()
- # Verify that JSON data is provided as an object
- if self.local_context_data and type(self.local_context_data) is not dict:
- raise ValidationError(
- {'local_context_data': _('JSON data must be in object form. Example: {"foo": 123}')}
- )
- #
- # Config templates
- #
- class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
- name = models.CharField(
- verbose_name=_('name'),
- max_length=100
- )
- description = models.CharField(
- verbose_name=_('description'),
- max_length=200,
- blank=True
- )
- template_code = models.TextField(
- verbose_name=_('template code'),
- help_text=_('Jinja2 template code.')
- )
- environment_params = models.JSONField(
- verbose_name=_('environment parameters'),
- blank=True,
- null=True,
- default=dict,
- help_text=_(
- 'Any <a href="https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment">additional parameters</a>'
- ' to pass when constructing the Jinja2 environment.'
- )
- )
- 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
- sync_data.alters_data = True
- def render(self, context=None):
- """
- Render the contents of the template.
- """
- context = context or {}
- # Initialize the Jinja2 environment and instantiate the Template
- environment = self._get_environment()
- if self.data_file:
- template = environment.get_template(self.data_file.path)
- else:
- template = environment.from_string(self.template_code)
- output = template.render(**context)
- # Replace CRLF-style line terminators
- return output.replace('\r\n', '\n')
- def _get_environment(self):
- """
- Instantiate and return a Jinja2 environment suitable for rendering the ConfigTemplate.
- """
- # Initialize the template loader & cache the base template code (if applicable)
- if self.data_file:
- loader = ConfigTemplateLoader(data_source=self.data_source)
- loader.cache_templates({
- self.data_file.path: self.template_code
- })
- else:
- loader = BaseLoader()
- # Initialize the environment
- env_params = self.environment_params or {}
- environment = SandboxedEnvironment(loader=loader, **env_params)
- environment.filters.update(get_config().JINJA2_FILTERS)
- return environment
|