We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
awx.awx.tower not working with null groups
##### ISSUE TYPE
 - Bug Report

##### ENVIRONMENT
* AWX version: 15.0.0
* AWX install method: docker on linux
* Ansible version:  2.9.13
* Operating System: macOS

##### STEPS TO REPRODUCE
Try parse inventory from cli where some groups has no any hosts
 ```
ansible-inventory -i inventory_tower/test_awx_tower.yml --graph
[WARNING]: You are running collection version 15.0.1 but connecting to tower version 15.0.0
@all:
  |--@test_group1:
  |  |--@test_group2:
  |  |  |--test_host_1
  |  |--test_host_1
  |--@ungrouped:
```
if remove test_host1 from test_group2
```
ansible-inventory -i inventory_tower/test_awx_tower.yml --graph
[WARNING]: You are running collection version 15.0.1 but connecting to tower version 15.0.0
[WARNING]:  * Failed to parse /Users/meonacist/playbook/inventory_tower/test_awx_tower.yml with auto plugin: test_group2 is not a known host nor group
[WARNING]:  * Failed to parse /Users/meonacist/playbook/inventory_tower/test_awx_tower.yml with yaml plugin: Plugin configuration YAML file, not YAML inventory
[WARNING]:  * Failed to parse /Users/meonacist/playbook/inventory_tower/test_awx_tower.yml with ini plugin: Invalid host pattern 'plugin:' supplied, ending in ':' is not allowed, this
character is reserved to provide a port.
[WARNING]: Unable to parse /Users/meonacist/playbook/inventory_tower/test_awx_tower.yml as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
@all:
  |--@ungrouped:
```
cat inventory_tower/test_awx_tower.yml
```
plugin: awx.awx.tower
host: awx.local.lan
inventory_id: 58
validate_certs: no
include_metadata: yes
```

<!-- Please describe exactly how to reproduce the problem. -->

