AWX as Code - Constructed Inventory

Started up a new AWX instance as I am going to repurpose my old server with my existing AWX on it. So, I have taken this task, to build it all from scratch using Ansible. I have had some sticking points, but through frustration I have solved most parts

The bit I can’t figure out is Constructed Inventory. Official Docs tells you to create an inventory first with some details, then edit it using inventory_source

- name: Add constructed inventory with two existing input inventories
  inventory:
    name: My Constructed Inventory
    organization: Default
    kind: constructed
    input_inventories:
      - "West Datacenter"
      - "East Datacenter"

- name: Edit the constructed inventory source
  inventory_source:
    # The constructed inventory source will always be in the format:
    # "Auto-created source for: <constructed inventory name>"
    name: "Auto-created source for: My Constructed Inventory"
    inventory: My Constructed Inventory
    limit: host3,host4,host6
    source_vars:
      plugin: constructed
      strict: true
      use_vars_plugins: true
      groups:
        shutdown: resolved_state == "shutdown"
        shutdown_in_product_dev: resolved_state == "shutdown" and account_alias == "product_dev"
      compose:
        resolved_state: state | default("running")

I have 2 Dynamic Inventories that pull data from 2 Proxmox Hosts, this I have got working fine and it populates hosts, groups etc. This Constructed Inventory, I want to pull out all LXC Containers, I have others that pull EL Only Hosts and Debian based Hosts. Here is my code based off of the example above:

    - role: awx_inventory
      vars:
        awx_inventory:
          - name: ConInv_Proxmox_LXC
            description: Inventory Source for Proxmox LXC Containers
            organization: SixteenOne
            kind: constructed
            input_inventories:
              - DynInv_Oberon
              - DynInv_Pandora
    - role: awx_inventory_source
      vars:
        awx_inventory_source:
          - name: ConInv_Proxmox_LXC
            description: Inventory Source for Proxmox LXC Containers
            organization: SixteenOne
            inventory: ConInv_Proxmox_LXC
            limit: proxmox_all_lxc
            # timeout: 3600
            source_vars:
              plugin: constructed
              strict: true
              use_vars_plugins: true

I am using Roles, so that I can eventually put them on Galaxy, I have already put a couple on already. The Inventory creates fine, but the error that I get when trying to update using inventory_source is as follows:

The full traceback is:
Traceback (most recent call last):
  File "/Users/sixteenone/.ansible/tmp/ansible-tmp-1742853618.5283918-5908-254240443186731/AnsiballZ_inventory_source.py", line 107, in <module>
    _ansiballz_main()
    ~~~~~~~~~~~~~~~^^
  File "/Users/sixteenone/.ansible/tmp/ansible-tmp-1742853618.5283918-5908-254240443186731/AnsiballZ_inventory_source.py", line 99, in _ansiballz_main
    invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sixteenone/.ansible/tmp/ansible-tmp-1742853618.5283918-5908-254240443186731/AnsiballZ_inventory_source.py", line 47, in invoke_module
    runpy.run_module(mod_name='ansible_collections.awx.awx.plugins.modules.inventory_source', init_globals=dict(_module_fqn='ansible_collections.awx.awx.plugins.modules.inventory_source', _modlib_path=modlib_path),
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                     run_name='__main__', alter_sys=True)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen runpy>", line 226, in run_module
  File "<frozen runpy>", line 98, in _run_module_code
  File "<frozen runpy>", line 88, in _run_code
  File "/var/folders/5p/z5gs10rs4bn_lgc7k1j5cr040000gn/T/ansible_awx.awx.inventory_source_payload_qsgyl_lu/ansible_awx.awx.inventory_source_payload.zip/ansible_collections/awx/awx/plugins/modules/inventory_source.py", line 328, in <module>
  File "/var/folders/5p/z5gs10rs4bn_lgc7k1j5cr040000gn/T/ansible_awx.awx.inventory_source_payload_qsgyl_lu/ansible_awx.awx.inventory_source_payload.zip/ansible_collections/awx/awx/plugins/modules/inventory_source.py", line 318, in main
