Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Ansible By James Joyner IV · · 12 min read

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_cmdb that fetches hosts from a REST API. Subclass BaseInventoryPlugin and mix in Constructable and Cacheable. Requirements: verify_file only claims files ending in my_cmdb.yml; declare endpoint and token options in DOCUMENTATION and read them with get_option; handle API pagination; on auth or timeout failure raise AnsibleParserError (do NOT return an empty inventory); expose each host’s region and role as variables; support compose, groups, and keyed_groups. Include a sample inventory config and the ansible-inventory --graph command 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.

Free download · 368-page PDF

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.