1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #14438: Database representation of scripts

- Introduces the Script model to represent individual Python classes within a ScriptModule file
- Automatically migrates jobs & event rules

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
Arthur Hanson
2024-02-23 05:27:37 -08:00
committed by GitHub
parent 7e7e5d5eb0
commit ca2ee436a0
25 changed files with 569 additions and 336 deletions

View File

@@ -115,10 +115,6 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
ct_field='action_object_type',
fk_field='action_object_id'
)
action_parameters = models.JSONField(
blank=True,
null=True
)
action_data = models.JSONField(
verbose_name=_('data'),
blank=True,

View File

@@ -2,8 +2,11 @@ import inspect
import logging
from functools import cached_property
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.db.models import Q
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -22,12 +25,63 @@ __all__ = (
logger = logging.getLogger('netbox.data_backends')
class Script(EventRulesMixin, models.Model):
"""
Dummy model used to generate permissions for custom scripts. Does not exist in the database.
"""
class Script(EventRulesMixin, JobsMixin):
name = models.CharField(
verbose_name=_('name'),
max_length=79, # Maximum length for a Python class name
editable=False,
)
module = models.ForeignKey(
to='extras.ScriptModule',
on_delete=models.CASCADE,
related_name='scripts',
editable=False
)
is_executable = models.BooleanField(
default=True,
verbose_name=_('is executable'),
editable=False
)
events = GenericRelation(
'extras.EventRule',
content_type_field='action_object_type',
object_id_field='action_object_id'
)
def __str__(self):
return self.name
objects = RestrictedQuerySet.as_manager()
class Meta:
managed = False
ordering = ('module', 'name')
constraints = (
models.UniqueConstraint(
fields=('name', 'module'),
name='extras_script_unique_name_module'
),
)
verbose_name = _('script')
verbose_name_plural = _('scripts')
def get_absolute_url(self):
return reverse('extras:script', args=[self.pk])
@property
def result(self):
return self.jobs.all().order_by('-created').first()
@cached_property
def python_class(self):
return self.module.module_scripts.get(self.name)
def delete(self, soft_delete=False, **kwargs):
if soft_delete and self.jobs.exists():
self.is_executable = False
self.save()
else:
super().delete(**kwargs)
self.id = None
class ScriptModuleManager(models.Manager.from_queryset(RestrictedQuerySet)):
@@ -55,7 +109,7 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
return self.python_name
@cached_property
def scripts(self):
def module_scripts(self):
def _get_name(cls):
# For child objects in submodules use the full import path w/o the root module as the name
@@ -78,6 +132,39 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
return scripts
def sync_classes(self):
"""
Syncs the file-based module to the database, adding and removing individual Script objects
in the database as needed.
"""
db_classes = {
script.name: script for script in self.scripts.all()
}
db_classes_set = set(db_classes.keys())
module_classes_set = set(self.module_scripts.keys())
# remove any existing db classes if they are no longer in the file
removed = db_classes_set - module_classes_set
for name in removed:
db_classes[name].delete(soft_delete=True)
added = module_classes_set - db_classes_set
for name in added:
Script.objects.create(
module=self,
name=name,
is_executable=True,
)
def sync_data(self):
super().sync_data()
self.sync_classes()
def save(self, *args, **kwargs):
self.file_root = ManagedFileRootPathChoices.SCRIPTS
return super().save(*args, **kwargs)
@receiver(post_save, sender=ScriptModule)
def script_module_post_save_handler(instance, created, **kwargs):
instance.sync_classes()