A Ludus source is a versioned bundle of Packer templates, Ansible roles, and blueprints, served from a git repo, tarball, or local directory. ludus source add registers the contents in one step.
This repo is a starting point for publishing your own source. Use it as a template (or clone it and repoint origin at your new repo), edit the files, push, then run:
ludus source add https://github.com/<you>/<repo>
ludus blueprint apply <repo>/example # if your source ships a blueprint
ludus range deployAny git host works (GitHub, GitLab, self-hosted). You can also pass source add a local tarball (source add ./source.tar.gz) or a local directory (source add -d ./my-source). Full reference: Sources.
A source can carry any combination of three artifact types. All three are optional, but a source must ship at least one.
| Artifact | Where it goes | Visibility |
|---|---|---|
| Blueprints | blueprints/<id>/ |
Per-source, addressed as <sourceID>/<id> |
| Packer templates | templates/<n>/ at source root |
Global registry by name |
| Ansible roles | roles/<n>/ at source root |
User-scoped by default; --global-roles for instance-wide |
A blueprints-only source, a roles-only source, and a templates-only source are all valid.
LICENSE MIT placeholder; replace with your own
source.yml repo metadata: name, authors, homepage, license
scripts/validate.py manifest schema check; extend with your own rules
.github/workflows/validate.yml GitHub Actions: runs scripts/validate.py on every push
.gitlab-ci.yml GitLab CI: runs scripts/validate.py on every push
blueprints/example/ one blueprint
├── blueprint.yml display metadata
├── range-config.yml the range config
└── requirements.yml galaxy roles, collections, subscription roles
roles/ Ansible roles shipped by this source
templates/ Packer templates shipped by this source
The empty roles/ and templates/ directories are tracked with .gitkeep so the structure ships with the template. Drop a role or template in (or delete the directories you don't need).
scripts/validate.py and the two CI workflows ship a basic manifest check out of the box — it confirms your blueprint.yml parses and references resolve. If your org has its own CI conventions, delete scripts/ and .github/workflows/validate.yml (and/or .gitlab-ci.yml) and wire in your own; nothing else in the template depends on them.
blueprints/<id>/requirements.yml is the single manifest for everything a blueprint needs from outside the bundle. Every role referenced under roles: in range-config.yml must be declared here (or shipped locally under roles/); Ludus surfaces an undeclared-dependency warning at sync time otherwise.
Three sections, all optional:
roles:
- name: geerlingguy.docker
version: 7.4.4 # pin a galaxy role
- name: badsectorlabs.ludus_adcs # off-galaxy: name + src
src: https://github.com/badsectorlabs/ludus_adcs
version: v1.2.0
collections:
- name: community.crypto # required when range-config
version: 2.16.0 # references a FQCN role
# like community.crypto.openssl_certificate
subscription_roles:
- ludus_ghosts_client # license-gated role; bare scalar
- name: ludus_adcs # or structured shapeA few rules worth knowing:
- Names must match what
range-config.ymlreferences; otherwise Ludus installs one role and tries to run another. - Collections are required for FQCN role refs. A 3-part reference like
namespace.collection.rolewon't work unlessnamespace.collectionis listed undercollections:. - Subscription roles never travel in the bundle — only their names. At apply time Ludus serves them from the license catalog. If the target instance has no valid license, or the catalog doesn't cover one of the names, the apply returns
403. Version pinning isn't currently supported for subscription roles — whatever the catalog reports as current gets installed. - Local roles win over galaxy. If a
roles/<name>/directory exists in the bundle (per-blueprint or at source root), it satisfies the dependency without a galaxy lookup.
Each templates/<name>/ directory is a standard Ludus Packer template, the same shape as the Ludus template catalog:
templates/my-debian-base/
├── my-debian-base.pkr.hcl the Packer build config
├── http/ Linux: preseed.cfg / kickstart served at install time
└── Autounattend.xml Windows only: unattended install answer file
Templates register to a global, single-namespace pool. If two sources both register a template named my-debian-base, the second source add will conflict. Prefix shared template names with your source slug (bsl-debian-base, not debian-base).
After ludus source add, run ludus templates build to produce the VM image.
Each roles/<name>/ directory is a standard Ansible role:
roles/my_helper/
├── tasks/main.yml the role's tasks (typical entry point)
├── defaults/main.yml default variables
├── handlers/main.yml handlers
└── meta/main.yml role metadata, dependencies
Local roles are installed to the user's Ansible roles directory at source add time and are usable from any range-config thereafter — they don't need to be referenced from a blueprint in this source. When a blueprint does reference one, use the directory name (my_helper) under roles: in range-config.yml. If a local role shares a name with a galaxy role, Ludus skips the galaxy install and uses the local role.
The validator and the server enforce these:
source.yml:manifest_version. Everything else is optional. The whole file is optional too.blueprint.yml(when you ship a blueprint):manifest_version,id,name,description,version(semver),config. Optional:tags,thumbnail,min_ludus_version.
License, homepage, and authors live in source.yml and apply to every blueprint in the source.
The example files are annotated inline.
Two separate fields:
manifest_versionis the schema version of the manifest file. Ludus bumps it when the format changes incompatibly. Leave it at1.versionis your semver for the blueprint. Bump it any time you change a blueprint and want users to see it as new. Push to your repo, then users run:
ludus source sync <repo> # pull latest manifests + reinstall any new role deps
ludus blueprint info <repo>/example # see the new version
ludus blueprint apply <repo>/example # write the new config to their range
ludus range deploy # rebuildludus blueprint apply always writes whatever's currently in the source; there's no automatic upgrade prompt. The version field is for display and changelog purposes; pin to a git tag (source update <repo> --ref v1.2.0) to lock users to a specific release.
Full reference: Sources.