Skip to main content

πŸ“¦ Sources

A source is a versioned bundle of blueprints, Packer templates, and Ansible roles and collections. ludus source add registers it and opens an interactive installer.

# Pick what to install
ludus source add https://github.com/badsectorlabs/ludus-source-bsl

# Script an install
ludus source add https://github.com/badsectorlabs/ludus-source-bsl --blueprints goad

What's in a Source Repo​

my-source-repo/
β”œβ”€β”€ README.md
β”œβ”€β”€ source.yml # repo-level metadata
β”œβ”€β”€ templates/ # Packer template configs
β”‚ └── win-server-2025/
β”œβ”€β”€ ansible/
β”‚ β”œβ”€β”€ roles/ # Ansible roles
β”‚ β”‚ └── shared_role/
β”‚ └── collections/ # Ansible collections
β”‚ └── my_namespace.my_collection/
└── blueprints/ # one directory per blueprint
└── goad/
β”œβ”€β”€ blueprint.yml # display metadata
β”œβ”€β”€ range-config.yml # the range config
β”œβ”€β”€ requirements.yml # required ansible roles, collections, or ludus subscription roles
└── thumbnail.png
Secure code execution

Packer templates and Ansible roles run on the Ludus host as the ludus user β€” with your Proxmox credentials in scope. Consider reviewing the repo before installing resources or pinning an immutable commit with --ref <commit-sha>.

Submodules​

Any asset subdirectory β€” a blueprint, template, role (ansible/roles/<name>/) or collection (ansible/collections/<dir>/) β€” can be a git submodule. When you register or sync a git-backed source, Ludus clones it with --recurse-submodules, so submodules are pulled (and refreshed on re-sync) automatically. This lets a source aggregate content that lives in its own repository while keeping that repo independent for issues and development.