KeyError: 'source'
failed: [localhost] (item={'name': 'ConInv_Proxmox_LXC', 'description': 'Inventory Source for Proxmox LXC Containers', 'organization': 'SixteenOne', 'inventory': 'ConInv_Proxmox_LXC', 'limit': 'proxmox_all_lxc', 'source_vars': {'plugin': 'constructed', 'strict': True, 'use_vars_plugins': True}}) => {
    "ansible_loop_var": "item",
    "changed": false,
    "item": {
        "description": "Inventory Source for Proxmox LXC Containers",
        "inventory": "ConInv_Proxmox_LXC",
        "limit": "proxmox_all_lxc",
        "name": "ConInv_Proxmox_LXC",
        "organization": "SixteenOne",
        "source_vars": {
            "plugin": "constructed",
            "strict": true,
            "use_vars_plugins": true
        }
    },
    "module_stderr": "Traceback (most recent call last):\n  File \"/Users/sixteenone/.ansible/tmp/ansible-tmp-1742853618.5283918-5908-254240443186731/AnsiballZ_inventory_source.py\", line 107, in <module>\n    _ansiballz_main()\n    ~~~~~~~~~~~~~~~^^\n  File \"/Users/sixteenone/.ansible/tmp/ansible-tmp-1742853618.5283918-5908-254240443186731/AnsiballZ_inventory_source.py\", line 99, in _ansiballz_main\n    invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)\n    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/Users/sixteenone/.ansible/tmp/ansible-tmp-1742853618.5283918-5908-254240443186731/AnsiballZ_inventory_source.py\", line 47, in invoke_module\n    runpy.run_module(mod_name='ansible_collections.awx.awx.plugins.modules.inventory_source', init_globals=dict(_module_fqn='ansible_collections.awx.awx.plugins.modules.inventory_source', _modlib_path=modlib_path),\n    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n                     run_name='__main__', alter_sys=True)\n                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"<frozen runpy>\", line 226, in run_module\n  File \"<frozen runpy>\", line 98, in _run_module_code\n  File \"<frozen runpy>\", line 88, in _run_code\n  File \"/var/folders/5p/z5gs10rs4bn_lgc7k1j5cr040000gn/T/ansible_awx.awx.inventory_source_payload_qsgyl_lu/ansible_awx.awx.inventory_source_payload.zip/ansible_collections/awx/awx/plugins/modules/inventory_source.py\", line 328, in <module>\n  File \"/var/folders/5p/z5gs10rs4bn_lgc7k1j5cr040000gn/T/ansible_awx.awx.inventory_source_payload_qsgyl_lu/ansible_awx.awx.inventory_source_payload.zip/ansible_collections/awx/awx/plugins/modules/inventory_source.py\", line 318, in main\nKeyError: 'source'\n",
    "module_stdout": "",
    "msg": "MODULE FAILURE: No start of json char found\nSee stdout/stderr for the exact error",
    "rc": 1
}

Inventory Role:

- name: Create Inventory
  awx.awx.inventory:
    controller_host: "{{ awx_controller_host }}"
    controller_username: "{{ awx_controller_username }}"
    controller_password: "{{ awx_controller_password }}"
    name: "{{ item.name }}"
    description: "{{ item.description | default(omit) }}"
  ##### Different Details #####
    organization: "{{ item.organization }}"
    copy_from: "{{ item.copy_from | default(omit) }}"
    input_inventories: "{{ item.input_inventories | default(omit) }}"
    instance_groups: "{{ item.instance_groups | default(omit) }}"
    kind: "{{ item.kind | default(omit) }}"
    new_name: "{{ item.new_name | default(omit) }}"
    prevent_instance_group_fallback: "{{ item.prevent_instance_group_fallback | default(omit) }}"
    request_timeout: "{{ item.request_timeout | default(omit) }}"
    state: "{{ item.state | default('present') }}"
    validate_certs: "{{ item.validate_certs | default(omit) }}"
    variables: "{{ item.variables | default(omit) }}"
  loop: "{{ awx_inventory }}"

Inventory Source Role

