diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index 5ecb7ef46..cadf7912d 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -3,12 +3,14 @@ import copy from django import forms from core.models import * +from extras.forms.mixins import SyncedDataMixin from netbox.forms import NetBoxModelForm from netbox.registry import registry -from utilities.forms import BootstrapMixin, CommentField, get_field_value +from utilities.forms import CommentField, get_field_value __all__ = ( 'DataSourceForm', + 'ManagedFileForm', ) @@ -73,3 +75,26 @@ class DataSourceForm(NetBoxModelForm): self.instance.parameters = parameters return super().save(*args, **kwargs) + + +class ManagedFileForm(SyncedDataMixin, NetBoxModelForm): + upload_file = forms.FileField( + required=False + ) + + fieldsets = ( + ('File Upload', ('upload_file',)), + ('Data Source', ('data_source', 'data_file')), + ) + + class Meta: + model = ManagedFile + fields = ('data_source', 'data_file') + + def clean(self): + super().clean() + + if self.cleaned_data.get('upload_file') and self.cleaned_data.get('data_file'): + raise forms.ValidationError("Cannot upload a file and sync from an existing file") + + return self.cleaned_data diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py index 0ceb7dc6a..cbf29ae38 100644 --- a/netbox/core/models/data.py +++ b/netbox/core/models/data.py @@ -315,3 +315,14 @@ class DataFile(models.Model): self.data = f.read() return is_modified + + def write_to_disk(self, path, overwrite=False): + """ + Write the object's data to disk at the specified path + """ + # Check whether file already exists + if os.path.isfile(path) and not overwrite: + raise FileExistsError() + + with open(path, 'wb+') as new_file: + new_file.write(self.data) diff --git a/netbox/core/models/files.py b/netbox/core/models/files.py index 7fe98f64f..a080d30c8 100644 --- a/netbox/core/models/files.py +++ b/netbox/core/models/files.py @@ -71,3 +71,14 @@ class ManagedFile(SyncedDataMixin, models.Model): 'scripts': settings.SCRIPTS_ROOT, 'reports': settings.REPORTS_ROOT, }[self.file_root] + + def sync_data(self): + if self.data_file: + self.file_path = self.data_path + self.data_file.write_to_disk(self.full_path, overwrite=True) + + def delete(self, *args, **kwargs): + # Delete file from disk + os.remove(self.full_path) + + return super().delete(*args, **kwargs) diff --git a/netbox/core/urls.py b/netbox/core/urls.py index 32a286a4e..77c5724af 100644 --- a/netbox/core/urls.py +++ b/netbox/core/urls.py @@ -21,9 +21,6 @@ urlpatterns = ( # Managed files path('files/', views.ManagedFileListView.as_view(), name='managedfile_list'), - # path('files/add/', views.ManagedFileEditView.as_view(), name='managedfile_add'), - # path('files/edit/', views.ManagedFileBulkEditView.as_view(), name='managedfile_bulk_edit'), - # path('files/delete/', views.ManagedFileBulkDeleteView.as_view(), name='managedfile_bulk_delete'), path('files//', include(get_model_urls('core', 'managedfile'))), ) diff --git a/netbox/core/views.py b/netbox/core/views.py index 35f8f1dc0..8f1cc905a 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -128,35 +128,9 @@ class DataFileBulkDeleteView(generic.BulkDeleteView): class ManagedFileListView(generic.ObjectListView): queryset = ManagedFile.objects.all() - # filterset = filtersets.ManagedFileFilterSet - # filterset_form = forms.ManagedFileFilterForm table = tables.ManagedFileTable @register_model_view(ManagedFile) class ManagedFileView(generic.ObjectView): queryset = ManagedFile.objects.all() - - -# @register_model_view(ManagedFile, 'edit') -# class ManagedFileEditView(generic.ObjectEditView): -# queryset = ManagedFile.objects.all() -# form = forms.ManagedFileForm - - -@register_model_view(ManagedFile, 'delete') -class ManagedFileDeleteView(generic.ObjectDeleteView): - queryset = ManagedFile.objects.all() - - -# class ManagedFileBulkEditView(generic.BulkEditView): -# queryset = ManagedFile.objects.all() -# # filterset = filtersets.ManagedFileFilterSet -# table = tables.ManagedFileTable -# form = forms.ManagedFileBulkEditForm - - -# class ManagedFileBulkDeleteView(generic.BulkDeleteView): -# queryset = ManagedFile.objects.all() -# # filterset = filtersets.ManagedFileFilterSet -# table = tables.ManagedFileTable diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 6d9f78001..0990ecd8c 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -17,6 +17,10 @@ WEBHOOK_EVENT_TYPES = { EVENT_JOB_END: 'job_ended', } +# Managed files +REPORTS_ROOT_NAME = 'reports' +SCRIPTS_ROOT_NAME = 'scripts' + # Dashboard DEFAULT_DASHBOARD = [ { diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 6ddd30ebb..7420c9ab9 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -836,7 +836,7 @@ class Script(JobResultsMixin, WebhooksMixin, models.Model): managed = False -class ScriptModuleManager(models.Manager): +class ScriptModuleManager(models.Manager.from_queryset(RestrictedQuerySet)): def get_queryset(self): return super().get_queryset().filter(file_root='scripts') @@ -851,6 +851,10 @@ class ScriptModule(JobResultsMixin, WebhooksMixin, PythonModuleMixin, ManagedFil class Meta: proxy = True + def save(self, *args, **kwargs): + self.file_root = SCRIPTS_ROOT_NAME + return super().save(*args, **kwargs) + # # Reports @@ -864,7 +868,7 @@ class Report(JobResultsMixin, WebhooksMixin, models.Model): managed = False -class ReportModuleManager(models.Manager): +class ReportModuleManager(models.Manager.from_queryset(RestrictedQuerySet)): def get_queryset(self): return super().get_queryset().filter(file_root='reports') @@ -878,3 +882,7 @@ class ReportModule(JobResultsMixin, WebhooksMixin, PythonModuleMixin, ManagedFil class Meta: proxy = True + + def save(self, *args, **kwargs): + self.file_root = REPORTS_ROOT_NAME + return super().save(*args, **kwargs) diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index e9bea09aa..c62b256a5 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -96,17 +96,21 @@ urlpatterns = [ path('reports/', views.ReportListView.as_view(), name='report_list'), path('reports/results//', views.ReportResultView.as_view(), name='report_result'), re_path(r'^reports/(?P.([^.]+)).(?P.(.+))/', views.ReportView.as_view(), name='report'), + path('reports/add/', views.ScriptModuleCreateView.as_view(), name='reportmodule_add'), + path('reports//', include(get_model_urls('extras', 'reportmodule'))), + + # Scripts + path('scripts/', views.ScriptListView.as_view(), name='script_list'), + path('scripts/results//', views.ScriptResultView.as_view(), name='script_result'), + re_path(r'^scripts/(?P.([^.]+)).(?P.(.+))/', views.ScriptView.as_view(), name='script'), + path('scripts/add/', views.ScriptModuleCreateView.as_view(), name='scriptmodule_add'), + path('scripts//', include(get_model_urls('extras', 'scriptmodule'))), # Job results path('job-results/', views.JobResultListView.as_view(), name='jobresult_list'), path('job-results/delete/', views.JobResultBulkDeleteView.as_view(), name='jobresult_bulk_delete'), path('job-results//delete/', views.JobResultDeleteView.as_view(), name='jobresult_delete'), - # Scripts - path('scripts/', views.ScriptListView.as_view(), name='script_list'), - path('scripts/results//', views.ScriptResultView.as_view(), name='script_result'), - re_path(r'^scripts/(?P.([^.]+)).(?P.(.+))/', views.ScriptView.as_view(), name='script'), - # Markdown path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown") ] diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 091237cfc..4118a5d04 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.views.generic import View +from core.forms import ManagedFileForm from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.utils import get_widget_class from netbox.views import generic @@ -18,6 +19,7 @@ from utilities.utils import copy_safe_request, count_related, get_viewname, norm from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from . import filtersets, forms, tables from .choices import JobResultStatusChoices +from .constants import SCRIPTS_ROOT_NAME, REPORTS_ROOT_NAME from .forms.reports import ReportForm from .models import * from .reports import get_report, get_reports, run_report @@ -790,6 +792,21 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View): # Reports # +@register_model_view(ReportModule, 'edit') +class ReportModuleCreateView(generic.ObjectEditView): + queryset = ReportModule.objects.all() + form = ManagedFileForm + + def alter_object(self, obj, *args, **kwargs): + obj.file_root = REPORTS_ROOT_NAME + return obj + + +@register_model_view(ReportModule, 'delete') +class ReportModuleDeleteView(generic.ObjectDeleteView): + queryset = ReportModule.objects.all() + + class ReportListView(ContentTypePermissionRequiredMixin, View): """ Retrieve all of the available reports from disk and the recorded JobResult (if any) for each. @@ -819,6 +836,7 @@ class ReportListView(ContentTypePermissionRequiredMixin, View): ret.append((module, module_reports)) return render(request, 'extras/report_list.html', { + 'model': ReportModule, 'reports': ret, }) @@ -924,6 +942,21 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View): # Scripts # +@register_model_view(ScriptModule, 'edit') +class ScriptModuleCreateView(generic.ObjectEditView): + queryset = ScriptModule.objects.all() + form = ManagedFileForm + + def alter_object(self, obj, *args, **kwargs): + obj.file_root = SCRIPTS_ROOT_NAME + return obj + + +@register_model_view(ScriptModule, 'delete') +class ScriptModuleDeleteView(generic.ObjectDeleteView): + queryset = ScriptModule.objects.all() + + class GetScriptMixin: def _get_script(self, name, module=None): if module is None: @@ -957,6 +990,7 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View): script.result = results.get(script.full_name) return render(request, 'extras/script_list.html', { + 'model': ScriptModule, 'scripts': scripts, }) diff --git a/netbox/templates/extras/report_list.html b/netbox/templates/extras/report_list.html index 56b92c96d..64e99e660 100644 --- a/netbox/templates/extras/report_list.html +++ b/netbox/templates/extras/report_list.html @@ -1,5 +1,7 @@ {% extends 'base/layout.html' %} +{% load buttons %} {% load helpers %} +{% load perms %} {% block title %}Reports{% endblock %} @@ -11,6 +13,15 @@ {% endblock tabs %} +{% block controls %} +
+
+ {% block extra_controls %}{% endblock %} + {% add_button model %} +
+
+{% endblock controls %} + {% block content-wrapper %}
{% if reports %} diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index 1f34f4d5e..bfa8f0d0f 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -1,8 +1,18 @@ {% extends 'base/layout.html' %} +{% load buttons %} {% load helpers %} {% block title %}Scripts{% endblock %} +{% block controls %} +
+
+ {% block extra_controls %}{% endblock %} + {% add_button model %} +
+
+{% endblock controls %} + {% block tabs %}