diff --git a/awx_collection/README.md b/awx_collection/README.md index 73efb5e2495c..767073c4ef93 100644 --- a/awx_collection/README.md +++ b/awx_collection/README.md @@ -32,11 +32,28 @@ Installing the `tar.gz` involves no special instructions. ## Running Non-deprecated modules in this collection have no Python requirements, but -may require the AWX CLI +may require the official [AWX CLI](https://pypi.org/project/awxkit/) in the future. The `DOCUMENTATION` for each module will report this. You can specify authentication by host, username, and password. +These can be specified via (from highest to lowest precedence): + + - direct module parameters + - environment variables (most useful when running against localhost) + - a config file path specified by the `tower_config_file` parameter + - a config file at `~/.tower_cli.cfg` + - a config file at `/etc/tower/tower_cli.cfg` + +Config file syntax looks like this: + +``` +[general] +host = https://localhost:8043 +verify_ssl = true +username = foo +password = bar +``` ## Release and Upgrade Notes @@ -46,6 +63,7 @@ Notable releases of the `awx.awx` collection: - 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/). - 19.2.1 large renaming purged "tower" names (like options and module names), adding redirects for old names - 21.11.0 "tower" modules deprecated and symlinks removed. + - 25.0.0 "token" and "application" modules have been removed as oauth is no longer supported, use basic auth instead - X.X.X added support of named URLs to all modules. Anywhere that previously accepted name or id can also support named URLs - 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable. diff --git a/awx_collection/TESTING.md b/awx_collection/TESTING.md index 71748769c38b..a77e4780d44f 100644 --- a/awx_collection/TESTING.md +++ b/awx_collection/TESTING.md @@ -109,7 +109,7 @@ rootdir: /home/student1/awx, configfile: pytest.ini plugins: cov-2.10.1, django-3.10.0, pythonpath-0.7.3, mock-1.11.1, timeout-1.4.2, forked-1.3.0, xdist-1.34.0 collected 116 items -awx_collection/test/awx/test_application.py::test_create_application PASSED [ 0%] +awx_collection/test/awx/test_ad_hoc_wait.py::test_ad_hoc_wait_successful PASSED [ 0%] awx_collection/test/awx/test_completeness.py::test_completeness PASSED [ 1%] ... @@ -124,18 +124,18 @@ FAILED awx_collection/test/awx/test_module_utils.py::test_type_warning - SystemE make: *** [Makefile:382: test_collection] Error 1 ``` -In addition to running all of the tests, you can also specify specific tests to run. This is useful when developing a single module. In this example, we will run the tests for the `token` module: +In addition to running all of the tests, you can also specify specific tests to run. This is useful when developing a single module. In this example, we will run the tests for the `project` module: ``` -$ pytest awx_collection/test/awx/test_token.py +$ pytest awx_collection/test/awx/test_project.py ============================ test session starts ============================ platform darwin -- Python 3.7.0, pytest-3.6.0, py-1.8.1, pluggy-0.6.0 django: settings: awx.settings.development (from ini) rootdir: /Users/jowestco/junk/awx, inifile: pytest.ini plugins: xdist-1.27.0, timeout-1.3.4, pythonpath-0.7.3, mock-1.11.1, forked-1.1.3, django-3.9.0, cov-2.8.1 -collected 1 item +collected 1 item -awx_collection/test/awx/test_token.py . [100%] +awx_collection/test/awx/test_project.py . [100%] ========================= 1 passed in 1.72 seconds ========================= ``` diff --git a/awx_collection/meta/runtime.yml b/awx_collection/meta/runtime.yml index 7ffdbce161b8..23c0f42cbb49 100644 --- a/awx_collection/meta/runtime.yml +++ b/awx_collection/meta/runtime.yml @@ -5,7 +5,6 @@ action_groups: - ad_hoc_command - ad_hoc_command_cancel - ad_hoc_command_wait - - application - bulk_job_launch - bulk_host_create - bulk_host_delete @@ -42,7 +41,6 @@ action_groups: - settings - subscriptions - team - - token - user - workflow_approval - workflow_job_template_node diff --git a/awx_collection/plugins/doc_fragments/auth.py b/awx_collection/plugins/doc_fragments/auth.py index 763fe94dcddf..f8241e1c7841 100644 --- a/awx_collection/plugins/doc_fragments/auth.py +++ b/awx_collection/plugins/doc_fragments/auth.py @@ -32,16 +32,6 @@ class ModuleDocFragment(object): - If value not set, will try environment variable C(CONTROLLER_PASSWORD) and then config files type: str aliases: [ tower_password ] - controller_oauthtoken: - description: - - The OAuth token to use. - - This value can be in one of two formats. - - A string which is the token itself. (i.e. bqV5txm97wqJqtkxlMkhQz0pKhRMMX) - - A dictionary structure as returned by the token module. - - If value not set, will try environment variable C(CONTROLLER_OAUTH_TOKEN) and then config files - type: raw - version_added: "3.7.0" - aliases: [ tower_oauthtoken ] validate_certs: description: - Whether to allow insecure connections to AWX. diff --git a/awx_collection/plugins/doc_fragments/auth_plugin.py b/awx_collection/plugins/doc_fragments/auth_plugin.py index b46eaf6bbf03..49118baf714f 100644 --- a/awx_collection/plugins/doc_fragments/auth_plugin.py +++ b/awx_collection/plugins/doc_fragments/auth_plugin.py @@ -43,17 +43,6 @@ class ModuleDocFragment(object): version: '4.0.0' why: Collection name change alternatives: 'CONTROLLER_PASSWORD' - oauth_token: - description: - - The OAuth token to use. - env: - - name: CONTROLLER_OAUTH_TOKEN - - name: TOWER_OAUTH_TOKEN - deprecated: - collection_name: 'awx.awx' - version: '4.0.0' - why: Collection name change - alternatives: 'CONTROLLER_OAUTH_TOKEN' verify_ssl: description: - Specify whether Ansible should verify the SSL certificate of the controller host. diff --git a/awx_collection/plugins/lookup/controller_api.py b/awx_collection/plugins/lookup/controller_api.py index 4d5d79cf9181..d1b213f12bfe 100644 --- a/awx_collection/plugins/lookup/controller_api.py +++ b/awx_collection/plugins/lookup/controller_api.py @@ -18,7 +18,7 @@ options: _terms: description: - - The endpoint to query, i.e. teams, users, tokens, job_templates, etc. + - The endpoint to query, i.e. teams, users, job_templates, etc. required: True query_params: description: diff --git a/awx_collection/plugins/module_utils/awxkit.py b/awx_collection/plugins/module_utils/awxkit.py index 02549b73e34c..ad9c3a9cc48d 100644 --- a/awx_collection/plugins/module_utils/awxkit.py +++ b/awx_collection/plugins/module_utils/awxkit.py @@ -33,13 +33,8 @@ def __init__(self, argument_spec, **kwargs): def authenticate(self): try: - if self.oauth_token: - # MERGE: fix conflicts with removal of OAuth2 token from collection branch - self.connection.login(None, None) - self.authenticated = True - elif self.username: - self.connection.login(username=self.username, password=self.password) - self.authenticated = True + self.connection.login(username=self.username, password=self.password) + self.authenticated = True except Exception: self.fail_json("Failed to authenticate") diff --git a/awx_collection/plugins/module_utils/controller_api.py b/awx_collection/plugins/module_utils/controller_api.py index c2681a49afe9..2e9cdd126cb1 100644 --- a/awx_collection/plugins/module_utils/controller_api.py +++ b/awx_collection/plugins/module_utils/controller_api.py @@ -6,7 +6,7 @@ from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError from ansible.module_utils.parsing.convert_bool import boolean as strtobool from ansible.module_utils.six import PY2 -from ansible.module_utils.six import raise_from, string_types +from ansible.module_utils.six import raise_from from ansible.module_utils.six.moves import StringIO from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.six.moves.http_cookiejar import CookieJar @@ -55,9 +55,6 @@ class ControllerModule(AnsibleModule): controller_password=dict(no_log=True, aliases=['tower_password'], required=False, fallback=(env_fallback, ['CONTROLLER_PASSWORD', 'TOWER_PASSWORD'])), validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['CONTROLLER_VERIFY_SSL', 'TOWER_VERIFY_SSL'])), request_timeout=dict(type='float', required=False, fallback=(env_fallback, ['CONTROLLER_REQUEST_TIMEOUT'])), - controller_oauthtoken=dict( - type='raw', no_log=True, aliases=['tower_oauthtoken'], required=False, fallback=(env_fallback, ['CONTROLLER_OAUTH_TOKEN', 'TOWER_OAUTH_TOKEN']) - ), controller_config_file=dict(type='path', aliases=['tower_config_file'], required=False, default=None), ) # Associations of these types are ordered and have special consideration in the modified associations function @@ -68,15 +65,12 @@ class ControllerModule(AnsibleModule): 'password': 'controller_password', 'verify_ssl': 'validate_certs', 'request_timeout': 'request_timeout', - 'oauth_token': 'controller_oauthtoken', } host = '127.0.0.1' username = None password = None verify_ssl = True request_timeout = 10 - oauth_token = None - oauth_token_id = None authenticated = False config_name = 'tower_cli.cfg' version_checked = False @@ -111,20 +105,6 @@ def __init__(self, argument_spec=None, direct_params=None, error_callback=None, if direct_value is not None: setattr(self, short_param, direct_value) - # Perform magic depending on whether controller_oauthtoken is a string or a dict - if self.params.get('controller_oauthtoken'): - token_param = self.params.get('controller_oauthtoken') - if isinstance(token_param, dict): - if 'token' in token_param: - self.oauth_token = self.params.get('controller_oauthtoken')['token'] - else: - self.fail_json(msg="The provided dict in controller_oauthtoken did not properly contain the token entry") - elif isinstance(token_param, string_types): - self.oauth_token = self.params.get('controller_oauthtoken') - else: - error_msg = "The provided controller_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) - self.fail_json(msg=error_msg) - # Perform some basic validation if not re.match('^https{0,1}://', self.host): self.host = "https://{0}".format(self.host) @@ -312,9 +292,6 @@ class ControllerAPIModule(ControllerModule): IDENTITY_FIELDS = {'users': 'username', 'workflow_job_template_nodes': 'identifier', 'instances': 'hostname'} ENCRYPTED_STRING = "$encrypted$" - # which app was used to create the oauth_token - oauth_token_app_key = None - def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): kwargs['supports_check_mode'] = True @@ -338,8 +315,7 @@ def get_item_name(self, item, allow_unknown=False): for field_name in ControllerAPIModule.IDENTITY_FIELDS.values(): if field_name in item: return item[field_name] - - if item.get('type', None) in ('o_auth2_access_token', 'credential_input_source'): + if item.get('type', None) == 'credential_input_source': return item['id'] if allow_unknown: @@ -498,15 +474,12 @@ def make_request(self, method, endpoint, *args, **kwargs): # Extract the headers, this will be used in a couple of places headers = kwargs.get('headers', {}) - # Authenticate to AWX (if we don't have a token and if not already done so) - if not self.oauth_token and not self.authenticated: - # This method will set a cookie in the cookie jar for us and also an oauth_token when possible + # Authenticate to AWX (if not already done so) + if not self.authenticated: + # This method will set a cookie in the cookie jar for us self.authenticate(**kwargs) - if self.oauth_token: - # If we have a oauth token, we just use a bearer header - headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token) - elif self.username and self.password: - headers['Authorization'] = self._get_basic_authorization_header() + + headers['Authorization'] = self._get_basic_authorization_header() if method in ['POST', 'PUT', 'PATCH']: headers.setdefault('Content-Type', 'application/json') @@ -665,71 +638,11 @@ def _authenticate_with_basic_auth(self): }, ) - def _authenticate_create_token(self, app_key=None): - # in case of failure and to give a chance to authenticate via other means, should not raise exceptions - # but only warnings - if self.username and self.password: - login_data = { - "description": "Automation Platform Controller Module Token", - "application": None, - "scope": "write", - } - - api_token_url = self.build_url("tokens", app_key=app_key).geturl() - try: - response = self.session.open( - 'POST', - api_token_url, - validate_certs=self.verify_ssl, - timeout=self.request_timeout, - follow_redirects=True, - data=dumps(login_data), - headers={ - "Content-Type": "application/json", - "Authorization": self._get_basic_authorization_header(), - }, - ) - - except Exception as exp: - self.warn("url: {0} - Failed to get token: {1}".format(api_token_url, exp)) - return - - token_response = None - try: - token_response = response.read() - response_json = loads(token_response) - self.oauth_token_id = response_json['id'] - self.oauth_token = response_json['token'] - # set the app that received the token create request, this is needed when removing the token at logout - self.oauth_token_app_key = app_key - except Exception as exp: - self.warn( - "url: {0} - Failed to extract token information from login response: {1}, response: {2}".format( - api_token_url, exp, token_response, - ) - ) - return - - return None - def authenticate(self, **kwargs): - # As a temporary solution for version 4.6 try to get a token by using basic authentication from: - # /api/gateway/v1/tokens/ when app_key is gateway - # /api/v2/tokens/ when app_key is None and _COLLECTION_TYPE = "awx" - # /api/controller/v2/tokens/ when app_key is None and _COLLECTION_TYPE != "awx" - for app_key in ["gateway", None]: - # to give a chance to authenticate via basic authentication in case of failure, - # _authenticate_create_token, should not raise exception but only warnings, - self._authenticate_create_token(app_key=app_key) - if self.oauth_token: - break - - if not self.oauth_token: - # if not having an oauth_token and when collection_type is awx try to login with basic authentication - try: - self._authenticate_with_basic_auth() - except Exception as exp: - self.fail_json(msg='Failed to get user info: {0}'.format(exp)) + try: + self._authenticate_with_basic_auth() + except Exception as exp: + self.fail_json(msg='Failed to get user info: {0}'.format(exp)) self.authenticated = True @@ -1080,37 +993,7 @@ def create_or_update_if_needed( ) def logout(self): - if self.authenticated and self.oauth_token_id: - # Attempt to delete our current token from /api/v2/tokens/ - # Post to the tokens endpoint with baisc auth to try and get a token - api_token_url = self.build_url( - "tokens/{0}/".format(self.oauth_token_id), - app_key=self.oauth_token_app_key, - ).geturl() - - try: - self.session.open( - 'DELETE', - api_token_url, - validate_certs=self.verify_ssl, - timeout=self.request_timeout, - follow_redirects=True, - headers={ - "Authorization": self._get_basic_authorization_header(), - } - ) - self.oauth_token_id = None - self.oauth_token = None - self.authenticated = False - except HTTPError as he: - try: - resp = he.read() - except Exception as e: - resp = 'unknown {0}'.format(e) - self.warn('Failed to release token: {0}, response: {1}'.format(he, resp)) - except (Exception) as e: - # Sanity check: Did the server send back some kind of internal error? - self.warn('Failed to release token {0}: {1}'.format(self.oauth_token_id, e)) + self.authenticated = False def is_job_done(self, job_status): if job_status in ['new', 'pending', 'waiting', 'running']: diff --git a/awx_collection/plugins/modules/application.py b/awx_collection/plugins/modules/application.py deleted file mode 100644 index 4c858394d81c..000000000000 --- a/awx_collection/plugins/modules/application.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/python -# coding: utf-8 -*- - -# (c) 2020,Geoffrey Bachelot -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: application -author: "Geoffrey Bacheot (@jffz)" -short_description: create, update, or destroy Automation Platform Controller applications -description: - - Create, update, or destroy Automation Platform Controller applications. See - U(https://www.ansible.com/tower) for an overview. -options: - name: - description: - - Name of the application. - required: True - type: str - new_name: - description: - - Setting this option will change the existing name (looked up via the name field. - type: str - description: - description: - - Description of the application. - type: str - authorization_grant_type: - description: - - The grant type the user must use for acquire tokens for this application. - choices: ["password", "authorization-code"] - type: str - required: False - client_type: - description: - - Set to public or confidential depending on how secure the client device is. - choices: ["public", "confidential"] - type: str - required: False - organization: - description: - - Name, ID, or named URL of organization for application. - type: str - required: True - redirect_uris: - description: - - Allowed urls list, space separated. Required when authorization-grant-type=authorization-code - type: list - elements: str - state: - description: - - Desired state of the resource. - default: "present" - choices: ["present", "absent", "exists"] - type: str - skip_authorization: - description: - - Set True to skip authorization step for completely trusted applications. - type: bool - -extends_documentation_fragment: awx.awx.auth -''' - - -EXAMPLES = ''' -- name: Add Foo application - application: - name: "Foo" - description: "Foo bar application" - organization: "test" - state: present - authorization_grant_type: password - client_type: public - -- name: Add Foo application - application: - name: "Foo" - description: "Foo bar application" - organization: "test" - state: present - authorization_grant_type: authorization-code - client_type: confidential - redirect_uris: - - http://tower.com/api/v2/ -''' - -from ..module_utils.controller_api import ControllerAPIModule - - -def main(): - # Any additional arguments that are not fields of the item can be added here - argument_spec = dict( - name=dict(required=True), - new_name=dict(), - description=dict(), - authorization_grant_type=dict(choices=["password", "authorization-code"]), - client_type=dict(choices=['public', 'confidential']), - organization=dict(required=True), - redirect_uris=dict(type="list", elements='str'), - state=dict(choices=['present', 'absent', 'exists'], default='present'), - skip_authorization=dict(type='bool'), - ) - - # Create a module for ourselves - module = ControllerAPIModule(argument_spec=argument_spec) - - # Extract our parameters - name = module.params.get('name') - new_name = module.params.get("new_name") - description = module.params.get('description') - authorization_grant_type = module.params.get('authorization_grant_type') - client_type = module.params.get('client_type') - organization = module.params.get('organization') - redirect_uris = module.params.get('redirect_uris') - skip_authorization = module.params.get('skip_authorization') - state = module.params.get('state') - - # Attempt to look up the related items the user specified (these will fail the module if not found) - org_id = module.resolve_name_to_id('organizations', organization) - - # Attempt to look up application based on the provided name and org ID - application = module.get_one('applications', name_or_id=name, check_exists=(state == 'exists'), **{'data': {'organization': org_id}}) - - if state == 'absent': - # If the state was absent we can let the module delete it if needed, the module will handle exiting from this - module.delete_if_needed(application) - - # Create the data that gets sent for create and update - application_fields = { - 'name': new_name if new_name else (module.get_item_name(application) if application else name), - 'organization': org_id, - } - if authorization_grant_type is not None: - application_fields['authorization_grant_type'] = authorization_grant_type - if client_type is not None: - application_fields['client_type'] = client_type - if description is not None: - application_fields['description'] = description - if redirect_uris is not None: - application_fields['redirect_uris'] = ' '.join(redirect_uris) - if skip_authorization is not None: - application_fields['skip_authorization'] = skip_authorization - - response = module.create_or_update_if_needed(application, application_fields, endpoint='applications', item_type='application', auto_exit=False) - if 'client_id' in response: - module.json_output['client_id'] = response['client_id'] - if 'client_secret' in response: - module.json_output['client_secret'] = response['client_secret'] - module.exit_json(**module.json_output) - - -if __name__ == '__main__': - main() diff --git a/awx_collection/plugins/modules/token.py b/awx_collection/plugins/modules/token.py deleted file mode 100644 index b6e13cd02c39..000000000000 --- a/awx_collection/plugins/modules/token.py +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/python -# coding: utf-8 -*- - - -# (c) 2020, John Westcott IV -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: token -author: "John Westcott IV (@john-westcott-iv)" -version_added: "2.3.0" -short_description: create, update, or destroy Automation Platform Controller tokens. -description: - - Create or destroy Automation Platform Controller tokens. See - U(https://www.ansible.com/tower) for an overview. - - In addition, the module sets an Ansible fact which can be passed into other - controller modules as the parameter controller_oauthtoken. See examples for usage. - - Because of the sensitive nature of tokens, the created token value is only available once - through the Ansible fact. (See RETURN for details) - - Due to the nature of tokens this module is not idempotent. A second will - with the same parameters will create a new token. - - If you are creating a temporary token for use with modules you should delete the token - when you are done with it. See the example for how to do it. -options: - description: - description: - - Optional description of this access token. - required: False - type: str - application: - description: - - The application name, ID, or named URL tied to this token. - required: False - type: str - scope: - description: - - Allowed scopes, further restricts user's permissions. Must be a simple space-separated string with allowed scopes ['read', 'write']. - required: False - type: str - choices: ["read", "write"] - existing_token: - description: The data structure produced from token in create mode to be used with state absent. - type: dict - existing_token_id: - description: A token ID (number) which can be used to delete an arbitrary token with state absent. - type: str - state: - description: - - Desired state of the resource. - choices: ["present", "absent"] - default: "present" - type: str -extends_documentation_fragment: awx.awx.auth -''' - -EXAMPLES = ''' -- block: - - name: Create a new token using an existing token - token: - description: '{{ token_description }}' - scope: "write" - state: present - controller_oauthtoken: "{{ my_existing_token }}" - - - name: Delete this token - token: - existing_token: "{{ controller_token }}" - state: absent - - - name: Create a new token using username/password - token: - description: '{{ token_description }}' - scope: "write" - state: present - controller_username: "{{ my_username }}" - controller_password: "{{ my_password }}" - - - name: Use our new token to make another call - job_list: - controller_oauthtoken: "{{ controller_token }}" - - always: - - name: Delete our Token with the token we created - token: - existing_token: "{{ controller_token }}" - state: absent - when: token is defined - -- name: Delete a token by its id - token: - existing_token_id: 4 - state: absent -''' - -RETURN = ''' -controller_token: - type: dict - description: An Ansible Fact variable representing a token object which can be used for auth in subsequent modules. See examples for usage. - contains: - token: - description: The token that was generated. This token can never be accessed again, make sure this value is noted before it is lost. - type: str - id: - description: The numeric ID of the token created - type: str - returned: on successful create -''' - -from ..module_utils.controller_api import ControllerAPIModule - - -def return_token(module, last_response): - # A token is special because you can never get the actual token ID back from the API. - # So the default module return would give you an ID but then the token would forever be masked on you. - # This method will return the entire token object we got back so that a user has access to the token - - module.json_output['ansible_facts'] = { - 'controller_token': last_response, - 'tower_token': last_response, - } - module.exit_json(**module.json_output) - - -def main(): - # Any additional arguments that are not fields of the item can be added here - argument_spec = dict( - description=dict(), - application=dict(), - scope=dict(choices=['read', 'write']), - existing_token=dict(type='dict', no_log=False), - existing_token_id=dict(), - state=dict(choices=['present', 'absent'], default='present'), - ) - - # Create a module for ourselves - module = ControllerAPIModule( - argument_spec=argument_spec, - mutually_exclusive=[ - ('existing_token', 'existing_token_id'), - ], - # If we are state absent make sure one of existing_token or existing_token_id are present - required_if=[ - [ - 'state', - 'absent', - ('existing_token', 'existing_token_id'), - True, - ], - ], - ) - - # Extract our parameters - description = module.params.get('description') - application = module.params.get('application') - scope = module.params.get('scope') - existing_token = module.params.get('existing_token') - existing_token_id = module.params.get('existing_token_id') - state = module.params.get('state') - - if state == 'absent': - if not existing_token: - existing_token = module.get_one( - 'tokens', - **{ - 'data': { - 'id': existing_token_id, - } - } - ) - - # If the state was absent we can let the module delete it if needed, the module will handle exiting from this - module.delete_if_needed(existing_token) - - # Attempt to look up the related items the user specified (these will fail the module if not found) - application_id = None - if application: - application_id = module.resolve_name_to_id('applications', application) - - # Create the data that gets sent for create and update - new_fields = {} - if description is not None: - new_fields['description'] = description - if application is not None: - new_fields['application'] = application_id - if scope is not None: - new_fields['scope'] = scope - - # If the state was present and we can let the module build or update the existing item, this will return on its own - module.create_or_update_if_needed( - None, - new_fields, - endpoint='tokens', - item_type='token', - associations={}, - on_create=return_token, - ) - - -if __name__ == '__main__': - main() diff --git a/awx_collection/test/awx/test_application.py b/awx_collection/test/awx/test_application.py deleted file mode 100644 index c93e2f333365..000000000000 --- a/awx_collection/test/awx/test_application.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -import pytest - -from awx.main.models import Organization -from awx.main.models.oauth import OAuth2Application - - -@pytest.mark.django_db -def test_create_application(run_module, admin_user): - org = Organization.objects.create(name='foo') - - module_args = { - 'name': 'foo_app', - 'description': 'barfoo', - 'state': 'present', - 'authorization_grant_type': 'password', - 'client_type': 'public', - 'organization': 'foo', - } - - result = run_module('application', module_args, admin_user) - assert result.get('changed'), result - - application = OAuth2Application.objects.get(name='foo_app') - assert application.description == 'barfoo' - assert application.organization_id == org.id diff --git a/awx_collection/test/awx/test_completeness.py b/awx_collection/test/awx/test_completeness.py index 42d013df6cbb..132e2bb4e08a 100644 --- a/awx_collection/test/awx/test_completeness.py +++ b/awx_collection/test/awx/test_completeness.py @@ -21,7 +21,9 @@ # If a module should not be created for an endpoint and the endpoint is not read-only add it here # THINK HARD ABOUT DOING THIS no_module_for_endpoint = [ + 'application', # Usage of OAuth tokens is deprecated 'constructed_inventory', # This is a view for inventory with kind=constructed + 'token', # Usage of OAuth tokens is deprecated ] # Some modules work on the related fields of an endpoint. These modules will not have an auto-associated endpoint @@ -61,8 +63,6 @@ no_api_parameter_ok = { # The wait is for whether or not to wait for a project update on change 'project': ['wait', 'interval', 'update_project'], - # Existing_token and id are for working with an existing tokens - 'token': ['existing_token', 'existing_token_id'], # /survey spec is now how we handle associations # We take an organization here to help with the lookups only 'job_template': ['survey_spec', 'organization'], diff --git a/awx_collection/test/awx/test_organization.py b/awx_collection/test/awx/test_organization.py index e6b3cc5e2dac..a38ca3dbe108 100644 --- a/awx_collection/test/awx/test_organization.py +++ b/awx_collection/test/awx/test_organization.py @@ -20,7 +20,6 @@ def test_create_organization(run_module, admin_user): 'controller_username': None, 'controller_password': None, 'validate_certs': None, - 'controller_oauthtoken': None, 'controller_config_file': None, } @@ -53,7 +52,6 @@ def test_galaxy_credential_order(run_module, admin_user): 'controller_username': None, 'controller_password': None, 'validate_certs': None, - 'controller_oauthtoken': None, 'controller_config_file': None, 'galaxy_credentials': cred_ids, } @@ -78,7 +76,6 @@ def test_galaxy_credential_order(run_module, admin_user): 'controller_username': None, 'controller_password': None, 'validate_certs': None, - 'controller_oauthtoken': None, 'controller_config_file': None, 'galaxy_credentials': cred_ids, } diff --git a/awx_collection/tests/integration/targets/application/tasks/main.yml b/awx_collection/tests/integration/targets/application/tasks/main.yml deleted file mode 100644 index e2e82eab8287..000000000000 --- a/awx_collection/tests/integration/targets/application/tasks/main.yml +++ /dev/null @@ -1,131 +0,0 @@ ---- -- name: Generate a test id - set_fact: - test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - when: test_id is not defined - -- name: Generate names - set_fact: - app1_name: "AWX-Collection-tests-application-app1-{{ test_id }}" - app2_name: "AWX-Collection-tests-application-app2-{{ test_id }}" - app3_name: "AWX-Collection-tests-application-app3-{{ test_id }}" - -- block: - - name: Create an application - application: - name: "{{ app1_name }}" - authorization_grant_type: "password" - client_type: "public" - organization: "Default" - state: present - register: result - - - assert: - that: - - "result is changed" - - - name: Run an application with exists - application: - name: "{{ app1_name }}" - authorization_grant_type: "password" - client_type: "public" - organization: "Default" - state: exists - register: result - - - assert: - that: - - "result is not changed" - - - name: Delete our application - application: - name: "{{ app1_name }}" - organization: "Default" - state: absent - register: result - - - assert: - that: - - "result is changed" - - - name: Run an application with exists - application: - name: "{{ app1_name }}" - authorization_grant_type: "password" - client_type: "public" - organization: "Default" - state: exists - register: result - - - assert: - that: - - "result is changed" - - - name: Delete our application - application: - name: "{{ app1_name }}" - organization: "Default" - state: absent - register: result - - - assert: - that: - - "result is changed" - - - name: Create a second application - application: - name: "{{ app2_name }}" - authorization_grant_type: "authorization-code" - client_type: "confidential" - organization: "Default" - description: "Another application" - redirect_uris: - - http://tower.com/api/v2/ - - http://tower.com/api/v2/teams - state: present - register: result - - - assert: - that: - - "result is changed" - - - name: Create an all trusting application - application: - name: "{{ app3_name }}" - organization: "Default" - description: "All Trusting Application" - skip_authorization: true - authorization_grant_type: "password" - client_type: "confidential" - state: present - register: result - - - assert: - that: - - "result is changed" - - "'client_secret' in result" - - - name: Rename an inventory - application: - name: "{{ app3_name }}" - new_name: "{{ app3_name }}a" - organization: Default - state: present - register: result - - - assert: - that: - - result.changed - - always: - - name: Delete our application - application: - name: "{{ item }}" - organization: "Default" - state: absent - register: result - loop: - - "{{ app1_name }}" - - "{{ app2_name }}" - - "{{ app3_name }}" - - "{{ app3_name }}a" diff --git a/awx_collection/tests/integration/targets/token/tasks/main.yml b/awx_collection/tests/integration/targets/token/tasks/main.yml deleted file mode 100644 index 9cd4972a931c..000000000000 --- a/awx_collection/tests/integration/targets/token/tasks/main.yml +++ /dev/null @@ -1,115 +0,0 @@ ---- -- name: Generate a test ID - set_fact: - test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - when: test_id is not defined - -- name: Generate names - set_fact: - token_description: "AWX-Collection-tests-token-description-{{ test_id }}" - -- name: Try to use a token as a dict which is missing the token parameter - job_list: - controller_oauthtoken: - not_token: "This has no token entry" - register: results - ignore_errors: true - -- assert: - that: - - results is failed - - '"The provided dict in controller_oauthtoken did not properly contain the token entry" == results.msg' - -- name: Try to use a token as a list - job_list: - controller_oauthtoken: - - dummy_token - register: results - ignore_errors: true - -- assert: - that: - - results is failed - - '"The provided controller_oauthtoken type was not valid (list). Valid options are str or dict." == results.msg' - -- name: Try to delete a token with no existing_token or existing_token_id - token: - state: absent - register: results - ignore_errors: true - -- assert: - that: - - results is failed - # We don't assert a message here because it's handled by ansible - -- name: Try to delete a token with both existing_token or existing_token_id - token: - existing_token: - id: 1234 - existing_token_id: 1234 - state: absent - register: results - ignore_errors: true - -- assert: - that: - - results is failed - # We don't assert a message here because it's handled by ansible - - -- block: - - name: Create a Token - token: - description: '{{ token_description }}' - scope: "write" - state: present - register: new_token - - - name: Validate our token works by token - job_list: - controller_oauthtoken: "{{ controller_token.token }}" - register: job_list - - - name: Validate our token works by object - job_list: - controller_oauthtoken: "{{ controller_token }}" - register: job_list - - always: - - name: Delete our Token with our own token - token: - existing_token: "{{ controller_token }}" - controller_oauthtoken: "{{ controller_token }}" - state: absent - when: controller_token is defined - register: results - - - assert: - that: - - results is changed or results is skipped - -- block: - - name: Create a second token - token: - description: '{{ token_description }}' - scope: "write" - state: present - register: results - - - assert: - that: - - results is changed - - always: - - name: Delete the second Token with our own token - token: - existing_token_id: "{{ controller_token['id'] }}" - controller_oauthtoken: "{{ controller_token }}" - state: absent - when: controller_token is defined - register: results - - - assert: - that: - - results is changed or resuslts is skipped diff --git a/awx_collection/tests/integration/targets/user/tasks/main.yml b/awx_collection/tests/integration/targets/user/tasks/main.yml index a3fae666b0e9..2d88bb199ab5 100644 --- a/awx_collection/tests/integration/targets/user/tasks/main.yml +++ b/awx_collection/tests/integration/targets/user/tasks/main.yml @@ -220,7 +220,6 @@ user: controller_username: "{{ username }}-orgadmin" controller_password: "{{ username }}-orgadmin" - controller_oauthtoken: false # Hack for CI where we use oauth in config file username: "{{ username }}" first_name: Joe password: "{{ 65535 | random | to_uuid }}" diff --git a/awx_collection/tools/roles/generate/templates/module.j2 b/awx_collection/tools/roles/generate/templates/module.j2 index 57e6bc4c7799..6efe6340e952 100644 --- a/awx_collection/tools/roles/generate/templates/module.j2 +++ b/awx_collection/tools/roles/generate/templates/module.j2 @@ -82,11 +82,6 @@ options: choices: ["present", "absent"] default: "present" type: str - controller_oauthtoken: - description: - - The OAuth token to use. - required: False - type: str extends_documentation_fragment: awx.awx.auth ''' diff --git a/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 b/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 index e7c232b26395..def1d0c64d43 100644 --- a/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 +++ b/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 @@ -37,11 +37,28 @@ This collection should be installed from [Content Hub](https://cloud.redhat.com/ ## Running Non-deprecated modules in this collection have no Python requirements, but -may require the AWX CLI +may require the official [AWX CLI](https://pypi.org/project/awxkit/) in the future. The `DOCUMENTATION` for each module will report this. You can specify authentication by host, username, and password. +These can be specified via (from highest to lowest precedence): + + - direct module parameters + - environment variables (most useful when running against localhost) + - a config file path specified by the `tower_config_file` parameter + - a config file at `~/.tower_cli.cfg` + - a config file at `/etc/tower/tower_cli.cfg` + +Config file syntax looks like this: + +``` +[general] +host = https://localhost:8043 +verify_ssl = true +username = foo +password = bar +``` ## Release and Upgrade Notes @@ -52,12 +69,14 @@ Notable releases of the `{{ collection_namespace }}.{{ collection_package }}` co - 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/). - 19.2.1 large renaming purged "tower" names (like options and module names), adding redirects for old names - 21.11.0 "tower" modules deprecated and symlinks removed. + - 25.0.0 "token" and "application" modules have been removed as oauth is no longer supported, use basic auth instead - X.X.X added support of named URLs to all modules. Anywhere that previously accepted name or id can also support named URLs - 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable. {% else %} - 3.7.0 initial release - 4.0.0 ansible.tower renamed to ansible.controller - tower_ prefix is dropped from the module names, e.g. tower_inventory becomes inventory + - 4.7.0 "token" module has been removed as oauth is no longer supported, use basic auth instead {% endif %} The following notes are changes that may require changes to playbooks: @@ -90,7 +109,7 @@ The following notes are changes that may require changes to playbooks: - The `notification_configuration` parameter of `tower_notification_template` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict. - `tower_credential` no longer supports passing a file name to `ssh_key_data`. - The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification_template` module. - - Lookup plugins now always reutrn a list, and if you want a scalar value use `lookup` as opposed to `query` + - Lookup plugins now always return a list, and if you want a scalar value use `lookup` as opposed to `query` {% if collection_package | lower() == "awx" %} ## Running Unit Tests