- name: Create Inventory Sources
  awx.awx.inventory_source:
    controller_host: "{{ awx_controller_host }}"
    controller_username: "{{ awx_controller_username }}"
    controller_password: "{{ awx_controller_password }}"
    name: "{{ item.name }}"
    description: "{{ item.description | default(omit) }}"
    organization: "{{ item.organization }}"
  ##### Different Details #####
    credential: "{{ item.credential | default(omit) }}"
    custom_virtualenv: "{{ item.custom_virtualenv | default(omit) }}"
    enabled_value: "{{ item.enabled_value | default(omit) }}"
    enabled_var: "{{ item.enabled_var | default(omit) }}"
    execution_environment: "{{ item.execution_environment | default(omit) }}"
    host_filter: "{{ item.host_filter | default(omit) }}"
    inventory: "{{ item.inventory | default(omit) }}"
    limit: "{{ item.limit | default(omit) }}"
    new_name: "{{ item.new_name | default(omit) }}"
    notification_templates_error: "{{ item.notification_templates_error | default(omit) }}"
    notification_templates_started: "{{ item.notification_templates_started | default(omit) }}"
    notification_templates_success: "{{ item.notification_templates_success | default(omit) }}"
    overwrite: "{{ item.overwrite | default(omit) }}"
    overwrite_vars: "{{ item.overwrite_vars | default(omit) }}"
    request_timeout: "{{ item.request_timeout | default(omit) }}"
    scm_branch: "{{ item.scm_branch | default(omit) }}"
    source: "{{ item.source | default(omit) }}"
    source_path: "{{ item.source_path | default(omit) }}"
    source_project: "{{ item.source_project | default(omit) }}"
    source_vars: "{{ item.source_vars | default(omit) }}"
    state: "{{ item.state | default('present') }}"
    timeout: "{{ item.timeout | default(omit) }}"
    update_cache_timeout: "{{ item.update_cache_timeout | default(omit) }}"
    update_on_launch: "{{ item.update_on_launch | default(omit) }}"
    validate_certs: "{{ item.validate_certs | default(omit) }}"
    verbosity: "{{ item.verbosity | default(omit) }}"
  loop: "{{ awx_inventory_source }}"

Any help is appreciated, or if anyone has a Repo where they have created one successfully let me know. Searching on the web is difficult as it keeps showing guides on doing this manually, I have done that, I now want to automate it

Figured it out using the REST API webpage

A Constructed Inventory is both an Inventory and an Inventory Source at the same time. The Inventory API only holds certain parts, the Inventory Source holds the good stuff like limit & source_vars

I was using my old AWX as a reference, so I could look on the REST API at the existing data that I knew worked. Below is a part of the JSON from the REST API. On the Inventory Source List - the name is actually called Auto-created source forInventory name

            "inventory": {
                "id": 14,
                "name": "ConInv_Debian_Hosts",
                "description": "Inventory Source for Debian based Hosts",
                "has_active_failures": false,
                "total_hosts": 15,
                "hosts_with_active_failures": 0,
                "total_groups": 29,
                "has_inventory_sources": false,
                "total_inventory_sources": 0,
                "inventory_sources_with_failures": 0,
                "organization_id": 2,
                "kind": "constructed"

...

 "created": "2025-03-24T22:31:05.604791Z",
            "modified": "2025-03-28T08:05:31.247010Z",
            "name": "Auto-created source for: ConInv_Debian_Hosts",
            "description": "",
            "source": "constructed",
            "source_path": "",
            "source_vars": "{\"plugin\": \"constructed\", \"strict\": true, \"use_vars_plugins\": true}",
            "scm_branch": "",
            "credential": null,
            "enabled_var": "",
            "enabled_value": "",
            "host_filter": "",
            "overwrite": true,
            "overwrite_vars": true,
            "custom_virtualenv": null,
            "timeout": 0,
            "verbosity": 0,
            "limit": "arch_ubuntu:arch_debian:&proxmox_all_running",
            "last_job_run": "2025-03-28T09:09:11.319602Z",
            "last_job_failed": false,
            "next_job_run": null,
            "status": "successful",
            "execution_environment": null,
            "inventory": 14,
            "update_on_launch": true,
            "update_cache_timeout": 3600,
            "source_project": null,
            "last_update_failed": false,
            "last_updated": "2025-03-28T09:09:11.319602Z"

Original post was for my LXC Containers, but I am using my Debian list here. So, the name is now set to "Auto-created source for: ConInv_Debian_Hosts" Quotes are important due to the semi-colon inside and my lint puts a red-squiggly line under it, if I don’t !

Inventory is the same name that I used when I created the Inventory. With the Inventory Source Role task now set with the below variables, I can happily update it to the correct values that I want :smile:

          - name: "Auto-created source for: ConInv_Debian_Hosts"
            inventory: ConInv_Debian_Hosts
            limit: arch_ubuntu:arch_debian:&proxmox_all_running
            update_cache_timeout: 3600
            verbosity: 0
            source_vars:
              plugin: constructed
              strict: true
              use_vars_plugins: true
1 Like

You may be interested in looking at the infra.controller_configuration collection. It has a bunch of roles for configuring AAP, but technically should work for AWX. There’s even sort of export/import roles that do 90% of the work.