Writing a Custom Ansible Inventory Plugin in Python With AI
Build a custom Ansible dynamic inventory plugin in Python with AI help: verify_file, parse, caching, and constructed groups, all verified before any play runs.
- #ansible
- #ai
- #inventory
- #plugins
- #python
There’s a category of Ansible bug that’s worse than all the others, and it’s the inventory bug. If a task fails, you fix a task. If your inventory silently returns the wrong hosts, you run the right playbook against the wrong machines — or against nothing at all while every play reports success. So when your source of truth for hosts is some internal API or CMDB that no built-in inventory plugin covers, and you decide to write your own plugin, you are writing the most consequential piece of code in your Ansible setup. That’s exactly the kind of thing I want AI to draft and myself to verify, hard.
A lot of teams reach for a script inventory here — a standalone executable that prints JSON. It works, but you give up caching, config validation, and constructed groups, and you reinvent all of it badly. A real inventory plugin gets those for free. Here’s how I build one with AI doing the boilerplate and me owning the correctness.
The plugin contract
An inventory plugin subclasses BaseInventoryPlugin and implements two methods: verify_file, which decides whether this plugin owns a given config file, and parse, which actually builds the inventory. Mixing in Constructable and Cacheable adds grouping and caching with very little extra code.
# inventory_plugins/my_cmdb.py
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
from ansible.errors import AnsibleParserError
DOCUMENTATION = """
name: my_cmdb
short_description: hosts from our internal CMDB
options:
plugin:
description: token that ensures this is our config
required: True
choices: ['my_cmdb']
endpoint:
description: CMDB API base URL
required: True
token:
description: CMDB API token
required: True
"""
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
NAME = 'my_cmdb'
def verify_file(self, path):
return super().verify_file(path) and path.endswith(('my_cmdb.yml', 'my_cmdb.yaml'))
def parse(self, inventory, loader, path, cache=True):
super().parse(inventory, loader, path, cache)
self._read_config_data(path)
hosts = self._get_hosts() # your fetch
for h in hosts:
self.inventory.add_host(h['name'])
self.inventory.set_variable(h['name'], 'ansible_host', h['ip'])
That verify_file check matters more than it looks. Without the endswith guard, your plugin will try to claim every inventory YAML it sees, including ones meant for other plugins. The plugin: option in the config file is the second line of defense — a user has to explicitly write plugin: my_cmdb to activate yours.
Prompting AI for the draft
I give the model the contract and the source details, and I’m specific about failure behavior because that’s where drafts go wrong:
Write an Ansible inventory plugin named
my_cmdbthat fetches hosts from a REST API. SubclassBaseInventoryPluginand mix inConstructableandCacheable. Requirements:verify_fileonly claims files ending inmy_cmdb.yml; declareendpointandtokenoptions inDOCUMENTATIONand read them withget_option; handle API pagination; on auth or timeout failure raiseAnsibleParserError(do NOT return an empty inventory); expose each host’s region and role as variables; supportcompose,groups, andkeyed_groups. Include a sample inventory config and theansible-inventory --graphcommand to verify it.
The non-negotiable line is the one about failure. A draft that returns an empty inventory on error is the trap — it makes a play silently target nothing and hides the outage. I always make the AI raise AnsibleParserError instead.
Caching so you don’t hammer the source
Inventory is loaded constantly — every playbook run, every ansible-inventory call, every ad-hoc command. Without caching, each of those hits your CMDB. The Cacheable mixin plus respecting the user’s cache options fixes that:
def parse(self, inventory, loader, path, cache=True):
super().parse(inventory, loader, path, cache)
self._read_config_data(path)
cache_key = self.get_cache_key(path)
use_cache = self.get_option('cache') and cache
if use_cache:
try:
hosts = self._cache[cache_key]
except KeyError:
hosts = self._get_hosts()
self._cache[cache_key] = hosts
else:
hosts = self._get_hosts()
self._populate(hosts)
The user controls this through their inventory config and ansible.cfg:
# inventories/my_cmdb.yml
plugin: my_cmdb
endpoint: https://cmdb.internal/api
token: "{{ lookup('env', 'CMDB_TOKEN') }}"
cache: true
cache_plugin: jsonfile
cache_timeout: 3600
Constructed groups without editing the plugin
The best thing about mixing in Constructable is that users build groups from host attributes in config, not code. You expose the attributes; they decide the grouping:
plugin: my_cmdb
endpoint: https://cmdb.internal/api
keyed_groups:
- key: region
prefix: region
- key: role
prefix: role
groups:
production: env == 'prod'
compose:
ansible_user: "'deploy'"
After this, a host in region us-east with role web automatically lands in region_us_east and role_web, and prod hosts land in production. You never touched the plugin to make that happen. That’s the difference between a plugin people adopt and one they fork the moment their grouping needs change.
Verify before any play runs
This is the whole ballgame. You do not trust a source-of-truth component because the code looks right; you trust it because ansible-inventory shows you exactly what it produces:
# What hosts and groups did the plugin build?
ansible-inventory -i inventories/my_cmdb.yml --graph
# Show the vars too
ansible-inventory -i inventories/my_cmdb.yml --list
# Can we actually reach one host?
ansible -i inventories/my_cmdb.yml region_us_east -m ping --limit 1
@all:
|--@region_us_east:
| |--web01
| |--web02
|--@role_web:
| |--web01
| |--web02
|--@production:
| |--web01
I read that graph against what I expect the CMDB to contain. If a region is missing, or a host count looks low, the plugin has a bug I need to find before any playbook runs — not after it skipped half the fleet. The --limit 1 ping confirms the connection vars are right too.
Pro Tip: Add a tiny sanity test that asserts a known host appears in the graph, and run it in CI. A keyed_groups typo that drops half your fleet from a group is exactly the kind of silent inventory bug that costs you a 3am outage.
When a plugin is overkill
If your hosts come from a cloud provider, a built-in plugin almost certainly already exists — use it. If the list is small and static, a YAML inventory is simpler and safer. Write a custom plugin only when the source is genuinely yours and a script inventory would mean reinventing caching and grouping by hand. When that’s the case, let AI handle the boilerplate, but own verify_file, the error path, and the --graph verification yourself.
For the design considerations behind multi-environment inventories, see designing group_vars and host_vars for multi-environment inventories with AI and the full AI for Ansible category. For a reusable prompt to scaffold the plugin, see the Ansible prompts.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.