Each submodule points at its upstream repository URL in .gitmodules (e.g. https://github.com/badsectorlabs/ludus_adcs.git); public repositories clone without credentials.

Common Workflows​

Register Someone Else's Source​

ludus source add https://github.com/badsectorlabs/ludus-source-bsl
ludus templates build
ludus blueprint apply badsectorlabs-ludus-source-bsl/goad
ludus range deploy

By default source add runs in two phases: it registers the source (clone or extract + walk), then opens an interactive picker for which blueprints, templates, and source-bundled roles and collections to install. The picker also lists the galaxy roles and collections a selected blueprint will pull in. Pass --all to skip the picker, or pass --blueprints/--templates/--source-roles/--source-collections to script the selection. In a non-TTY context (CI, piped stdin) add defaults to --all.

Templates are registered but not built; run ludus templates build separately.

Slug-prefixed IDs (badsectorlabs-ludus-source-bsl/goad) keep blueprints from different sources separate. If two sources both ship goad, they appear as badsectorlabs-ludus-source-bsl/goad and secteam-workshop-labs/goad. Apply by full prefix.

Fork to Edit Source Blueprints​

ludus blueprint create --from-blueprint badsectorlabs-ludus-source-bsl/goad --id scratch-pad
ludus blueprint config edit scratch-pad
ludus blueprint apply scratch-pad
ludus range deploy

Roles-Only or Templates-Only Sources​

A source doesn't need to ship blueprints. Register a roles-only or templates-only source the same way:

ludus source add https://github.com/foo/ludus-role-pack --all
# Roles installed for your user; no apply step.

Pick or Extend What's Installed​

Run source add <existing-sourceID> to open the picker against a source you already registered. Useful for installing additional items later, or for finishing a source whose picker you closed without committing.

ludus source add badsectorlabs-ludus-source-bsl                 # opens picker
ludus source add badsectorlabs-ludus-source-bsl --blueprints goad # scripted
ludus source add badsectorlabs-ludus-source-bsl --all # install everything in the catalog

Re-adding the same git URL is idempotent β€” Ludus re-pulls and refreshes the catalog without touching what's already installed. Re-adding the same sourceID with a different URL returns 409; pick a different sourceID, or repoint the existing source with ludus source set-url <sourceID> <git-url>.

Private Git Repos​

Ludus runs git clone under the ludus system user, inheriting whatever git auth that user has configured on the host β€” no Ludus-side flags or secret storage. Here is a recipe for setting up an SSH deploy key:

# terminal-command-host (run as root on the Ludus host)
sudo -u ludus -H bash -c '
mkdir -p ~/.ssh && chmod 700 ~/.ssh
cp /path/to/deploy_key ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan github.com >> ~/.ssh/known_hosts
'
# Then register the source with an SSH URL:
ludus source add git@github.com:owner/private-repo

Other git auth schemes (HTTPS credential helpers, ~/.git-credentials, SSH agents, etc.) also work in principle β€” anything you can make sudo -u ludus -H git clone <url> succeed with on the host will work for Ludus.

Caveats:

  • Host-wide: every Ludus user on the instance clones with the same credentials. For multi-tenant deployments where users need distinct git access, treat private sources as not yet supported β€” per-source credentials are not implemented.
  • The systemd unit applies ProtectHome=read-only, so the service can read /home/ludus/ but cannot write to it. Set up credentials from a root shell, not from inside the service.

Authoring a Source​

Publishing your own

Fork the Ludus Source Template to start your own.

Packer templates​

Each templates/<n>/ directory is a standard Ludus Packer template, the same shape as the templates in the Bad Sector Labs source:

templates/my-debian-base/
β”œβ”€β”€ my-debian-base.pkr.hcl # the Packer build config (incl. description + icon_path vars)
β”œβ”€β”€ icon.png # optional: the catalog icon referenced by the icon_path variable
β”œβ”€β”€ http/ # Linux: preseed.cfg / kickstart served at install time
└── Autounattend.xml # Windows only: unattended install answer file

Give the template a catalog description and icon with two static variables in the .pkr.hcl, so all of a template's metadata lives in one file:

variable "description" {
type = string
default = "Debian 12 minimal base image."
}

variable "icon_path" {
type = string
default = "icon.png"
}

description is the one-line summary shown when browsing the source via the TUI and GUI installers. icon_path is a relative path to an image bundled in the template dir β€” a square, transparent PNG (256Γ—256 works well) β€” shown on the template's card, with an OS glyph as the fallback. Packer requires a variable's default to be a literal, so both stay static strings.

Templates install per-user and persist across server updates. Each is keyed by the *-template name in its .pkr.hcl β€” the name ludus templates list reports β€” so re-installing a name you already have is a no-op, and a name that collides with a built-in template is rejected.

Run ludus templates build to produce the VM image β€” a separate step after source add. Built images are shared instance-wide by name, so one build makes a template usable by every range.

Ansible roles​

Each ansible/roles/<name>/ directory is a standard Ansible role:

ansible/roles/my_helper/
β”œβ”€β”€ tasks/main.yml # the role's tasks
β”œβ”€β”€ defaults/main.yml # default variables
β”œβ”€β”€ handlers/main.yml # handlers
β”œβ”€β”€ meta/version.yml # optional: defines the display version for the role in Ludus (overrides git tag detection)
└── meta/main.yml # role metadata; galaxy_info.description shows in the catalog

Ludus reads each role's meta/main.yml galaxy_info.description and shows it as the role's description in the catalog and picker.

Reference roles by directory name (my_helper) under roles: in any blueprint's range-config.yml. If a local role shares a name with a galaxy role, Ludus skips the galaxy / subscription role install and uses the local role.

Roles install per-user by default; admins can install globally via the TUI or use the --global flag on a scripted source add.

Ansible collections​

Each ansible/collections/<dir>/ directory is a standard Ansible collection β€” any directory with a galaxy.yml at its root:

ansible/collections/my_namespace.my_collection/
β”œβ”€β”€ galaxy.yml # namespace, name, version, description
β”œβ”€β”€ roles/ # collection roles
β”œβ”€β”€ plugins/ # modules, filters, lookups, etc.
└── playbooks/

The collection's identity is the namespace.name from its galaxy.yml β€” not the directory name. Ludus reads galaxy.yml for the version and description (shown in the catalog and picker) and installs it under the namespaced collections path, where a blueprint or range config references collection roles with fully-qualified names (my_namespace.my_collection.some_module).

Like roles, collections install per-user by default; admins can install globally via the TUI or use the --global flag on a scripted source add.

Blueprints​

Each blueprints/<id>/ directory holds one blueprint in the standard on-disk format: blueprint.yml (display metadata) and range-config.yml are required, plus requirements.yml when the blueprint has galaxy or subscription dependencies. See Blueprints: Directory Structure for the file formats.

Two rules are specific to sources:

  • Every role referenced under roles: in a blueprint's range-config.yml must be declared in its requirements.yml or vendored as a directory under the source's ansible/roles/ (collections likewise under ansible/collections/). Vendored copies win: they install from the source's pinned content and are never re-fetched from galaxy.
  • Subscription role bytes never travel with a source; only the names declared under subscription_roles:. The importing instance's license must cover them. See the Private Role Catalog.

source.yml at repo root​

Repo-level metadata used by ludus source list. License, homepage, and authors apply to the source as a whole; blueprints in the source inherit them.

manifest_version: 1
name: "My Lab Library"
description: "Production-ready labs"
authors:
- "Alice Anderson <alice@example.com>"
- "Bob Builder <bob@example.com>"
homepage: https://example.com/labs
license: MIT

When absent, Ludus defaults name to the derived sourceID and homepage to the git URL for git sources.

Local development workflow​

Develop your source locally β€” pass the directory via -d:

# First registration: tars and uploads the directory
ludus source add -d ./my-source-repo --id mysource

# After edits, push the new content
ludus source update mysource -d ./my-source-repo

When ready, push to a remote and switch to the git form.

ludus source rm mysource
ludus source add https://github.com/you/my-source-repo

Source IDs​

Every source gets a sourceID auto-derived from the URL or path when you run source add. Git URLs default to <org>-<repo> so two repos with the same name under different orgs don't collide. For example:

InputDerived sourceID
https://github.com/badsectorlabs/ludus-source-bslbadsectorlabs-ludus-source-bsl
https://github.com/badsectorlabs/ludus-source-bsl.gitbadsectorlabs-ludus-source-bsl
git@gitlab.com:secteam/workshop-labs.gitsecteam-workshop-labs
/tmp/my-source.tar.gzmy-source
/home/user/my-workshop-lab (directory)my-workshop-lab

Override it with --id for a shorter alias:

ludus source add https://github.com/badsectorlabs/ludus-source-bsl --id bsl
ludus blueprint apply bsl/goad

If you already have a source registered under the auto-derived ID, pass --id to give the new one a distinct alias. The same repo can be added twice to one account this way β€” useful for tracking different branches of the same upstream.

Sharing what's in a source​

Sources are personal β€” only the user who ran source add sees them in source list. To make a source's contents available to others, share each piece individually.

Templates install per-user. The built VM image is shared instance-wide by name, so building a template once makes it usable by every range; another user installs it from the source only to rebuild it.

Roles and collections install per-user. An admin can install them instance-wide by passing --global to source add, which makes them available to every user on the instance.

Blueprints share per-blueprint with ludus blueprint share user <sourceID>/<bpID> <userID> (or share group).

# Admin: register a source with global roles and collections for all users
ludus source add https://github.com/.../my-class --global

# Share each blueprint with the class group
ludus blueprint share group <sourceID>/lab-1 students
ludus blueprint share group <sourceID>/lab-2 students

Startup behavior​

On startup the Ludus server auto-registers the Bad Sector Labs source β€” owned by ROOT, visible to every admin β€” and re-syncs every registered source's catalog. Both run by default; disable either in /opt/ludus/config.yml:

register_default_source: false   # don't auto-register the Bad Sector Labs source
sync_sources_on_startup: false # don't re-sync registered sources on each startup

A source removed with ludus source rm is re-registered on the next restart unless register_default_source: false is set.

CLI Reference​

Source Management​

CommandDescription
ludus source add <url|tarball|directory|existing-sourceID>Register a new source or open the picker for an existing one (argument auto-detected)
ludus source list [<sourceID>]List registered sources, or show one source's metadata (--catalog to see what it ships instead)
ludus source sync [<sourceID>]Re-pull a git source's content and refresh its catalog (read-only β€” installs nothing)
ludus source set-url <sourceID> <git-url>Repoint a git source at a new remote URL (--ref to also switch the tracked ref)
ludus source update <sourceID> <tarball> (or -d <dir>)Push new content to an upload source
ludus source rm <sourceID>Remove a source's registration and blueprints (installed templates, roles, and collections stay on disk)

Installing is additive β€” re-running ludus source add only ever adds to what's installed, and each install acts only on the items you name. To uninstall, remove items with the per-artifact commands: ludus blueprint rm <sourceID>/<blueprint>, ludus templates rm -n <name>, ludus ansible role rm <name>, or ludus ansible collection rm <fqcn>. Each one also releases the source's claim on the artifact, and nothing reinstalls removed items behind your back (re-running ludus source add <sourceID> --all reinstates everything).

Blueprint Commands (Extended for Sources)​

CommandDescription
ludus blueprint listList local and source blueprints; --tag <tag> filters by tag
ludus blueprint apply <id>Apply a local blueprint or a source blueprint (bsl/goad)
ludus blueprint install <id>Install one blueprint's role dependencies
ludus blueprint info <id>Show metadata and dependency status

Useful Flags​

FlagAvailable onDescription
--allsource addSkip the picker; install everything the source ships
--blueprints <ids>source addScripted selection: blueprint IDs to install (CSV or repeated)
--templates <names>source addScripted selection: template names to install (CSV or repeated)
--source-roles <names>source addScripted selection: source-bundled role names to install (CSV or repeated)
--source-collections <fqcns>source addScripted selection: source-bundled collection FQCNs to install (CSV or repeated)
--globalsource add, source sync, source update, blueprint installAdmin only. Install the source's roles and collections instance-wide instead of user-scoped
--forcesource add, source sync, source updateOverwrite already-installed templates and galaxy/local roles
--force-rolesblueprint installOverwrite already-installed galaxy/local roles
--id <sourceID>source addOverride the auto-derived sourceID
--ref <ref>source add, source set-urlGit branch, tag, or commit to track