Skip to content

Custom runes

A custom rune is a plain pyinfra deploy module run with -r:

nullforge cast -i inventory.py -r ./runes/my_rune.py
nullforge cast -i inventory.py -r base -r ./runes/my_rune.py   # mix with built-ins, order preserved

Custom runes run in the same environment as built-in ones: host.data.system and host.data.features arrive as validated molds, and the full NullForge API - smithy helpers, templates, models - is importable. Keep them in your deployment repo next to the inventory.

Skeleton

runes/my_rune.py
"""One-line summary of what this rune deploys."""

from pyinfra.context import host
from pyinfra.operations import files, systemd

from nullforge.molds import FeaturesMold
from nullforge.smithy.packages import get_pm


def deploy_my_rune() -> None:
    features: FeaturesMold = host.data.features

    get_pm().install(name="Install my package", packages=["mypackage"], _sudo=True)

    config = files.put(
        name="Deploy my config",
        src="files/my.conf",
        dest="/etc/my/my.conf",
        mode="0644",
        _sudo=True,
    )

    systemd.service(service="my", restarted=True, _sudo=True, _if=config.did_change)
    systemd.service(service="my", running=True, enabled=True, _sudo=True)


deploy_my_rune()

The file must call its deploy function at module level - see how runes execute.

Reacting to features and host data

Everything the inventory attached is on host.data. Branch on other features' molds to compose with them, and on your own extra keys for role logic:

features: FeaturesMold = host.data.features

if features.haproxy.install:
    ...  # grant an automation user access to the haproxy config

if getattr(host.data, "zone", None) == "web":
    ...  # zone-specific provisioning

The deploy repo of the Conglomerate proxy fleet uses this shape for a repo-local automation rune: it provisions a CI user, then derives that user's sudoers entries and ACL grants from whichever features are active on each host.

Useful smithy helpers

Helper Use
smithy.packages.get_pm() Distro-mapped package install/update/upgrade (apt/dnf)
smithy.install.install_release_binary() Download -> sha256-verify -> extract -> install a release binary
smithy.versions.is_pinned_version_installed() Skip installs already at the pinned version
smithy.service.ensure_service_user() Dedicated system user + group for a service
smithy.admin.ensure_acl_access() Grant a user ACL access to a directory tree
smithy.network.has_ipv6() IPv6 connectivity fact
templates.get_template_path() / render_template() Jinja2 templates (your own paths work too)

Rules that keep casts stable

Custom runes follow the same conventions as built-ins - host.loop for operation-emitting loops, OperationMeta change-detection guards, and fact- or pin-guarded installs.