Просмотр исходного кода

Merge pull request #10290 from kkthxbye-code/10258-nested-module-scripts

Fixes #10258 - Allow running scripts nested in modules/packages
Jeremy Stretch 3 лет назад
Родитель
Сommit
7b4f5252f1

+ 4 - 4
netbox/extras/api/views.py

@@ -159,7 +159,7 @@ class ReportViewSet(ViewSet):
         # Read the PK as "<module>.<report>"
         # Read the PK as "<module>.<report>"
         if '.' not in pk:
         if '.' not in pk:
             raise Http404
             raise Http404
-        module_name, report_name = pk.split('.', 1)
+        module_name, report_name = pk.split('.', maxsplit=1)
 
 
         # Raise a 404 on an invalid Report module/name
         # Raise a 404 on an invalid Report module/name
         report = get_report(module_name, report_name)
         report = get_report(module_name, report_name)
@@ -183,8 +183,8 @@ class ReportViewSet(ViewSet):
         }
         }
 
 
         # Iterate through all available Reports.
         # Iterate through all available Reports.
-        for module_name, reports in get_reports():
-            for report in reports:
+        for module_name, reports in get_reports().items():
+            for report in reports.values():
 
 
                 # Attach the relevant JobResult (if any) to each Report.
                 # Attach the relevant JobResult (if any) to each Report.
                 report.result = results.get(report.full_name, None)
                 report.result = results.get(report.full_name, None)
@@ -257,7 +257,7 @@ class ScriptViewSet(ViewSet):
     lookup_value_regex = '[^/]+'  # Allow dots
     lookup_value_regex = '[^/]+'  # Allow dots
 
 
     def _get_script(self, pk):
     def _get_script(self, pk):
-        module_name, script_name = pk.split('.')
+        module_name, script_name = pk.split('.', maxsplit=1)
         script = get_script(module_name, script_name)
         script = get_script(module_name, script_name)
         if script is None:
         if script is None:
             raise Http404
             raise Http404

+ 2 - 2
netbox/extras/management/commands/runreport.py

@@ -21,8 +21,8 @@ class Command(BaseCommand):
         reports = get_reports()
         reports = get_reports()
 
 
         # Run reports
         # Run reports
-        for module_name, report_list in reports:
-            for report in report_list:
+        for module_name, report_list in reports.items():
+            for report in report_list.values():
                 if module_name in options['reports'] or report.full_name in options['reports']:
                 if module_name in options['reports'] or report.full_name in options['reports']:
 
 
                     # Run the report and create a new JobResult
                     # Run the report and create a new JobResult

+ 17 - 10
netbox/extras/reports.py

@@ -26,20 +26,18 @@ def get_report(module_name, report_name):
     """
     """
     Return a specific report from within a module.
     Return a specific report from within a module.
     """
     """
-    file_path = '{}/{}.py'.format(settings.REPORTS_ROOT, module_name)
+    reports = get_reports()
+    module = reports.get(module_name)
 
 
-    spec = importlib.util.spec_from_file_location(module_name, file_path)
-    module = importlib.util.module_from_spec(spec)
-    try:
-        spec.loader.exec_module(module)
-    except FileNotFoundError:
+    if module is None:
         return None
         return None
 
 
-    report = getattr(module, report_name, None)
+    report = module.get(report_name)
+
     if report is None:
     if report is None:
         return None
         return None
 
 
-    return report()
+    return report
 
 
 
 
 def get_reports():
 def get_reports():
@@ -52,7 +50,7 @@ def get_reports():
         ...
         ...
     ]
     ]
     """
     """
-    module_list = []
+    module_list = {}
 
 
     # Iterate through all modules within the reports path. These are the user-created files in which reports are
     # Iterate through all modules within the reports path. These are the user-created files in which reports are
     # defined.
     # defined.
@@ -61,7 +59,16 @@ def get_reports():
         report_order = getattr(module, "report_order", ())
         report_order = getattr(module, "report_order", ())
         ordered_reports = [cls() for cls in report_order if is_report(cls)]
         ordered_reports = [cls() for cls in report_order if is_report(cls)]
         unordered_reports = [cls() for _, cls in inspect.getmembers(module, is_report) if cls not in report_order]
         unordered_reports = [cls() for _, cls in inspect.getmembers(module, is_report) if cls not in report_order]
-        module_list.append((module_name, [*ordered_reports, *unordered_reports]))
+
+        module_reports = {}
+
+        for cls in [*ordered_reports, *unordered_reports]:
+            # For reports in submodules use the full import path w/o the root module as the name
+            report_name = cls.full_name.split(".", maxsplit=1)[1]
+            module_reports[report_name] = cls
+
+        if module_reports:
+            module_list[module_name] = module_reports
 
 
     return module_list
     return module_list
 
 

