Maximilian Eschenbacher

Personal blog

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

  • netbox 3.6.9 reports and scripts
  • netbox 4.4.10

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.

Unlock Debian Bullseye natively encrypted ZFS-on-Root with Dropbear
2021-09-04 22:31

This very short post describes how to unlock Debian Bullseye with natively encrypted ZFS-on-Root. I'm assuming dropbear is installed and configured. No special configuration option is required.

Connect to the dropbear server and issue

zfsunlock

Configure grub to automatically install bootcode on all drives
2021-08-29 00:03

TIL the hard way how to configure grub to automatically install bootcode on all drives. Bootcode needs to be present on at least one drive. During package and operating system upgrades, the process of installing/updating bootcode is crucial if you expect the next system boot to be successful.

For software RAID configurations, such as mdadm or zfs, the bootcode must be installed on all 'boot' drives (i.e. drives which contain your / or /boot file systems) to avoid boot failures because

  • the machine randomly decides which disk to boot from
  • the disk with bootcode has been replaced
  • the HBA (host bus adapter) supports only a limited and/or hard coded number or position of drives

Grub provides a way to install bootcode manually. This method is well known and used in most tutorials as well as during Debian system installation:

grub-install /dev/sda
grub-install /dev/sdb

but there is also a way of performing the task automatically (at least on Debian):

dpkg-reconfigure grub-pc

By interactively selecting all boot disks, you lower your chances of boot failure, however, we need to make sure to run the either one of the commands after replacing a disk.

Warren Zevon - Frank and Jesse James - piano sheet music
2020-05-19 18:31

In 2017, I had discovered Warren Zevon and am listening to his music ever since. In fact, I had Frank and Jesse James transcribed as piano sheet music by mysheetmusictranscriptions.com. MySheetMusicTranscriptions granted me permission to share it within a small community for private (non-commercial) use only.

Read more…

ps aux | grep ...
2020-01-14 10:53

Have you ever used ps aux | grep ... on GNU/Linux to grep for a running process? Have you also used ps aux to view the whole process list on your terminal? At first glance, the two commands behave exactly the same, however ps will truncate every line to fit within your terminal's witdh of echo $COLUMNS (see the man page ps(1)). If you want wide output (132 columns), specify another w e.g. ps auxw. If you want unlimited output, specify yet another w e.g. ps auxww.

Read more…

Dropbear remote crypto unlock via IPv6
2019-12-31 17:04

This post will explain how to set up IPv6 connectivity via a initramfs script to remotely unlock your root partition on a server which uses a technology stack of dropbear (to be included in the intramfs) and cryptsetup via IPv6 and IPv6 only. Unlocking your root partition with this workflow is less secure than using the out-of-band management if you consider unattended hardware access of an attacker to your device as probable.

Version history

  • Initial release
  • 2021-06-12: Add section for IPv6-only
  • 2025-01-01: With Debian 12 (bookworm), the paths of the dropbear initramfs changed from /etc/dropbear-initramfs to /etc/dropbear/initramfs and Thorsten Glaser inspired me to use his scripts for creating a repo initramfs-ipv6. I'm currently working on using a lacp bond for unlocking, stay tuned.

Prerequisites

I am assuming you are successfully using dropbear to remotely unlock your root partition using cryptsetup via legacy-ip (IPv4).

Read more…

WireGuard and OpenVPN on the same port
2019-04-12 16:42

Accessing the internet through an always-on VPN full tunnel could be considered standard user behaviour these days. Things start to get annoying when the network (provider) limits VPN usage. For me, the most prominent case was an Eduroam wifi at a german university which grants only the absolute minimum of network access to its users, as required by the Eduroam specification. Here's the full list of mandatory (as in RFC MUST) unblocked ports (see page 32):

Read more…

Force linux to assume IPv6 unicast connectivity while assigned only IPv6 unique local addresses
2018-12-26 13:07

Problem

You are using linux with an IPv6 unique local address (ULA) but no IPv6 unicast address assigned to the interface but use them e.g. in a VPN context to provide IPv6 unicast connectivity through NAT. The same interface is also set up as the default route. This causes the default configuration of getaddrinfo(3) with ai_flags set to include AI_ADDRCONFIG to prefer IPv4 because missing IPv6 connectivity is assumed.

Solution

Read more…