Netbox reports and scripts through the years

2026-01-27 16:57

I'm managing a netbox version 3.6.9. The version is quite old, however, we're running it privately with only a few humans having access and without the need for any new features. Recently, the netbox branching plugin was announced and now we're in the process of upgrading.

We use reports (this links to the docs of version 3.7 because I could not find 3.6 docs) for formulating a few invariants which we want to abide to. E.g. naming conventions, duplicate address and prefix detection, etc.

We know that reports are being deprecated by netbox version 4 and users are advised to convert them to scripts. This is why we set up a staging version running 4.4.10 and developed a way to run our reports with both

The result is a sort of compatibility layer which you can use in your reports file:

from extras.scripts import Script
# TODO Report may become unavailable at some point because it is deprecated in netbox 4
from extras.reports import Report
from netbox.settings import VERSION
import inspect

# most of the strange code in here is for implementing compatibility with netbox Reports and
# Script through a wide range of netbox versions from 3.6.9 to 4.4.10. We initially
# implemented both Reports and Scripts to be used with netbox. We started using reports as
# they produce more structured output. Netbox 4 deprecated the Report but retained backwards
# compatibility, however even the Script class logging methods have changed between Netbox 3
# and 4: log(message) -> log(msg=message, obj=o). With the current netbox, the scripts remain
# as the only possible method and dramatically improved readability in the mean time.
# Everything is good :) Except for this abstraction.

# figure out if we're supposed to provide Script or Report
BaseModel = None
for s in inspect.stack():
    if s.filename.endswith('extras/models/scripts.py'):
        # we're called through netbox scripts
        BaseModel = Script
        break
    elif s.filename.endswith('extras/models/reports.py'):
        # we're called through netbox reports
        BaseModel = Report
        break
if not BaseModel:
    # we're called through migrations
    if int(VERSION.split('.')[0]) <= 3:
        BaseModel = Report
    else:
        BaseModel = Script


class CustomBase(BaseModel):

    """
    provide our abstraction for implementation of the log_* methods for the three different
    types.

    Counting all combinations of Report and Script during their lifetimes, log_* methods may
    have been called with several argument options (log* means log_info, log_warning, etc.):

    log*(msg) used by Scripts with netbox <= 3
    log*(obj) used by Reports
    log*(obj, msg) used by Reports
    log*(msg=msg, obj=obj) Scripts with netbox >= 4
    log*(obj=obj) Scripts with netbox >= 4
    log*(msg, obj) Scripts with netbox >= 4

    use this signature:
    log*(msg=msg, obj=obj)

    reading the documentation for Scripts, msg may be None for log_success.
    """

    # _log is already defined from Scripts and Reports (made that mistake).
    def __log(self, typ, *args, **kwargs):
        method = getattr(super(), f'log_{typ}')
        if isinstance(self, Report):
            # for Report, log requires obj, message
            obj = kwargs.get('obj', args[1] if len(args) == 2 else None)
            msg = kwargs.get('msg', args[0] if len(args) >= 1 else None)
            method(obj, msg)
        elif isinstance(self, Script):
            if int(VERSION.split('.')[0]) <= 3:
                # pre 4 versions used only a single string
                method(' '.join([ str(x) for x in list(args) + list(kwargs.values()) ]))
            else:
                # post 4 use msg=msg, obj=obj
                method(*args, **kwargs)

    def log_debug(self, *args, **kwargs):
        self.__log('debug', *args, **kwargs)

    def log_success(self, *args, **kwargs):
        self.__log('success', *args, **kwargs)

    def log_info(self, *args, **kwargs):
        self.__log('info', *args, **kwargs)

    def log_warning(self, *args, **kwargs):
        self.__log('warning', *args, **kwargs)

    def log_failure(self, *args, **kwargs):
        self.__log('failure', *args, **kwargs)

    def run(self, *args, **kwargs):
        """
        only netbox 3.x scripts require the run method but since we define it here, we need to
        cover the two other cases: reports and scripts with netbox >= 4
        """
        if isinstance(self, Report):
            super().run(*args, **kwargs)
            return
        elif VERSION.startswith('3.'):
            # Netbox3 Script really needs the run() method and raises an exception otherwise
            if (method := getattr(self, 'pre_run', None)):
                # only run if pre_run exists
                method()
            for name, method in inspect.getmembers(self, predicate=inspect.ismethod):
                if not name.startswith('test_'): continue
                method()
            if (method := getattr(self, 'post_run', None)):
                # only run if post_run exists
                method()
        else:
            super().run(*args, **kwargs)

# start your reporting classes here

It's not pretty but it works.

Howto use it:

Either place it within your file or create a own file e.g. custom_base.py and create it alongside your main file(s) as either report or script.

Use the logging signature as

log*(msg=msg, obj=obj)

Where log* is one of the logging functions.