A powerful and flexible Ansible dynamic inventory script for LXD that supports multiple endpoints, advanced filtering, and hostname customization.
This script was created after running into too many limitations of community.general.lxd inventory. Using many projects on multiple clusters, I ended up with over 20 inventory files. This has now been reduced to 1.
Should also work fine with Incus!
Feel free to request additional features.
- Multi-Endpoint Support: Connect to multiple LXD servers in a single inventory run
- Tag-based Filtering: Filter instances using
user.*configuration keys - Flexible Hostname Formatting: Customize how hostnames appear in your inventory (
{name}.{project}.{endpoint}.example.com) - Custom Group Formatting: Customize Ansible group names with templates
- User-Defined Groups: Create custom groups using
user.ansible_groupsconfiguration - Project and Profile Filtering: Filter by LXD projects and profiles
- Network Interface Control: Configure IP address selection with interface filtering and IPv4/6 preference
- Status and Type Filtering: Include/exclude based on instance status and type
- Regex-based Name Filtering: Exclude instances based on names with support for regex
- Bearer Token Authentication: Per-endpoint tokens via environment variables — CI/CD friendly (GitHub Actions secrets map 1:1 to clusters)
- Unix Socket Support: Local LXD daemon connections
- Debug Mode: Detailed logging for troubleshooting
pip install PyYAML requests urllib3For Unix socket connections (local LXD):
pip install requests-unixsocketgit clone https://github.com/vosdev/ansible-lxd-inventory
chmod +x ansible-lxd-inventory/lxd_inventory.pyCreate lxd_inventory.yml:
global_defaults:
verify_ssl: false
filters:
status: [running, stopped]
type: [container, virtual-machine]
projects: [all]
lxd_endpoints:
local:
endpoint: "unix:///var/lib/lxd/unix.socket"
remote:
endpoint: "https://lxd.example.com:8443"
# Bearer token is read from the LXD_TOKEN_REMOTE environment variable
# (derived from the endpoint name). Or name the env var explicitly:
# token_env: MY_LXD_TOKENexport LXD_TOKEN_REMOTE="<bearer token>"# List all instances
./lxd_inventory.py --list
# Get specific instance details
./lxd_inventory.py --instance mycontainer
# Debug mode
./lxd_inventory.py --list --debug# Run playbook using dynamic inventory
ansible-playbook -i lxd_inventory.py playbook.yml
# List hosts in inventory
ansible-inventory -i lxd_inventory.py --listHTTPS endpoints authenticate with LXD bearer tokens. Unix socket endpoints need no token.
Each endpoint reads its token from an environment variable, resolved in this order:
- The env var named by the endpoint's
token_envconfig key (ortoken_envinglobal_defaults) LXD_TOKEN_<ENDPOINT_NAME>— the endpoint name uppercased, with non-alphanumeric characters replaced by underscores (e.g. endpointproduction→LXD_TOKEN_PRODUCTION,my-lab→LXD_TOKEN_MY_LAB)
Tokens are never placed in the YAML config, so inventory files stay committable and each cluster's token can live in its own CI secret (e.g. GitHub Actions secrets mapping 1:1 to clusters).
If an HTTPS endpoint has no token in its environment the script exits with an error naming the missing variable — a misconfigured secret breaks the run visibly instead of silently dropping a cluster from the inventory.
# Create a bearer identity (optionally add it to a group with the right permissions)
lxc auth identity create bearer/ansible-inventory
lxc auth identity token issue bearer/ansible-inventory --expiry=1y
# Tokens expire per core.bearer_token_expiry - set a sensible default
lxc config set core.bearer_token_expiry 90dGrant the identity read access via LXD's fine-grained authorization (e.g. a group with viewer entitlements on the relevant projects).
lxd_endpoints:
production:
endpoint: "https://prod-lxd:8443"
# reads LXD_TOKEN_PRODUCTION by default; override with:
token_env: LXD_PROD_TOKENMigrating from TLS client certificates? Certificate authentication (
cert_path,key_path,ca_cert_pathand theLXD_CERT*/LXD_KEY*environment variables) has been removed. The script exits with an error if the old config keys are still present. Server certificate verification is still controlled byverify_ssl.
To allow for multiple instances of the script in restricted environments (e.g. SemaphoreUI, AWX or Tower), the script searches for configuration files in this order based on it's file name. e.g., lxd_inventory_dev.py -> lxd_inventory_dev.yml in the following locations:
<script_dir>/{base_name}.yml<script_dir>/{base_name}.yaml./{base_name}.yml./{base_name}.yaml~/.config/{base_name}.yml~/.config/{base_name}.yaml/etc/{base_name}.yml/etc/{base_name}.yaml
<script_dir> is the directory the script — or the symlink that invoked it — actually lives in (resolved without following the symlink). This is searched first so you can keep a per-instance config beside each symlink and have it load no matter which directory you run ansible-playbook from. For example, with a git-tracked layout:
inventories/prod/dynamic-inventory.py # symlink to lxd_inventory.py
inventories/prod/dynamic-inventory.yml # this instance's config
running ansible-playbook --inventory inventories/prod/dynamic-inventory.py ... from the repo root loads inventories/prod/dynamic-inventory.yml automatically.
If those are not found, it will fallback to the default config filename in the following locations:
--config /path/to/config.yml(CLI argument)<script_dir>/lxd_inventory.yml<script_dir>/lxd_inventory.yaml./lxd_inventory.yml./lxd_inventory.yaml~/.config/lxd_inventory.yml~/.config/lxd_inventory.yaml/etc/lxd_inventory.yml/etc/lxd_inventory.yaml
global_defaults:
# SSL settings - server certificate verification
verify_ssl: true
# Bearer tokens are read from environment variables - see Authentication.
# Optionally set token_env here to use one env var for all endpoints:
# token_env: LXD_TOKEN
# Hostname formatting template
hostname_format: "{name}"
# Group formatting templates
group_formats:
project: "lxd_project_{project}" # Default format
profile: "lxd_profile_{profile}"
endpoint: "lxd_endpoint_{endpoint}"
status: "lxd_{status}"
type: "lxd_{type}"
# User-defined groups
user_groups:
enabled: true # Enable user groups (default)
key: "user.ansible_groups" # Configuration key to check
# Default filters
filters:
status: [running, stopped, frozen, error]
type: [container, virtual-machine]
projects: [all]
exclude_projects: []
profiles: []
tags: {}
ignore_interfaces: [lo, docker0, lxdbr0]
prefer_ipv6: false
exclude_names: []
lxd_endpoints:
endpoint_name:
endpoint: "https://lxd.example.com:8443"
# Bearer token env var (default: LXD_TOKEN_ENDPOINT_NAME)
token_env: LXD_TOKEN_ENDPOINT_NAME
# Override any global_defaults here
filters:
# Override specific filters for this endpoint# CLI
./lxd_inventory.py --list --status running
./lxd_inventory.py --list --status running,stopped
# Config
filters:
status: [running, stopped]# CLI
./lxd_inventory.py --list --type vm
./lxd_inventory.py --list --type vm,lxc
# Config
filters:
type: [container, virtual-machine]Filter instances based on their project.
# CLI - specific projects
./lxd_inventory.py --list --project production,development
# CLI - all projects
./lxd_inventory.py --list --all-projectsfilters:
# only list instances from production and development project
projects: [production, development]
# list instances from all projects (default value)
projects: [all]
# list instances from all projects with some exceptions
projects: [all]
exclude_projects:
- backup
- 'regex:^temp.*'
- 'regex:^test[1-9]'Filter instances based on user.* configuration keys. Tags can be set directly on instances or inherited from profiles.
# Only include instances with user.ansible=true
./lxd_inventory.py --list --tag "user.ansible=true"
# Multiple requirements
./lxd_inventory.py --list --tag "user.ansible=true,user.env=production"
# Exclude instances (negation)
./lxd_inventory.py --list --tag "user.ansible!=false"
# Check for key existence
./lxd_inventory.py --list --tag "user.managed"filters:
tags:
# Include only instances with user.ansible=true
user.ansible: "true"
# Exclude instances with user.ansible=false
"user.ansible!=": "false"
# Multiple requirements
user.environment: "production"
"user.backup!=": "true"# Direct instance configuration
lxc config set myinstance user.ansible true
lxc config set myinstance user.environment production
# Profile-based tags (applies to all instances using the profile)
lxc profile create ansible-managed
lxc profile set ansible-managed user.ansible true
lxc profile add myinstance ansible-managed# CLI
./lxd_inventory.py --list --profile web,database
# Config
filters:
profiles: [web, database]filters:
exclude_names:
- 'vm1' # excludes vm1 from any project
- 'project/vm1' # excludes vm1 only from specified project
- 'regex:^vm.*' # excludes instances matching regex pattern
- 'regex:project/^vm[1-3]' # excludes instances matching regex in specific projectCustomize how instance names appear in your Ansible inventory using template variables:
{name}- Instance name{project}- LXD project name{endpoint}- Endpoint name from config{type}- Instance type (container/virtual-machine){status}- Instance status (running/stopped/etc)
# Global default
global_defaults:
hostname_format: "{name}" # Output: "myinstance"
# Include project
global_defaults:
hostname_format: "{name}.{project}" # Output: "myinstance.web"
# FQDN-style (My personal preference)
global_defaults:
hostname_format: "{name}.{project}.{endpoint}.example.com"
# Output: "myinstance.web.prod.example.com"
# Per-endpoint override
lxd_endpoints:
production:
hostname_format: "{name}.prod.example.com"
development:
hostname_format: "{name}.dev.example.com"Customize how Ansible groups are named using template variables:
project- Groups by LXD projectprofile- Groups by LXD profileendpoint- Groups by LXD endpointstatus- Groups by instance statustype- Groups by instance type
global_defaults:
group_formats:
project: "lxd_project_{project}" # Default: lxd_project_web
profile: "lxd_profile_{profile}" # Default: lxd_profile_nginx
endpoint: "lxd_endpoint_{endpoint}" # Default: lxd_endpoint_production
status: "lxd_{status}" # Default: lxd_running
type: "lxd_{type}" # Default: lxd_containers# Simple naming without lxd_ prefix
global_defaults:
group_formats:
project: "{project}" # Output: "web"
profile: "{profile}" # Output: "nginx"
endpoint: "{endpoint}" # Output: "production"
status: "{status}" # Output: "running"
type: "{type}" # Output: "containers"
# Hierarchical naming
global_defaults:
group_formats:
project: "{endpoint}_{project}" # Output: "production_web"
profile: "{project}_{profile}" # Output: "web_nginx"
status: "{endpoint}_{status}" # Output: "production_running"
# Per-endpoint customization
lxd_endpoints:
production:
group_formats:
project: "prod_{project}" # Output: "prod_web"
profile: "prod_{profile}" # Output: "prod_nginx"
development:
group_formats:
project: "dev_{project}" # Output: "dev_web"
profile: "dev_{profile}" # Output: "dev_nginx"Create custom groups by setting the user.ansible_groups (or a key to your personal preference) configuration key on instances or profiles:
# Direct instance configuration
lxc config set myinstance user.ansible_groups "webservers,production,frontend"
# Profile-based groups (applies to all instances using the profile)
lxc profile set nginx-profile user.ansible_groups "webservers,nginx"
lxc profile add myinstance nginx-profileglobal_defaults:
user_groups:
enabled: true # Enable user-defined groups (default: true)
key: "user.ansible_groups" # Configuration key to check (default)
# Per-endpoint override
lxd_endpoints:
production:
user_groups:
enabled: true
key: "user.prod_groups" # Use different key for this endpoint# Web server instances
lxc config set web1 user.ansible_groups "webservers,nginx,production"
lxc config set web2 user.ansible_groups "webservers,nginx,production"
# Database instances
lxc config set db1 user.ansible_groups "databases,mysql,production"
lxc config set db2 user.ansible_groups "databases,mysql,production"
# Load balancer
lxc config set lb1 user.ansible_groups "loadbalancers,haproxy,production,frontend"This creates Ansible groups: webservers, nginx, production, databases, mysql, loadbalancers, haproxy, frontend
filters:
# Interfaces to ignore when finding IP addresses
ignore_interfaces: [lo, docker0, cilium_host, lxdbr0]
# Prefer IPv6 over IPv4
prefer_ipv6: true# CLI
./lxd_inventory.py --list --ignore-interface "lo,docker0" --prefer-ipv6# Single endpoint
./lxd_inventory.py --list --endpoint production
# Multiple endpoints
./lxd_inventory.py --list --endpoint production,development
# All endpoints (default)
./lxd_inventory.py --listlxd_endpoints:
production:
endpoint: "https://prod-lxd:8443"
verify_ssl: true
filters:
projects: [production]
tags:
user.ansible: "true"
user.environment: "production"
development:
endpoint: "https://dev-lxd:8443"
verify_ssl: false
filters:
projects: [development, testing]
tags:
"user.ansible!=": "false"The script automatically creates these groups:
Legacy groups (always available for backward compatibility):
all- All instanceslxd_containers- Container instanceslxd_vms- Virtual machine instanceslxd_running- Running instanceslxd_stopped- Stopped instanceslxd_frozen- Frozen instanceslxd_error- Error state instances
Configurable groups (customizable via group_formats):
- Project groups - Instances from specific projects
- Profile groups - Instances with specific profiles
- Endpoint groups - Instances from specific endpoints
- Status groups - Instances by status (customizable names)
- Type groups - Instances by type (customizable names)
User-defined groups:
- Custom groups from
user.ansible_groupsconfiguration
Each instance includes these variables:
lxd_name: "original-instance-name"
lxd_hostname: "formatted-hostname"
lxd_type: "container"
lxd_status: "running"
lxd_architecture: "x86_64"
lxd_profiles: ["default", "web"]
lxd_project: "production"
lxd_endpoint: "production"
lxd_endpoint_url: "https://prod-lxd:8443"
lxd_ip: ["10.0.1.100","2001:db8::100"]
lxd_config: {...}
lxd_expanded_config: {...}
ansible_host: "10.0.1.100"global_defaults:
verify_ssl: true
hostname_format: "{name}.{project}.{endpoint}.example.com"
# Bearer tokens read from LXD_TOKEN_PRODUCTION and LXD_TOKEN_STAGING
# (derived from the endpoint names below) - see Authentication.
filters:
# Only include ansible-managed instances
tags:
user.ansible: "true"
"user.maintenance!=": "true"
lxd_endpoints:
production:
endpoint: "https://prod-lxd.example.com:8443"
filters:
projects: [production]
tags:
user.environment: "production"
staging:
endpoint: "https://staging-lxd.example.com:8443"
filters:
projects: [staging]
tags:
user.environment: "staging"With instances configured like:
# Web servers
lxc config set web1 user.ansible "true"
lxc config set web1 user.environment "production"
# Database
lxc config set db1 user.ansible "true"
lxc config set db1 user.environment "production"global_defaults:
verify_ssl: true
hostname_format: "{name}.{project}.{endpoint}.example.com"
# Custom group naming
group_formats:
project: "{endpoint}_{project}" # production_web instead of lxd_project_web
profile: "{project}_{profile}" # web_nginx instead of lxd_profile_nginx
endpoint: "{endpoint}" # production instead of lxd_endpoint_production
status: "{status}" # running instead of lxd_running
type: "{type}" # containers instead of lxd_containers
# Enable user-defined groups
user_groups:
enabled: true
key: "user.ansible_groups"With instances configured like:
# Web servers
lxc config set web1 user.ansible_groups "webservers,nginx,frontend"
# Database
lxc config set db1 user.ansible_groups "databases,mysql,backend"lxd_endpoints:
local:
endpoint: "unix:///var/lib/lxd/unix.socket"
hostname_format: "{name}.local"
filters:
projects: [all]
status: [running]
tags:
"user.ansible!=": "false"lxd_endpoints:
multi_env:
endpoint: "https://multi-lxd.example.com:8443"
filters:
projects: [all]
tags:
user.managed: "true" # Must be managed
"user.backup!=": "true" # Exclude backup instances
"user.template!=": "true" # Exclude templates
"user.ansible!=": "false" # Exclude explicitly disabledlxd_endpoints:
prod:
endpoint: "https://lxd-prod.example.com:8443"
hostname_format: "{name}.{project}.{endpoint}.example.com" # nginx2.webshop.prod.example.com
filters:
projects: [all]
exclude_names:
- webshop/nginx1 # Exclude nginx1 in the webshop project
- sophos # Exclude all instances named sophos
- 'regex:^talos.*' # Exclude all instances starting with talos
- 'regex:security/^opnsense[1-2]' # Exclude opnsense1 and opnsense2 in the security project
exclude_projects:
- backup # Exclude backup project
- 'regex:^temp.*' # Exclude all projects starting with temp./lxd_inventory.py --list --debugThis shows:
- Configuration file loading
- Endpoint connections
- Instance filtering decisions
- IP address selection
- Tag matching results
- Group creation and naming
No instances found:
- Check endpoint connectivity with
--debug - Verify project names and permissions
- Check tag filters - they may be too restrictive
SSL Certificate errors:
- Use
verify_ssl: falsefor testing - Ensure the server certificate is valid and not expired
Authentication errors:
- Check the token env var is set — the error message names the exact variable
- Bearer tokens expire (
core.bearer_token_expiry) — reissue withlxc auth identity token issue - Verify the bearer identity has permissions on the relevant projects
- Check that user is in
lxdgroup for Unix socket access
IP addresses not detected:
- Check
ignore_interfacesconfiguration - Verify instances have network interfaces configured
- Use
--debugto see network interface processing
Group names not as expected:
- Check
group_formatsconfiguration - Use
--debugto see group name generation - Verify template variables are correct
User groups not appearing:
- Verify
user_groups.enabledistrue - Check the
user_groups.keyconfiguration - Use
--debugto see user group parsing - Ensure instances have the correct configuration key set
# Test specific endpoint
./lxd_inventory.py --list --endpoint production --debug
# Test tag filtering
./lxd_inventory.py --list --tag "user.ansible=true" --debug
# Validate instance details
./lxd_inventory.py --instance mycontainer
# Test group formatting
./lxd_inventory.py --list --debug | grep -E "(Debug:|\"hosts\")"Bug reports and feature requests are welcome. When reporting issues, please include:
- Configuration file (redacted)
- Command used
- Debug output (
--debug) - LXD version and OS details
This script is provided as-is for managing LXD infrastructure with Ansible.