NodeRingsDocs

Hook Scripts

Restricted Python scripts that run after VM lifecycle events on your agent cluster

Hook scripts let you react after a customer VM is created, started, stopped, restarted, reinstalled, or deleted. You write one restricted Python script per event for your provider organisation. After a successful VM operation, your operator starts an isolated Kubernetes Job that runs that script.

Script source is stored in Vault on the mothership (Postgres keeps only hook, enabled, and timestamps). Treat the script as secret: webhook tokens in the source are likely. The operator still snapshots enabled scripts onto the VM custom resource for that operation.

They work on Proxmox, VirtFusion, and SolusVM 2. Hook failure never fails the VM operation.

Manage scripts at /infrastructure/hooks.

Edits apply to subsequent operations. The current power action already in flight keeps the scripts that were embedded when that order was generated.


When a hook runs

HookFires after
CreatedThe VM is provisioned
StartedThe VM is powered on
StoppedThe VM is powered off
RestartedThe VM is restarted
ReinstalledThe VM is reinstalled
DeletedThe VM is deleted

If the operator skips the VM operation (already done, generation already handled), the matching hook does not run. A reinstall operation uses the Reinstalled script.

Typical uses: POST to your billing or inventory webhook, notify an internal chat, or kick off your own automation. Keep work short: the Job has a 5-minute deadline.


Write and enable a script

Edit the event

Choose the row, write stdlib-only Python (64 KB max), and save. Saving creates the script if it did not exist.

Enable it

The enable switch is available after a script exists. Disable a hook to stop embedding it on later operations without deleting the source.

Delete removes the script for later operations. It cannot be undone.

API: HookScriptService_ListHookScripts, HookScriptService_UpsertHookScript, HookScriptService_DeleteHookScript.


Sandbox

Jobs never run inside the operator process. Each hook pod has no Kubernetes service-account token, runs as non-root, uses a read-only root filesystem, and cannot install packages (pip is not available).

LimitValue
LanguagePython 3 stdlib only
Size64 KB
Deadline5 minutes
IsolationJobs run in {helm-namespace}-hooks (for example virtfusion-system-hooks), not beside VM CRs or hypervisor credentials
ConcurrencyUp to 5 active hook Jobs in that hook namespace; extras are skipped

Default egress allows public HTTP/HTTPS and your private LAN / Tailscale ranges (any TCP port). Kubernetes pod and service CIDRs, cloud metadata (169.254.169.254), loopback, and multicast stay blocked so a script cannot reach the agent cluster or the mothership overlay. DNS is limited to cluster DNS. Carve-outs and extra destinations are chart values hooks.privateEgress and hooks.extraEgress.

Scripts are delivered to your agent cluster with the VM order. Customers never see them. Do not put hypervisor credentials or NodeRings tokens in a script. Prefer a public HTTPS webhook you control, or the optional provider secrets Secret on your cluster.


Environment

The Job sets these variables. Event JSON never includes the script source, secret refs, or cloud-init data.

VariableMeaning
NODERINGS_HOOKEvent name (created, started, stopped, restarted, reinstalled, deleted)
NODERINGS_VM_NAMEVM custom-resource name (for example NR-{uuid})
NODERINGS_VM_IDHypervisor VM or server id when known
NODERINGS_IPV4Primary IPv4 when the operator already knows it (Proxmox from staticIpConfig only — DHCP guests are empty; VirtFusion / SolusVM after the panel assigns it)
NODERINGS_IPV4_PREFIXPrimary IPv4 CIDR prefix length (for example 24). Proxmox from the order; VirtFusion / SolusVM from the panel when it publishes cidr / netmask
NODERINGS_IPV4_NETMASKDotted IPv4 netmask derived from that prefix (for example 255.255.255.0)
NODERINGS_IPV6Primary IPv6 when known
NODERINGS_IPV6_PREFIXPrimary IPv6 CIDR prefix length (for example 64)
NODERINGS_EVENT_JSONCurated JSON: hook, vmName, namespace, vmId, operation, ipv4, ipv6, gateways, prefixes, ipv4Netmask

Provider secrets (optional)

If your script needs credentials that must not live in NodeRings Vault, create an Opaque Secret named noderings-hook-secrets in the hooks namespace on your agent cluster.