+ 7 - 1
netbox/extras/scripts.py

@@ -299,6 +299,10 @@ class BaseScript:
     def module(cls):
     def module(cls):
         return cls.__module__
         return cls.__module__
 
 
+    @classmethod
+    def root_module(cls):
+        return cls.__module__.split(".")[0]
+
     @classproperty
     @classproperty
     def job_timeout(self):
     def job_timeout(self):
         return getattr(self.Meta, 'job_timeout', None)
         return getattr(self.Meta, 'job_timeout', None)
@@ -514,7 +518,9 @@ def get_scripts(use_names=False):
         ordered_scripts = [cls for cls in script_order if is_script(cls)]
         ordered_scripts = [cls for cls in script_order if is_script(cls)]
         unordered_scripts = [cls for _, cls in inspect.getmembers(module, is_script) if cls not in script_order]
         unordered_scripts = [cls for _, cls in inspect.getmembers(module, is_script) if cls not in script_order]
         for cls in [*ordered_scripts, *unordered_scripts]:
         for cls in [*ordered_scripts, *unordered_scripts]:
-            module_scripts[cls.__name__] = cls
+            # For scripts in submodules use the full import path w/o the root module as the name
+            script_name = cls.full_name.split(".", maxsplit=1)[1]
+            module_scripts[script_name] = cls
         if module_scripts:
         if module_scripts:
             scripts[module_name] = module_scripts
             scripts[module_name] = module_scripts
 
 

+ 3 - 3
netbox/extras/urls.py

@@ -1,4 +1,4 @@
-from django.urls import path
+from django.urls import path, re_path
 
 
 from extras import models, views
 from extras import models, views
 from netbox.views.generic import ObjectChangeLogView
 from netbox.views.generic import ObjectChangeLogView
@@ -100,12 +100,12 @@ urlpatterns = [
 
 
     # Reports
     # Reports
     path('reports/', views.ReportListView.as_view(), name='report_list'),
     path('reports/', views.ReportListView.as_view(), name='report_list'),
-    path('reports/<str:module>.<str:name>/', views.ReportView.as_view(), name='report'),
     path('reports/results/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'),
     path('reports/results/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'),
+    re_path(r'^reports/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ReportView.as_view(), name='report'),
 
 
     # Scripts
     # Scripts
     path('scripts/', views.ScriptListView.as_view(), name='script_list'),
     path('scripts/', views.ScriptListView.as_view(), name='script_list'),
-    path('scripts/<str:module>.<str:name>/', views.ScriptView.as_view(), name='script'),
     path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
     path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
+    re_path(r'^scripts/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ScriptView.as_view(), name='script'),
 
 
 ]
 ]

+ 4 - 3
netbox/extras/views.py

@@ -534,9 +534,10 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
         }
         }
 
 
         ret = []
         ret = []
-        for module, report_list in reports:
+
+        for module, report_list in reports.items():
             module_reports = []
             module_reports = []
-            for report in report_list:
+            for report in report_list.values():
                 report.result = results.get(report.full_name, None)
                 report.result = results.get(report.full_name, None)
                 module_reports.append(report)
                 module_reports.append(report)
             ret.append((module, module_reports))
             ret.append((module, module_reports))
@@ -613,7 +614,7 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View):
         result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type)
         result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type)
 
 
         # Retrieve the Report and attach the JobResult to it
         # Retrieve the Report and attach the JobResult to it
-        module, report_name = result.name.split('.')
+        module, report_name = result.name.split('.', maxsplit=1)
         report = get_report(module, report_name)
         report = get_report(module, report_name)
         report.result = result
         report.result = result
 
 

+ 1 - 1
netbox/templates/extras/script_list.html

@@ -34,7 +34,7 @@
                 {% for class_name, script in module_scripts.items %}
                 {% for class_name, script in module_scripts.items %}
                   <tr>
                   <tr>
                     <td>
                     <td>
-                      <a href="{% url 'extras:script' module=script.module name=class_name %}" name="script.{{ class_name }}">{{ script.name }}</a>
+                      <a href="{% url 'extras:script' module=script.root_module name=class_name %}" name="script.{{ class_name }}">{{ script.name }}</a>
                     </td>
                     </td>
                     <td>
                     <td>
                       {% include 'extras/inc/job_label.html' with result=script.result %}
                       {% include 'extras/inc/job_label.html' with result=script.result %}