When the left4me bundle was first integrated, ovh.left4me's node
file carried ~40 lines of left4me-related metadata (git_url,
secret_key, full nginx vhost, monitoring, backups, nftables
rules). The maintainer pushed back: per-node metadata should be
only what genuinely varies per host. Refactor brought it down to
{'domain': 'left4.me'} with everything else in bundle defaults
or in a reactor deriving from the domain.
Add the rule to bundles/AGENTS.md from the bundle-author angle
(use defaults / vault-keyed-on-node for secrets, cite left4me
and postgresql for the established pattern). Add the reviewer's
form to nodes/AGENTS.md Pitfalls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3.8 KiB
3.8 KiB
nodes/
What's here
One file per node, ~22 in total. Each file is a single Python expression (a dict literal) describing one machine: hostname, groups, bundles, and metadata.
Loader mechanism
nodes.py (top of the repo) walks nodes/, reads each *.py file,
runs eval() on its content, and then demagifies the result —
resolving any !password_for:, !decrypt:, !decrypt_file:, and
!32_random_bytes_as_base64_for: magic strings via repo.vault. See
docs/agents/conventions.md#secrets
for the magic-string list.
This loader shape has consequences:
- No top-level imports. The file must be a single expression. No
import os, nodef, noif. Userepo.libs.<x>from bundle code if you need a helper. - Silent drop on parse failure. Vanilla bundlewrap omits a node
whose file fails to eval. The maintainer's
groups.pywas patched in commitdc40295to print the error; the node-loader prints onnodes.pyerrors via the same shape. Symptom either way:bw nodeslists fewer nodes than expected.
Conventions
- Filename = node name.
home.server.pydefines the nodehome.server. - Naming pattern:
<location>.<role>.py. Examples:home.server.py,htz.mails.py,ovh.left4me.py,mseibert.freescout.py. *.py_parks a node without loading it. Used to keep decommissioned-but-not-deleted configs in tree (e.g.htz.l4d2.py_). The loader only matches*.py.- Magic strings only resolve here.
!password_for:etc. work in node files; in groups, bundles, or items they don't — callrepo.vault.<verb>(...)directly there.
How to add a new node
-
Create
nodes/<location>.<role>.pywith a single dict expression:{ 'id': 'a-uuid-or-stable-name', 'hostname': '<dns-name-or-ip>', 'groups': { 'debian-13', 'monitored', # … }, 'bundles': { # only bundles not provided by the groups above }, 'metadata': { # node-local overrides and required keys }, } -
Add to relevant
groups/<axis>/<x>.pyif group membership is the attachment point (preferred over per-nodebundleslists). -
Verify:
bw nodes— your node should appear.bw nodes <node> -a groups— confirm group membership resolved as expected (bw groups -n <node>does not exist).bw metadata <node>— confirm merged metadata.
Pitfalls
- Renaming a node renames the node. Vault entries (anything keyed
on
!password_for:<node>),bw hashrecords, and ssh known_hosts associations all key on node name. Search-and-replace before renaming, or vault lookups silently return new (wrong) values. - Don't restore
_oldor*.py_files without checkingconventions.md#suspension-and-soft-delete-idioms. These are intentional parks/buffers, not bugs. idmust be unique. A pre-apply hook (hooks/unique_node_ids.py) enforces this; duplicate IDs failbw testandbw apply.- Bloated per-node metadata is usually a bundle smell. If a
bundle's metadata block in the node file has more than 3-5 keys,
the bundle is probably under-using
defaults/ reactors. Push the contribution into the bundle (seebundles/AGENTS.md) rather than growing the node file.
See also
groups/AGENTS.md— group-membership patterns, how metadata merges along the chain.docs/agents/conventions.md— demagify magic-strings, naming, eval-loader constraints.- Fork's
AGENTS.md— node attribute reference (hostname,username,dummy, etc.).