ansible / awx

AWX provides a web-based user interface, REST API, and task engine built on top of Ansible. It is one of the upstream projects for Red Hat Ansible Automation Platform.
Other
13.66k stars 3.37k forks source link

The module awx.awx.inventory is failing to create constructed inventories - Edge case uncaught #15310

Open cnfrancis opened 1 week ago

cnfrancis commented 1 week ago

Please confirm the following

Bug Summary

Edge Case

hello, I believe there is an edge case uncaught with the module awx.awx.inventory when attempting to add an input_inventories which causes the error message below:

“msg”: “The requested object could not be found at /api/v2/constructed_inventories/24/input_inventories/” 

It's due to the fact the URL built is wrong when adding an associations of type input_inventories to a resource constructed_inventory

Related discussion: https://forum.ansible.com/t/awx-awx-inventory-is-failing-to-create-constructed-inventories-potential-edge-case/6845

Problematic Code Snippets

I believe the problematic code that caused the URL to be built wrong and cause the 404 to happen is combination of the snippets below in awx/awx_collection/plugins/module_utils/controller_api.py

# awx/awx_collection/plugins/module_utils/controller_api.py
def create_if_needed(self, existing_item, new_item, endpoint, on_create=None, auto_exit=True, item_type='unknown', associations=None):

...

            if response['status_code'] in [200, 201]:
                self.json_output['name'] = 'unknown'
                for key in ('name', 'username', 'identifier', 'hostname'):
                    if key in response['json']:
                        self.json_output['name'] = response['json'][key]
                self.json_output['id'] = response['json']['id']
                self.json_output['changed'] = True
                item_url = response['json']['url']
            else:
                if 'json' in response and '__all__' in response['json']:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0]))
                elif 'json' in response:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json']))
                else:
                    self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['status_code']))

        # Process any associations with this item
        if associations is not None:
            for association_type in associations:
                sub_endpoint = '{0}{1}/'.format(item_url, association_type)
                self.modify_associations(sub_endpoint, associations[association_type])

and

# awx/awx_collection/plugins/module_utils/controller_api.py
    def modify_associations(self, association_endpoint, new_association_list):
        # if we got None instead of [] we are not modifying the association_list
        if new_association_list is None:
            return

        # First get the existing associations
        response = self.get_all_endpoint(association_endpoint)
        existing_associated_ids = [association['id'] for association in response['json']['results']]

Explanation

In ControllerAPIModule.create_if_needed(self) (code ref link)

The url and the body for the POST call

'https://<REDACTED>.com/api/v2/inventories/'
'{"name": "REDACTED", "organization": 2, "kind": "constructed", "host_filter": null, "input_inventories": [2]}'

will yield the successful response

response['json']['url'] = /api/v2/constructed_inventories/<id>

this is then stored in item_url which used to build the association sub_endpoint (code ref link)

`sub_endpoint=/api/v2/constructed_inventories//input_inventories

        if associations is not None:
            for association_type in associations:
                sub_endpoint = '{0}{1}/'.format(item_url, association_type)
                self.modify_associations(sub_endpoint, associations[association_type])

then later on, when self.get_all_endpoint(association_endpoint) which calls response = self.get_endpoint(endpoint, *args, **kwargs) with this /api/v2/constructed_inventories/<id>/input_inventories it will cause the 404 as that enpoint isn't defined.

 def modify_associations(self, association_endpoint, new_association_list):
        # if we got None instead of [] we are not modifying the association_list
        if new_association_list is None:
            return

        # First get the existing associations
        response = self.get_all_endpoint(association_endpoint)
        existing_associated_ids = [association['id'] for association in response['json']['results']]

Hope this makes sense. Please let me know what the action items are from this, or suggested course of action I can take.

FYI @TheRealHaoLiu @relrod (@cidrblock fom AnsibleFest 2024 conference Denver, US )

AWX version

22.0.0

Select the relevant components

Installation method

kubernetes

Modifications

no

Ansible version

2.17.1

Operating system

No response

Web browser

No response

Steps to reproduce

this assumes an inventory already exists when added to the input_inventory field. used arguments:

{
    "ANSIBLE_MODULE_ARGS": {
        "_ansible_check_mode": false,
        "_ansible_debug": false,
        "_ansible_diff": false,
        "_ansible_ignore_unknown_opts": false,
        "_ansible_keep_remote_files": true,
        "_ansible_module_name": "awx.awx.inventory",
        "_ansible_no_log": false,
        "_ansible_remote_tmp": "~/.ansible/tmp",
        "_ansible_selinux_special_fs": [
            "fuse",
            "nfs",
            "vboxsf",
            "ramfs",
            "9p",
            "vfat"
        ],
        "_ansible_shell_executable": "/bin/sh",
        "_ansible_socket": null,
        "_ansible_string_conversion_action": "warn",
        "_ansible_syslog_facility": "LOG_USER",
        "_ansible_target_log_info": null,
        "_ansible_tmpdir": "REDACTED",
        "_ansible_verbosity": 3,
        "_ansible_version": "2.17.1",
        "controller_host": "REDACTED",
        "input_inventories": [
            "REDACTED"
        ],
        "organization": "REDACTED",
        "kind": "constructed",
        "name": "REDACTED",        
        "controller_username": "REDACTED",        
        "controller_password": "REDACTED"
    }
}

Expected results

The built URL should be inventories/<id>/input_inventories when adding association of type input_inventories to a construced_inventory

Actual results

“msg”: “The requested object could not be found at /api/v2/constructed_inventories/24/input_inventories/”

Additional information

13448

Commit: https://github.com/ansible/awx/commit/e22967d28de40fe40d94fd4818aa9248942f6b4c

Denney-tech commented 3 days ago

I think this might actually be a duplicate of #13781 and resolved by #13797. Please upgrade to AWX >= 22.1.0 and test again.