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:
@@ -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,
|
||||
|
@@ -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()
|
||||
|
Reference in New Issue
Block a user