##### EXPECTED RESULTS
 ```
ansible-inventory -i inventory_tower/test_awx_tower.yml --graph
[WARNING]: You are running collection version 15.0.1 but connecting to tower version 15.0.0
@all:
  |--@test_group1:
  |  |--@test_group2:
  |  |--test_host_1
  |--@ungrouped:
```


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `awx_collection/plugins/inventory/tower.py`
Content:
```
1 # Copyright (c) 2018 Ansible Project
2 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3 
4 from __future__ import (absolute_import, division, print_function)
5 
6 __metaclass__ = type
7 
8 DOCUMENTATION = '''
9 name: tower
10 plugin_type: inventory
11 author:
12   - Matthew Jones (@matburt)
13   - Yunfan Zhang (@YunfanZhang42)
14 short_description: Ansible dynamic inventory plugin for Ansible Tower.
15 description:
16     - Reads inventories from Ansible Tower.
17     - Supports reading configuration from both YAML config file and environment variables.
18     - If reading from the YAML file, the file name must end with tower.(yml|yaml) or tower_inventory.(yml|yaml),
19       the path in the command would be /path/to/tower_inventory.(yml|yaml). If some arguments in the config file
20       are missing, this plugin will try to fill in missing arguments by reading from environment variables.
21     - If reading configurations from environment variables, the path in the command must be @tower_inventory.
22 extends_documentation_fragment: awx.awx.auth_plugin
23 options:
24     inventory_id:
25         description:
26             - The ID of the Ansible Tower inventory that you wish to import.
27             - This is allowed to be either the inventory primary key or its named URL slug.
28             - Primary key values will be accepted as strings or integers, and URL slugs must be strings.
29             - Named URL slugs follow the syntax of "inventory_name++organization_name".
30         type: raw
31         env:
32             - name: TOWER_INVENTORY
33         required: True
34     include_metadata:
35         description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host.
36         type: bool
37         default: False
38 '''
39 
40 EXAMPLES = '''
41 # Before you execute the following commands, you should make sure this file is in your plugin path,
42 # and you enabled this plugin.
43 
44 # Example for using tower_inventory.yml file
45 
46 plugin: awx.awx.tower
47 host: your_ansible_tower_server_network_address
48 username: your_ansible_tower_username
49 password: your_ansible_tower_password
50 inventory_id: the_ID_of_targeted_ansible_tower_inventory
51 # Then you can run the following command.
52 # If some of the arguments are missing, Ansible will attempt to read them from environment variables.
53 # ansible-inventory -i /path/to/tower_inventory.yml --list
54 
55 # Example for reading from environment variables:
56 
57 # Set environment variables:
58 # export TOWER_HOST=YOUR_TOWER_HOST_ADDRESS
59 # export TOWER_USERNAME=YOUR_TOWER_USERNAME
60 # export TOWER_PASSWORD=YOUR_TOWER_PASSWORD
61 # export TOWER_INVENTORY=THE_ID_OF_TARGETED_INVENTORY
62 # Read the inventory specified in TOWER_INVENTORY from Ansible Tower, and list them.
63 # The inventory path must always be @tower_inventory if you are reading all settings from environment variables.
64 # ansible-inventory -i @tower_inventory --list
65 '''
66 
67 import os
68 
69 from ansible.module_utils import six
70 from ansible.module_utils._text import to_text, to_native
71 from ansible.errors import AnsibleParserError, AnsibleOptionsError
72 from ansible.plugins.inventory import BaseInventoryPlugin
73 from ansible.config.manager import ensure_type
74 
75 from ..module_utils.tower_api import TowerAPIModule
76 
77 
78 def handle_error(**kwargs):
79     raise AnsibleParserError(to_native(kwargs.get('msg')))
80 
81 
82 class InventoryModule(BaseInventoryPlugin):
83     NAME = 'awx.awx.tower'  # REPLACE
84     # Stays backward compatible with tower inventory script.
85     # If the user supplies '@tower_inventory' as path, the plugin will read from environment variables.
86     no_config_file_supplied = False
87 
88     def verify_file(self, path):
89         if path.endswith('@tower_inventory'):
90             self.no_config_file_supplied = True
91             return True
92         elif super(InventoryModule, self).verify_file(path):
93             return path.endswith(('tower_inventory.yml', 'tower_inventory.yaml', 'tower.yml', 'tower.yaml'))
94         else:
95             return False
96 
97     def warn_callback(self, warning):
98         self.display.warning(warning)
99 
100     def parse(self, inventory, loader, path, cache=True):
101         super(InventoryModule, self).parse(inventory, loader, path)
102         if not self.no_config_file_supplied and os.path.isfile(path):
103             self._read_config_data(path)
104 
105         # Defer processing of params to logic shared with the modules
106         module_params = {}
107         for plugin_param, module_param in TowerAPIModule.short_params.items():
108             opt_val = self.get_option(plugin_param)
109             if opt_val is not None:
110                 module_params[module_param] = opt_val
111 
112         module = TowerAPIModule(
113             argument_spec={}, direct_params=module_params,
114             error_callback=handle_error, warn_callback=self.warn_callback
115         )
116 
117         # validate type of inventory_id because we allow two types as special case
118         inventory_id = self.get_option('inventory_id')
119         if isinstance(inventory_id, int):
120             inventory_id = to_text(inventory_id, nonstring='simplerepr')
121         else:
122             try:
123                 inventory_id = ensure_type(inventory_id, 'str')
124             except ValueError as e:
125                 raise AnsibleOptionsError(
126                     'Invalid type for configuration option inventory_id, '
127                     'not integer, and cannot convert to string: {err}'.format(err=to_native(e))
128                 )
129         inventory_id = inventory_id.replace('/', '')
130         inventory_url = '/api/v2/inventories/{inv_id}/script/'.format(inv_id=inventory_id)
131 
132         inventory = module.get_endpoint(
133             inventory_url, data={'hostvars': '1', 'towervars': '1', 'all': '1'}
134         )['json']
135 
136         # To start with, create all the groups.
137         for group_name in inventory:
138             if group_name != '_meta':
139                 self.inventory.add_group(group_name)
140 
141         # Then, create all hosts and add the host vars.
142         all_hosts = inventory['_meta']['hostvars']
143         for host_name, host_vars in six.iteritems(all_hosts):
144             self.inventory.add_host(host_name)
145             for var_name, var_value in six.iteritems(host_vars):
146                 self.inventory.set_variable(host_name, var_name, var_value)
147 
148         # Lastly, create to group-host and group-group relationships, and set group vars.
149         for group_name, group_content in six.iteritems(inventory):
150             if group_name != 'all' and group_name != '_meta':
151                 # First add hosts to groups
152                 for host_name in group_content.get('hosts', []):
153                     self.inventory.add_host(host_name, group_name)
154                 # Then add the parent-children group relationships.
155                 for child_group_name in group_content.get('children', []):
156                     self.inventory.add_child(group_name, child_group_name)
157             # Set the group vars. Note we should set group var for 'all', but not '_meta'.
158             if group_name != '_meta':
159                 for var_name, var_value in six.iteritems(group_content.get('vars', {})):
160                     self.inventory.set_variable(group_name, var_name, var_value)
161 
162         # Fetch extra variables if told to do so
163         if self.get_option('include_metadata'):
164 
165             config_data = module.get_endpoint('/api/v2/config/')['json']
166 
167             server_data = {}
168             server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown')
169             for key in ('version', 'ansible_version'):
170                 server_data[key] = config_data.get(key, 'unknown')
171             self.inventory.set_variable('all', 'tower_metadata', server_data)
172 
173         # Clean up the inventory.
174         self.inventory.reconcile_inventory()
175
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/awx_collection/plugins/inventory/tower.py b/awx_collection/plugins/inventory/tower.py
--- a/awx_collection/plugins/inventory/tower.py
+++ b/awx_collection/plugins/inventory/tower.py
@@ -153,6 +153,8 @@
                     self.inventory.add_host(host_name, group_name)
                 # Then add the parent-children group relationships.
                 for child_group_name in group_content.get('children', []):
+                    # add the child group to groups, if its already there it will just throw a warning
+                    self.inventory.add_group(child_group_name)
                     self.inventory.add_child(group_name, child_group_name)
             # Set the group vars. Note we should set group var for 'all', but not '_meta'.
             if group_name != '_meta':

{"golden_diff": "diff --git a/awx_collection/plugins/inventory/tower.py b/awx_collection/plugins/inventory/tower.py\n--- a/awx_collection/plugins/inventory/tower.py\n+++ b/awx_collection/plugins/inventory/tower.py\n@@ -153,6 +153,8 @@\n                     self.inventory.add_host(host_name, group_name)\n                 # Then add the parent-children group relationships.\n                 for child_group_name in group_content.get('children', []):\n+                    # add the child group to groups, if its already there it will just throw a warning\n+                    self.inventory.add_group(child_group_name)\n                     self.inventory.add_child(group_name, child_group_name)\n             # Set the group vars. Note we should set group var for 'all', but not '_meta'.\n             if group_name != '_meta':\n", "issue": "awx.awx.tower not working with null groups\n##### ISSUE TYPE\r\n - Bug Report\r\n\r\n##### ENVIRONMENT\r\n* AWX version: 15.0.0\r\n* AWX install method: docker on linux\r\n* Ansible version:  2.9.13\r\n* Operating System: macOS\r\n\r\n##### STEPS TO REPRODUCE\r\nTry parse inventory from cli where some groups has no any hosts\r\n ```\r\nansible-inventory -i inventory_tower/test_awx_tower.yml --graph\r\n[WARNING]: You are running collection version 15.0.1 but connecting to tower version 15.0.0\r\n@all:\r\n  |--@test_group1:\r\n  |  |--@test_group2:\r\n  |  |  |--test_host_1\r\n  |  |--test_host_1\r\n  |--@ungrouped:\r\n```\r\nif remove test_host1 from test_group2\r\n```\r\nansible-inventory -i inventory_tower/test_awx_tower.yml --graph\r\n[WARNING]: You are running collection version 15.0.1 but connecting to tower version 15.0.0\r\n[WARNING]:  * Failed to parse /Users/meonacist/playbook/inventory_tower/test_awx_tower.yml with auto plugin: test_group2 is not a known host nor group\r\n[WARNING]:  * Failed to parse /Users/meonacist/playbook/inventory_tower/test_awx_tower.yml with yaml plugin: Plugin configuration YAML file, not YAML inventory\r\n[WARNING]:  * Failed to parse /Users/meonacist/playbook/inventory_tower/test_awx_tower.yml with ini plugin: Invalid host pattern 'plugin:' supplied, ending in ':' is not allowed, this\r\ncharacter is reserved to provide a port.\r\n[WARNING]: Unable to parse /Users/meonacist/playbook/inventory_tower/test_awx_tower.yml as an inventory source\r\n[WARNING]: No inventory was parsed, only implicit localhost is available\r\n@all:\r\n  |--@ungrouped:\r\n```\r\ncat inventory_tower/test_awx_tower.yml\r\n```\r\nplugin: awx.awx.tower\r\nhost: awx.local.lan\r\ninventory_id: 58\r\nvalidate_certs: no\r\ninclude_metadata: yes\r\n```\r\n\r\n<!-- Please describe exactly how to reproduce the problem. -->\r\n\r\n##### EXPECTED RESULTS\r\n ```\r\nansible-inventory -i inventory_tower/test_awx_tower.yml --graph\r\n[WARNING]: You are running collection version 15.0.1 but connecting to tower version 15.0.0\r\n@all:\r\n  |--@test_group1:\r\n  |  |--@test_group2:\r\n  |  |--test_host_1\r\n  |--@ungrouped:\r\n```\r\n\n", "before_files": [{"content": "# Copyright (c) 2018 Ansible Project\n# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)\n\nfrom __future__ import (absolute_import, division, print_function)\n\n__metaclass__ = type\n\nDOCUMENTATION = '''\nname: tower\nplugin_type: inventory\nauthor:\n  - Matthew Jones (@matburt)\n  - Yunfan Zhang (@YunfanZhang42)\nshort_description: Ansible dynamic inventory plugin for Ansible Tower.\ndescription:\n    - Reads inventories from Ansible Tower.\n    - Supports reading configuration from both YAML config file and environment variables.\n    - If reading from the YAML file, the file name must end with tower.(yml|yaml) or tower_inventory.(yml|yaml),\n      the path in the command would be /path/to/tower_inventory.(yml|yaml). If some arguments in the config file\n      are missing, this plugin will try to fill in missing arguments by reading from environment variables.\n    - If reading configurations from environment variables, the path in the command must be @tower_inventory.\nextends_documentation_fragment: awx.awx.auth_plugin\noptions:\n    inventory_id:\n        description:\n            - The ID of the Ansible Tower inventory that you wish to import.\n            - This is allowed to be either the inventory primary key or its named URL slug.\n            - Primary key values will be accepted as strings or integers, and URL slugs must be strings.\n            - Named URL slugs follow the syntax of \"inventory_name++organization_name\".\n        type: raw\n        env:\n            - name: TOWER_INVENTORY\n        required: True\n    include_metadata:\n        description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host.\n        type: bool\n        default: False\n'''\n\nEXAMPLES = '''\n# Before you execute the following commands, you should make sure this file is in your plugin path,\n# and you enabled this plugin.\n\n# Example for using tower_inventory.yml file\n\nplugin: awx.awx.tower\nhost: your_ansible_tower_server_network_address\nusername: your_ansible_tower_username\npassword: your_ansible_tower_password\ninventory_id: the_ID_of_targeted_ansible_tower_inventory\n# Then you can run the following command.\n# If some of the arguments are missing, Ansible will attempt to read them from environment variables.\n# ansible-inventory -i /path/to/tower_inventory.yml --list\n\n# Example for reading from environment variables:\n\n# Set environment variables:\n# export TOWER_HOST=YOUR_TOWER_HOST_ADDRESS\n# export TOWER_USERNAME=YOUR_TOWER_USERNAME\n# export TOWER_PASSWORD=YOUR_TOWER_PASSWORD\n# export TOWER_INVENTORY=THE_ID_OF_TARGETED_INVENTORY\n# Read the inventory specified in TOWER_INVENTORY from Ansible Tower, and list them.\n# The inventory path must always be @tower_inventory if you are reading all settings from environment variables.\n# ansible-inventory -i @tower_inventory --list\n'''\n\nimport os\n\nfrom ansible.module_utils import six\nfrom ansible.module_utils._text import to_text, to_native\nfrom ansible.errors import AnsibleParserError, AnsibleOptionsError\nfrom ansible.plugins.inventory import BaseInventoryPlugin\nfrom ansible.config.manager import ensure_type\n\nfrom ..module_utils.tower_api import TowerAPIModule\n\n\ndef handle_error(**kwargs):\n    raise AnsibleParserError(to_native(kwargs.get('msg')))\n\n\nclass InventoryModule(BaseInventoryPlugin):\n    NAME = 'awx.awx.tower'  # REPLACE\n    # Stays backward compatible with tower inventory script.\n    # If the user supplies '@tower_inventory' as path, the plugin will read from environment variables.\n    no_config_file_supplied = False\n\n    def verify_file(self, path):\n        if path.endswith('@tower_inventory'):\n            self.no_config_file_supplied = True\n            return True\n        elif super(InventoryModule, self).verify_file(path):\n            return path.endswith(('tower_inventory.yml', 'tower_inventory.yaml', 'tower.yml', 'tower.yaml'))\n        else:\n            return False\n\n    def warn_callback(self, warning):\n        self.display.warning(warning)\n\n    def parse(self, inventory, loader, path, cache=True):\n        super(InventoryModule, self).parse(inventory, loader, path)\n        if not self.no_config_file_supplied and os.path.isfile(path):\n            self._read_config_data(path)\n\n        # Defer processing of params to logic shared with the modules\n        module_params = {}\n        for plugin_param, module_param in TowerAPIModule.short_params.items():\n            opt_val = self.get_option(plugin_param)\n            if opt_val is not None:\n                module_params[module_param] = opt_val\n\n        module = TowerAPIModule(\n            argument_spec={}, direct_params=module_params,\n            error_callback=handle_error, warn_callback=self.warn_callback\n        )\n\n        # validate type of inventory_id because we allow two types as special case\n        inventory_id = self.get_option('inventory_id')\n        if isinstance(inventory_id, int):\n            inventory_id = to_text(inventory_id, nonstring='simplerepr')\n        else:\n            try:\n                inventory_id = ensure_type(inventory_id, 'str')\n            except ValueError as e:\n                raise AnsibleOptionsError(\n                    'Invalid type for configuration option inventory_id, '\n                    'not integer, and cannot convert to string: {err}'.format(err=to_native(e))\n                )\n        inventory_id = inventory_id.replace('/', '')\n        inventory_url = '/api/v2/inventories/{inv_id}/script/'.format(inv_id=inventory_id)\n\n        inventory = module.get_endpoint(\n            inventory_url, data={'hostvars': '1', 'towervars': '1', 'all': '1'}\n        )['json']\n\n        # To start with, create all the groups.\n        for group_name in inventory:\n            if group_name != '_meta':\n                self.inventory.add_group(group_name)\n\n        # Then, create all hosts and add the host vars.\n        all_hosts = inventory['_meta']['hostvars']\n        for host_name, host_vars in six.iteritems(all_hosts):\n            self.inventory.add_host(host_name)\n            for var_name, var_value in six.iteritems(host_vars):\n                self.inventory.set_variable(host_name, var_name, var_value)\n\n        # Lastly, create to group-host and group-group relationships, and set group vars.\n        for group_name, group_content in six.iteritems(inventory):\n            if group_name != 'all' and group_name != '_meta':\n                # First add hosts to groups\n                for host_name in group_content.get('hosts', []):\n                    self.inventory.add_host(host_name, group_name)\n                # Then add the parent-children group relationships.\n                for child_group_name in group_content.get('children', []):\n                    self.inventory.add_child(group_name, child_group_name)\n            # Set the group vars. Note we should set group var for 'all', but not '_meta'.\n            if group_name != '_meta':\n                for var_name, var_value in six.iteritems(group_content.get('vars', {})):\n                    self.inventory.set_variable(group_name, var_name, var_value)\n\n        # Fetch extra variables if told to do so\n        if self.get_option('include_metadata'):\n\n            config_data = module.get_endpoint('/api/v2/config/')['json']\n\n            server_data = {}\n            server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown')\n            for key in ('version', 'ansible_version'):\n                server_data[key] = config_data.get(key, 'unknown')\n            self.inventory.set_variable('all', 'tower_metadata', server_data)\n\n        # Clean up the inventory.\n        self.inventory.reconcile_inventory()\n", "path": "awx_collection/plugins/inventory/tower.py"}], "after_files": [{"content": "# Copyright (c) 2018 Ansible Project\n# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)\n\nfrom __future__ import (absolute_import, division, print_function)\n\n__metaclass__ = type\n\nDOCUMENTATION = '''\nname: tower\nplugin_type: inventory\nauthor:\n  - Matthew Jones (@matburt)\n  - Yunfan Zhang (@YunfanZhang42)\nshort_description: Ansible dynamic inventory plugin for Ansible Tower.\ndescription:\n    - Reads inventories from Ansible Tower.\n    - Supports reading configuration from both YAML config file and environment variables.\n    - If reading from the YAML file, the file name must end with tower.(yml|yaml) or tower_inventory.(yml|yaml),\n      the path in the command would be /path/to/tower_inventory.(yml|yaml). If some arguments in the config file\n      are missing, this plugin will try to fill in missing arguments by reading from environment variables.\n    - If reading configurations from environment variables, the path in the command must be @tower_inventory.\nextends_documentation_fragment: awx.awx.auth_plugin\noptions:\n    inventory_id:\n        description:\n            - The ID of the Ansible Tower inventory that you wish to import.\n            - This is allowed to be either the inventory primary key or its named URL slug.\n            - Primary key values will be accepted as strings or integers, and URL slugs must be strings.\n            - Named URL slugs follow the syntax of \"inventory_name++organization_name\".\n        type: raw\n        env:\n            - name: TOWER_INVENTORY\n        required: True\n    include_metadata:\n        description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host.\n        type: bool\n        default: False\n'''\n\nEXAMPLES = '''\n# Before you execute the following commands, you should make sure this file is in your plugin path,\n# and you enabled this plugin.\n\n# Example for using tower_inventory.yml file\n\nplugin: awx.awx.tower\nhost: your_ansible_tower_server_network_address\nusername: your_ansible_tower_username\npassword: your_ansible_tower_password\ninventory_id: the_ID_of_targeted_ansible_tower_inventory\n# Then you can run the following command.\n# If some of the arguments are missing, Ansible will attempt to read them from environment variables.\n# ansible-inventory -i /path/to/tower_inventory.yml --list\n\n# Example for reading from environment variables:\n\n# Set environment variables:\n# export TOWER_HOST=YOUR_TOWER_HOST_ADDRESS\n# export TOWER_USERNAME=YOUR_TOWER_USERNAME\n# export TOWER_PASSWORD=YOUR_TOWER_PASSWORD\n# export TOWER_INVENTORY=THE_ID_OF_TARGETED_INVENTORY\n# Read the inventory specified in TOWER_INVENTORY from Ansible Tower, and list them.\n# The inventory path must always be @tower_inventory if you are reading all settings from environment variables.\n# ansible-inventory -i @tower_inventory --list\n'''\n\nimport os\n\nfrom ansible.module_utils import six\nfrom ansible.module_utils._text import to_text, to_native\nfrom ansible.errors import AnsibleParserError, AnsibleOptionsError\nfrom ansible.plugins.inventory import BaseInventoryPlugin\nfrom ansible.config.manager import ensure_type\n\nfrom ..module_utils.tower_api import TowerAPIModule\n\n\ndef handle_error(**kwargs):\n    raise AnsibleParserError(to_native(kwargs.get('msg')))\n\n\nclass InventoryModule(BaseInventoryPlugin):\n    NAME = 'awx.awx.tower'  # REPLACE\n    # Stays backward compatible with tower inventory script.\n    # If the user supplies '@tower_inventory' as path, the plugin will read from environment variables.\n    no_config_file_supplied = False\n\n    def verify_file(self, path):\n        if path.endswith('@tower_inventory'):\n            self.no_config_file_supplied = True\n            return True\n        elif super(InventoryModule, self).verify_file(path):\n            return path.endswith(('tower_inventory.yml', 'tower_inventory.yaml', 'tower.yml', 'tower.yaml'))\n        else:\n            return False\n\n    def warn_callback(self, warning):\n        self.display.warning(warning)\n\n    def parse(self, inventory, loader, path, cache=True):\n        super(InventoryModule, self).parse(inventory, loader, path)\n        if not self.no_config_file_supplied and os.path.isfile(path):\n            self._read_config_data(path)\n\n        # Defer processing of params to logic shared with the modules\n        module_params = {}\n        for plugin_param, module_param in TowerAPIModule.short_params.items():\n            opt_val = self.get_option(plugin_param)\n            if opt_val is not None:\n                module_params[module_param] = opt_val\n\n        module = TowerAPIModule(\n            argument_spec={}, direct_params=module_params,\n            error_callback=handle_error, warn_callback=self.warn_callback\n        )\n\n        # validate type of inventory_id because we allow two types as special case\n        inventory_id = self.get_option('inventory_id')\n        if isinstance(inventory_id, int):\n            inventory_id = to_text(inventory_id, nonstring='simplerepr')\n        else:\n            try:\n                inventory_id = ensure_type(inventory_id, 'str')\n            except ValueError as e:\n                raise AnsibleOptionsError(\n                    'Invalid type for configuration option inventory_id, '\n                    'not integer, and cannot convert to string: {err}'.format(err=to_native(e))\n                )\n        inventory_id = inventory_id.replace('/', '')\n        inventory_url = '/api/v2/inventories/{inv_id}/script/'.format(inv_id=inventory_id)\n\n        inventory = module.get_endpoint(\n            inventory_url, data={'hostvars': '1', 'towervars': '1', 'all': '1'}\n        )['json']\n\n        # To start with, create all the groups.\n        for group_name in inventory:\n            if group_name != '_meta':\n                self.inventory.add_group(group_name)\n\n        # Then, create all hosts and add the host vars.\n        all_hosts = inventory['_meta']['hostvars']\n        for host_name, host_vars in six.iteritems(all_hosts):\n            self.inventory.add_host(host_name)\n            for var_name, var_value in six.iteritems(host_vars):\n                self.inventory.set_variable(host_name, var_name, var_value)\n\n        # Lastly, create to group-host and group-group relationships, and set group vars.\n        for group_name, group_content in six.iteritems(inventory):\n            if group_name != 'all' and group_name != '_meta':\n                # First add hosts to groups\n                for host_name in group_content.get('hosts', []):\n                    self.inventory.add_host(host_name, group_name)\n                # Then add the parent-children group relationships.\n                for child_group_name in group_content.get('children', []):\n                    # add the child group to groups, if its already there it will just throw a warning\n                    self.inventory.add_group(child_group_name)\n                    self.inventory.add_child(group_name, child_group_name)\n            # Set the group vars. Note we should set group var for 'all', but not '_meta'.\n            if group_name != '_meta':\n                for var_name, var_value in six.iteritems(group_content.get('vars', {})):\n                    self.inventory.set_variable(group_name, var_name, var_value)\n\n        # Fetch extra variables if told to do so\n        if self.get_option('include_metadata'):\n\n            config_data = module.get_endpoint('/api/v2/config/')['json']\n\n            server_data = {}\n            server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown')\n            for key in ('version', 'ansible_version'):\n                server_data[key] = config_data.get(key, 'unknown')\n            self.inventory.set_variable('all', 'tower_metadata', server_data)\n\n        # Clean up the inventory.\n        self.inventory.reconcile_inventory()\n", "path": "awx_collection/plugins/inventory/tower.py"}]}