Find the namespace on the agent (source of truth — do not invent it from the agent UUID):

export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
kubectl get ns | grep hooks

Default names after nr cluster register / nr cluster operator upgrade:

HypervisorHelm namespaceHooks namespace
Proxmoxproxmox-systemproxmox-system-hooks
VirtFusionvirtfusion-systemvirtfusion-system-hooks
SolusVM 2solusvm-systemsolusvm-system-hooks

Do not use <agent-uuid>-operator-hooks. That pattern is for Liqo VM CR namespaces, not hook Jobs.

# Replace the -n value with the namespace from `kubectl get ns | grep hooks`
kubectl create secret generic noderings-hook-secrets -n virtfusion-system-hooks \
  --from-literal=USERNAME=admin \
  --from-literal=PASSWORD='...'

If grep hooks shows nothing, upgrade first: nr cluster operator upgrade --org-id <org-uuid>, then list namespaces again. Org UUID is in the Create agent CLI command (--org-id) in the console.

When the Secret exists, every key is available as an environment variable and as a file under /var/run/secrets/noderings-hooks/. If it is missing, Jobs still run. Explicit NODERINGS_* variables always win over Secret keys with the same name. Keys must be valid env names (USERNAME, not user-name).

Prefer a webhook you control when possible. Do not put hypervisor credentials or NodeRings tokens in this Secret.

Example:

#!/usr/bin/env python3
import os

username = os.environ.get("USERNAME", "")
password = os.environ.get("PASSWORD", "")
# Prefer files when you want to avoid env leakage in process listings:
# password = open("/var/run/secrets/noderings-hooks/PASSWORD").read()

print(f"hook={os.environ.get('NODERINGS_HOOK')} user={username}")
# Use username/password with your HTTPS API (urllib, etc.).

Example: POST to a webhook

#!/usr/bin/env python3
import json
import os
import urllib.request

payload = {
    "event": os.environ.get("NODERINGS_EVENT_JSON", "{}"),
    "hook": os.environ.get("NODERINGS_HOOK", ""),
    "vm_name": os.environ.get("NODERINGS_VM_NAME", ""),
    "vm_id": os.environ.get("NODERINGS_VM_ID", ""),
    "ipv4": os.environ.get("NODERINGS_IPV4", ""),
    "ipv6": os.environ.get("NODERINGS_IPV6", ""),
    "ipv4_prefix": os.environ.get("NODERINGS_IPV4_PREFIX", ""),
    "ipv4_netmask": os.environ.get("NODERINGS_IPV4_NETMASK", ""),
    "ipv6_prefix": os.environ.get("NODERINGS_IPV6_PREFIX", ""),
}
req = urllib.request.Request(
    "https://hooks.example.com/noderings",
    data=json.dumps(payload).encode("utf-8"),
    headers={"Content-Type": "application/json"},
    method="POST",
)
with urllib.request.urlopen(req, timeout=15) as resp:
    resp.read()

How it is delivered

  1. You save an enabled script for a hook.
  2. On the next matching VM operation, NodeRings embeds enabled scripts into the VM custom resource (spec.hooks) and sends the order to your agent.
  3. After the operator finishes the hypervisor call successfully, it creates a Job named from the VM, operation, and generation so the same operation cannot run the hook twice.
  4. Result is recorded on the VM as status.lastHook and a Kubernetes Event. Job logs follow the existing Alloy scrape.

You will not see an execution-history table in the console in this version. Use your webhook receiver, or inspect Jobs and Events on the agent cluster.


Troubleshooting

SymptomWhat to check
Script never runsConfirm the switch is enabled, then trigger a new operation. Skipped (already-done) operations do not fire hooks.
Webhook not reachedPublic URLs must be http:// or https:// on port 80/443. Private RFC1918 / Tailscale targets are allowed except Kubernetes CIDRs. The Job deadline is 5 minutes. Jobs run python3.
Hook skipped under loadAt most 5 hook Jobs run at once in the hooks namespace; extras are skipped with a warning Event.
VM operation succeeded but hook failedExpected: hooks are fire-and-forget. Check Job logs in your hooks namespace and status.lastHook on the VM custom resource.

For agent or operator connectivity issues, see Troubleshooting and Agents.


Next