Repository: netbox-community/netbox

Issue:
Running report via management command results in missing argument 'job_result'
### Environment
* Python version: Python 3.7.9
* NetBox version: 2.9.3

### Steps to Reproduce
I'm following the exact instructions on https://netbox.readthedocs.io/en/stable/additional-features/reports/ to create an (example) report.

1. Create a file `DeviceConnectionsReport.py` in the Netbox reports directory
2. Paste the example code from https://netbox.readthedocs.io/en/stable/additional-features/reports/
3. Run `python3 manage.py runreport DeviceConnectionsReport`

### Expected Behavior
I would expect the example report to run and produce results.

### Observed Behavior
```
$ python3 manage.py runreport DeviceConnectionsReport
[17:50:53] Running DeviceConnectionsReport.DeviceConnectionsReport...
Traceback (most recent call last):
  File "manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 401, in execute_from_command_line
    utility.execute()
  File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 395, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 330, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 371, in execute
    output = self.handle(*args, **options)
  File "/opt/netbox/netbox/extras/management/commands/runreport.py", line 27, in handle
    report.run()
TypeError: run() missing 1 required positional argument: 'job_result'
```

_PS: I ended up trying via the commandline, because in the webinterface and API the report (also) fails._


Discussion:
@lampwins can you look into this please?
Yep, I will take this. I forgot this management command exists. Should just have to modify it to follow the same execution pattern and wait on the results.

Resolving patch:
diff --git a/netbox/extras/management/commands/runreport.py b/netbox/extras/management/commands/runreport.py
--- a/netbox/extras/management/commands/runreport.py
+++ b/netbox/extras/management/commands/runreport.py
@@ -1,7 +1,12 @@
+import time
+
+from django.contrib.contenttypes.models import ContentType
 from django.core.management.base import BaseCommand
 from django.utils import timezone
 
-from extras.reports import get_reports
+from extras.choices import JobResultStatusChoices
+from extras.models import JobResult
+from extras.reports import get_reports, run_report
 
 
 class Command(BaseCommand):
@@ -24,11 +29,29 @@ def handle(self, *args, **options):
                     self.stdout.write(
                         "[{:%H:%M:%S}] Running {}...".format(timezone.now(), report.full_name)
                     )
-                    report.run()
+
+                    report_content_type = ContentType.objects.get(app_label='extras', model='report')
+                    job_result = JobResult.enqueue_job(
+                        run_report,
+                        report.full_name,
+                        report_content_type,
+                        None
+                    )
+
+                    # Wait on the job to finish
+                    while job_result.status not in JobResultStatusChoices.TERMINAL_STATE_CHOICES:
+                        time.sleep(1)
+                        job_result = JobResult.objects.get(pk=job_result.pk)
 
                     # Report on success/failure
-                    status = self.style.ERROR('FAILED') if report.failed else self.style.SUCCESS('SUCCESS')
-                    for test_name, attrs in report.result.data.items():
+                    if job_result.status == JobResultStatusChoices.STATUS_FAILED:
+                        status = self.style.ERROR('FAILED')
+                    elif job_result == JobResultStatusChoices.STATUS_ERRORED:
+                        status = self.style.ERROR('ERRORED')
+                    else:
+                        status = self.style.SUCCESS('SUCCESS')
+
+                    for test_name, attrs in job_result.data.items():
                         self.stdout.write(
                             "\t{}: {} success, {} info, {} warning, {} failure".format(
                                 test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failure']
@@ -37,6 +60,9 @@ def handle(self, *args, **options):
                     self.stdout.write(
                         "[{:%H:%M:%S}] {}: {}".format(timezone.now(), report.full_name, status)
                     )
+                    self.stdout.write(
+                        "[{:%H:%M:%S}] {}: Duration {}".format(timezone.now(), report.full_name, job_result.duration)
+                    )
 
         # Wrap things up
         self.stdout.write(


Test patch: