mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-17 13:08:16 -06:00
14438 check valid script for views
This commit is contained in:
parent
41c792a3e5
commit
172d1b00cc
@ -35,12 +35,17 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=79)),
|
('name', models.CharField(max_length=79)),
|
||||||
('module', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='scripts', to='extras.scriptmodule')),
|
('module', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='scripts', to='extras.scriptmodule')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ('name', 'pk'),
|
'ordering': ('name', 'pk'),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='script',
|
||||||
|
name='is_valid',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='script',
|
model_name='script',
|
||||||
constraint=models.UniqueConstraint(fields=('name', 'module'), name='extras_script_unique_name_module'),
|
constraint=models.UniqueConstraint(fields=('name', 'module'), name='extras_script_unique_name_module'),
|
||||||
|
@ -29,9 +29,12 @@ class Script(EventRulesMixin, JobsMixin, models.Model):
|
|||||||
)
|
)
|
||||||
module = models.ForeignKey(
|
module = models.ForeignKey(
|
||||||
to='extras.ScriptModule',
|
to='extras.ScriptModule',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.RESTRICT,
|
||||||
related_name='scripts'
|
related_name='scripts'
|
||||||
)
|
)
|
||||||
|
is_valid = models.BooleanField(
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -51,6 +54,14 @@ class Script(EventRulesMixin, JobsMixin, models.Model):
|
|||||||
def python_class(self):
|
def python_class(self):
|
||||||
return self.module.get_module_scripts.get(self.name)
|
return self.module.get_module_scripts.get(self.name)
|
||||||
|
|
||||||
|
def delete_if_no_jobs(self):
|
||||||
|
if self.jobs.all():
|
||||||
|
self.is_valid = False
|
||||||
|
self.save()
|
||||||
|
else:
|
||||||
|
self.delete()
|
||||||
|
self.id = None
|
||||||
|
|
||||||
|
|
||||||
class ScriptModuleManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
class ScriptModuleManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
||||||
|
|
||||||
@ -100,6 +111,35 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
|||||||
|
|
||||||
return scripts
|
return scripts
|
||||||
|
|
||||||
|
def sync_classes(self):
|
||||||
|
db_classes = {}
|
||||||
|
for obj in self.scripts.filter(module=self):
|
||||||
|
db_classes[obj.name] = obj
|
||||||
|
|
||||||
|
db_classes_set = {k for k in db_classes.keys()}
|
||||||
|
|
||||||
|
module_scripts = self.get_module_scripts
|
||||||
|
|
||||||
|
module_classes_set = {k for k in 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_if_no_jobs()
|
||||||
|
|
||||||
|
added = module_classes_set - db_classes_set
|
||||||
|
for name in added:
|
||||||
|
Script.objects.create(
|
||||||
|
module=self,
|
||||||
|
name=name,
|
||||||
|
is_valid=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def sync_data(self):
|
||||||
|
super().sync_data()
|
||||||
|
self.sync_classes()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.file_root = ManagedFileRootPathChoices.SCRIPTS
|
self.file_root = ManagedFileRootPathChoices.SCRIPTS
|
||||||
return super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
self.sync_classes()
|
||||||
|
@ -920,7 +920,7 @@ class DashboardWidgetAddView(LoginRequiredMixin, View):
|
|||||||
widget = widget_class(**data)
|
widget = widget_class(**data)
|
||||||
request.user.dashboard.add_widget(widget)
|
request.user.dashboard.add_widget(widget)
|
||||||
request.user.dashboard.save()
|
request.user.dashboard.save()
|
||||||
messages.success(request, f'Added widget {widget.id}')
|
messages.success(request, _('Added widget: ') + str(widget.id))
|
||||||
|
|
||||||
return HttpResponse(headers={
|
return HttpResponse(headers={
|
||||||
'HX-Redirect': reverse('home'),
|
'HX-Redirect': reverse('home'),
|
||||||
@ -961,7 +961,7 @@ class DashboardWidgetConfigView(LoginRequiredMixin, View):
|
|||||||
data['config'] = config_form.cleaned_data
|
data['config'] = config_form.cleaned_data
|
||||||
request.user.dashboard.config[str(id)].update(data)
|
request.user.dashboard.config[str(id)].update(data)
|
||||||
request.user.dashboard.save()
|
request.user.dashboard.save()
|
||||||
messages.success(request, f'Updated widget {widget.id}')
|
messages.success(request, _('Updated widget: ') + str(widget.id))
|
||||||
|
|
||||||
return HttpResponse(headers={
|
return HttpResponse(headers={
|
||||||
'HX-Redirect': reverse('home'),
|
'HX-Redirect': reverse('home'),
|
||||||
@ -997,9 +997,9 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
request.user.dashboard.delete_widget(id)
|
request.user.dashboard.delete_widget(id)
|
||||||
request.user.dashboard.save()
|
request.user.dashboard.save()
|
||||||
messages.success(request, f'Deleted widget {id}')
|
messages.success(request, _('Deleted widget: ') + str(id))
|
||||||
else:
|
else:
|
||||||
messages.error(request, f'Error deleting widget: {form.errors[0]}')
|
messages.error(request, _('Error deleting widget: ') + str(form.errors[0]))
|
||||||
|
|
||||||
return redirect(reverse('home'))
|
return redirect(reverse('home'))
|
||||||
|
|
||||||
@ -1042,19 +1042,45 @@ def get_script_module(module, request):
|
|||||||
return get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__regex=f"^{module}\\.")
|
return get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__regex=f"^{module}\\.")
|
||||||
|
|
||||||
|
|
||||||
class ScriptView(ContentTypePermissionRequiredMixin, View):
|
class BaseScriptView(ContentTypePermissionRequiredMixin, View):
|
||||||
|
script = None
|
||||||
|
script_class = None
|
||||||
|
jobs = None
|
||||||
|
|
||||||
|
def get_required_permission(self):
|
||||||
|
return 'extras.view_script'
|
||||||
|
|
||||||
|
def get_script(self, request, pk):
|
||||||
|
self.script = Script.objects.get(pk=pk)
|
||||||
|
if self.script.python_class:
|
||||||
|
self.script_class = script.python_class()
|
||||||
|
else:
|
||||||
|
self.script.delete_if_no_jobs()
|
||||||
|
messages.error(request, _("Script class has been deleted."))
|
||||||
|
if not self.script.id:
|
||||||
|
return redirect('extras:script_list')
|
||||||
|
else:
|
||||||
|
return redirect('extras:script_jobs', pk=self.script.id)
|
||||||
|
|
||||||
|
self.jobs = self.script.jobs.all()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptView(BaseScriptView):
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return 'extras.view_script'
|
return 'extras.view_script'
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
script = Script.objects.get(pk=pk)
|
if ret := self.get_script(request, pk):
|
||||||
script_class = script.python_class()
|
return ret
|
||||||
jobs = script.jobs.all()
|
|
||||||
form = script_class.as_form(initial=normalize_querydict(request.GET))
|
form = None
|
||||||
|
if self.script_class:
|
||||||
|
form = script_class.as_form(initial=normalize_querydict(request.GET))
|
||||||
|
|
||||||
return render(request, 'extras/script.html', {
|
return render(request, 'extras/script.html', {
|
||||||
'job_count': jobs.count(),
|
'job_count': self.jobs.count(),
|
||||||
'module': script.module,
|
'module': script.module,
|
||||||
'script': script,
|
'script': script,
|
||||||
'script_class': script_class,
|
'script_class': script_class,
|
||||||
@ -1065,85 +1091,93 @@ class ScriptView(ContentTypePermissionRequiredMixin, View):
|
|||||||
if not request.user.has_perm('extras.run_script'):
|
if not request.user.has_perm('extras.run_script'):
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
script = Script.objects.get(pk=pk)
|
if ret := self.get_script(request, pk):
|
||||||
script_class = script.python_class()
|
return ret
|
||||||
jobs = script.jobs.all()
|
|
||||||
form = script_class.as_form(request.POST, request.FILES)
|
form = None
|
||||||
|
if self.script_class:
|
||||||
|
form = script_class.as_form(request.POST, request.FILES)
|
||||||
|
|
||||||
# Allow execution only if RQ worker process is running
|
# Allow execution only if RQ worker process is running
|
||||||
if not get_workers_for_queue('default'):
|
if not get_workers_for_queue('default'):
|
||||||
messages.error(request, "Unable to run script: RQ worker process not running.")
|
messages.error(request, _("Unable to run script: RQ worker process not running."))
|
||||||
elif form.is_valid():
|
elif form.is_valid():
|
||||||
job = Job.enqueue(
|
job = Job.enqueue(
|
||||||
run_script,
|
run_script,
|
||||||
instance=script,
|
instance=self.script,
|
||||||
name=script_class.class_name,
|
name=self.script_class.class_name,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
schedule_at=form.cleaned_data.pop('_schedule_at'),
|
schedule_at=form.cleaned_data.pop('_schedule_at'),
|
||||||
interval=form.cleaned_data.pop('_interval'),
|
interval=form.cleaned_data.pop('_interval'),
|
||||||
data=form.cleaned_data,
|
data=form.cleaned_data,
|
||||||
request=copy_safe_request(request),
|
request=copy_safe_request(request),
|
||||||
job_timeout=script.python_class.job_timeout,
|
job_timeout=self.script.python_class.job_timeout,
|
||||||
commit=form.cleaned_data.pop('_commit')
|
commit=form.cleaned_data.pop('_commit')
|
||||||
)
|
)
|
||||||
|
|
||||||
return redirect('extras:script_result', job_pk=job.pk)
|
return redirect('extras:script_result', job_pk=job.pk)
|
||||||
|
|
||||||
return render(request, 'extras/script.html', {
|
return render(request, 'extras/script.html', {
|
||||||
'job_count': jobs.count(),
|
'job_count': self.jobs.count(),
|
||||||
'module': script.module,
|
'module': self.script.module,
|
||||||
'script': script,
|
'script': self.script,
|
||||||
'script_class': script_class,
|
'script_class': self.script_class,
|
||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ScriptSourceView(ContentTypePermissionRequiredMixin, View):
|
class ScriptSourceView(BaseScriptView):
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return 'extras.view_script'
|
return 'extras.view_script'
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
script = Script.objects.get(pk=pk)
|
if ret := self.get_script(request, pk):
|
||||||
script_class = script.python_class()
|
return ret
|
||||||
jobs = script.jobs.all()
|
|
||||||
|
|
||||||
return render(request, 'extras/script/source.html', {
|
return render(request, 'extras/script/source.html', {
|
||||||
'job_count': jobs.count(),
|
'job_count': self.jobs.count(),
|
||||||
'module': script.module,
|
'module': self.script.module,
|
||||||
'script': script,
|
'script': self.script,
|
||||||
'script_class': script_class,
|
'script_class': self.script_class,
|
||||||
'tab': 'source',
|
'tab': 'source',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ScriptJobsView(ContentTypePermissionRequiredMixin, View):
|
class ScriptJobsView(BaseScriptView):
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return 'extras.view_script'
|
return 'extras.view_script'
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
script = Script.objects.get(pk=pk)
|
self.script = Script.objects.get(pk=pk)
|
||||||
script_class = script.python_class()
|
if self.script.python_class:
|
||||||
jobs = script.jobs.all()
|
self.script_class = script.python_class()
|
||||||
|
else:
|
||||||
|
self.script.delete_if_no_jobs()
|
||||||
|
if not self.script.id:
|
||||||
|
messages.error(request, _("Script class has been deleted."))
|
||||||
|
return redirect('extras:script_list')
|
||||||
|
|
||||||
|
self.jobs = self.script.jobs.all()
|
||||||
|
|
||||||
jobs_table = JobTable(
|
jobs_table = JobTable(
|
||||||
data=jobs,
|
data=self.jobs,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
user=request.user
|
user=request.user
|
||||||
)
|
)
|
||||||
jobs_table.configure(request)
|
jobs_table.configure(request)
|
||||||
|
|
||||||
return render(request, 'extras/script/jobs.html', {
|
return render(request, 'extras/script/jobs.html', {
|
||||||
'job_count': jobs.count(),
|
'job_count': self.jobs.count(),
|
||||||
'module': script.module,
|
'module': self.script.module,
|
||||||
'script': script,
|
'script': self.script,
|
||||||
'table': jobs_table,
|
'table': jobs_table,
|
||||||
'tab': 'jobs',
|
'tab': 'jobs',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ScriptResultView(ContentTypePermissionRequiredMixin, View):
|
class ScriptResultView(BaseScriptView):
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return 'extras.view_script'
|
return 'extras.view_script'
|
||||||
|
Loading…
Reference in New Issue
Block a user