From 6c2a88858e5150c700a711d185d435dc43d77276 Mon Sep 17 00:00:00 2001 From: Pablo Rodriguez Nava Date: Tue, 7 Jan 2025 17:44:34 +0100 Subject: [PATCH] [repo_setup] Download rhos-release using kerberos If the RPM name points to a URL we now use a custom plugin to fetch the content. If the endpoint challenges the plugins with SPNEGO authentication and a kerberos ticket is present the plugin will authenticate itself using the ticket. --- docs/dictionary/en-custom.txt | 11 ++ plugins/modules/url_request.py | 192 ++++++++++++++++++++++++ roles/repo_setup/README.md | 1 + roles/repo_setup/defaults/main.yml | 1 + roles/repo_setup/tasks/rhos_release.yml | 23 ++- tests/sanity/ignore.txt | 1 + 6 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 plugins/modules/url_request.py diff --git a/docs/dictionary/en-custom.txt b/docs/dictionary/en-custom.txt index 38d65a19fd..c4751c2a6d 100644 --- a/docs/dictionary/en-custom.txt +++ b/docs/dictionary/en-custom.txt @@ -1,6 +1,7 @@ aaabbcc abcdefghij addr +afuscoar alertmanager ansible ansibleee @@ -105,6 +106,7 @@ ctl ctlplane ctrl ctx +cve customizations dashboard dataplane @@ -165,6 +167,7 @@ fbqufbqkfbzxrja fci fedoraproject fil +filesystem fips firewalld flbxutz @@ -174,6 +177,7 @@ freefonts frmo fsid fultonj +fusco fwcybtb gapped genericcloud @@ -296,6 +300,7 @@ manpage mawxlihjizaogicbjyxbzig mawxlihjizcbwb maxdepth +mcs mellanox metallb metalsmith @@ -303,6 +308,7 @@ mgmt mins minsizegigabytes mlnx +mls modprobe mountpoints mtcylje @@ -464,7 +470,11 @@ scansettingbinding scap scp sdk +selevel selinux +serole +setype +seuser sha shiftstack shiftstackclient @@ -473,6 +483,7 @@ sizepercent skbg skiplist specificities +spnego spxzvbhvtzxmsihbyb src sshkey diff --git a/plugins/modules/url_request.py b/plugins/modules/url_request.py new file mode 100644 index 0000000000..bb5a62c324 --- /dev/null +++ b/plugins/modules/url_request.py @@ -0,0 +1,192 @@ +#!/usr/bin/python + +# Copyright: (c) 2025, Pablo Rodriguez +# Apache License Version 2.0 (see LICENSE) + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: url_request +short_description: Downloads/fetches the content of a SPNEGO secured URL +extends_documentation_fragment: + - files + +description: +- Downloads/fetches the content of a SPNEGO secured URL +- A kerberos ticket should be already issued + +author: + - Adrian Fusco (@afuscoar) + - Pablo Rodriguez (@pablintino) + +options: + url: + description: + - The URL to retrieve the content from + required: True + type: str + verify_ssl: + description: + - Enables/disables using TLS to reach the URL + required: False + type: bool + default: true + dest: + description: + - Path to the destination file/dir where the content should be downloaded + - If not provided the content won't be written into disk + required: False + type: str + +""" + +EXAMPLES = r""" +- name: Get some content + url_request: + url: "http://someurl.local/resource" + dest: "{{ ansible_user_dir }}/content.raw" + mode: "0644" + register: _fetched_content + +- name: Show base64 content + debug: + msg: "{{ _fetched_content.response_b64 }}" +""" + +RETURN = r""" +status_code: + description: HTTP response code + type: int + returned: returned request +content_type: + description: HTTP response Content-Type header content + type: str + returned: returned request +headers: + description: HTTP response headers + type: dict + returned: returned request +response_b64: + description: Returned content base64 encoded + type: str + returned: successful request +response_json: + description: Returned content as a dict + type: str + returned: successful request that returned application/json +path: + description: Written file path + type: str + returned: successful request +""" + +import base64 +import os.path +import re + +from ansible.module_utils.basic import AnsibleModule + + +try: + from requests import get + + python_requests_installed = True +except ImportError: + python_requests_installed = False +try: + from requests_kerberos import HTTPKerberosAuth, OPTIONAL + + python_requests_kerberos_installed = True +except ImportError: + python_requests_kerberos_installed = False + + +def main(): + module_args = { + "url": {"type": "str", "required": True}, + "verify_ssl": {"type": "bool", "default": True}, + "dest": {"type": "str", "required": False}, + } + + result = { + "changed": False, + } + + module = AnsibleModule( + argument_spec=module_args, supports_check_mode=False, add_file_common_args=True + ) + + if not python_requests_installed: + module.fail_json(msg="requests required for this module.") + + if not python_requests_kerberos_installed: + module.fail_json(msg="requests_kerberos required for this module.") + + url = module.params["url"] + verify_ssl = module.params["verify_ssl"] + + auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL) + try: + response = get(url=url, auth=auth, verify=verify_ssl, allow_redirects=True) + + result["status_code"] = response.status_code + result["headers"] = dict(response.headers) + result["content_type"] = response.headers.get("Content-Type", None) + + if response.status_code < 200 or response.status_code >= 300: + module.fail_json( + msg=f"Error fetching the information {response.status_code}: {response.text}" + ) + + result["response_b64"] = base64.b64encode(response.content) + if "application/json" in result["content_type"]: + try: + result["response_json"] = response.json() + except ValueError as e: + module.fail_json(msg=f"Error with the JSON response: {str(e)}") + + if "dest" in module.params: + dest = module.params["dest"] + if ( + os.path.exists(dest) + and os.path.isdir(dest) + and "content-disposition" in response.headers + ): + # Destination is a directory but the filename is available in Content-Disposition + filename = re.findall( + "filename=(.+)", response.headers["content-disposition"] + ) + dest = filename[0] if filename else None + elif os.path.exists(dest) and os.path.isdir(dest): + # Destination is a directory but we cannot guess the filename from Content-Disposition + dest = None + + if not dest: + # Reached if dest points to a directory and: + # - Content-Disposition not available + # - Cannot extract the filename part from the Content-Disposition header + module.fail_json( + msg="Destination points to a directory and the filename cannot be retrieved from the response" + ) + + exists = os.path.exists(dest) + original_sha1 = module.sha1(dest) if exists else None + with open(dest, mode="wb") as file: + file.write(response.content) + file_args = module.load_file_common_arguments(module.params, path=dest) + result["changed"] = ( + (not exists) + or (module.sha1(dest) != original_sha1) + or module.set_fs_attributes_if_different(file_args, result["changed"]) + ) + result["path"] = dest + + except Exception as e: + module.fail_json(msg=f"Error fetching the information: {str(e)}") + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/roles/repo_setup/README.md b/roles/repo_setup/README.md index f6dcc73d0f..46dc74eb86 100644 --- a/roles/repo_setup/README.md +++ b/roles/repo_setup/README.md @@ -30,6 +30,7 @@ using `cifmw_repo_setup_src` role default var. * `cifmw_repo_setup_rhos_release_rpm`: (String) URL to rhos-release RPM. * `cifmw_repo_setup_rhos_release_args`: (String) Parameters to pass down to `rhos-release`. * `cifmw_repo_setup_rhos_release_gpg_check`: (Bool) Skips the gpg check during rhos-release rpm installation. Defaults to `True`. +* `cifmw_repo_setup_rhos_release_path`: (String) The path where the rhos-release rpm is downloaded. Defaults to `{{ cifmw_repo_setup_basedir }}/rhos-release`. ## Notes diff --git a/roles/repo_setup/defaults/main.yml b/roles/repo_setup/defaults/main.yml index 177f5df200..4cddb08773 100644 --- a/roles/repo_setup/defaults/main.yml +++ b/roles/repo_setup/defaults/main.yml @@ -40,3 +40,4 @@ cifmw_repo_setup_component_promotion_tag: component-ci-testing # cifmw_repo_setup_rhos_release_args: cifmw_repo_setup_enable_rhos_release: false cifmw_repo_setup_rhos_release_gpg_check: true +cifmw_repo_setup_rhos_release_path: "{{ cifmw_repo_setup_basedir }}/rhos-release" diff --git a/roles/repo_setup/tasks/rhos_release.yml b/roles/repo_setup/tasks/rhos_release.yml index 81bb7638be..ece4586f1f 100644 --- a/roles/repo_setup/tasks/rhos_release.yml +++ b/roles/repo_setup/tasks/rhos_release.yml @@ -1,8 +1,29 @@ --- +- name: Download RHOS Release if the given rpm is a URL + when: cifmw_repo_setup_rhos_release_rpm is url + block: + - name: Create download directory + ansible.builtin.file: + path: "{{ cifmw_repo_setup_rhos_release_path }}" + state: directory + mode: "0755" + + - name: Download the RPM + cifmw.general.url_request: + url: "{{ cifmw_repo_setup_rhos_release_rpm }}" + dest: "{{ cifmw_repo_setup_rhos_release_path }}/rhos-release.rpm" + mode: "0644" + register: _cifmw_repo_setup_url_get + - name: Install RHOS Release tool become: true ansible.builtin.package: - name: "{{ cifmw_repo_setup_rhos_release_rpm }}" + name: >- + {{ + cifmw_repo_setup_rhos_release_rpm + if cifmw_repo_setup_rhos_release_rpm is not url + else _cifmw_repo_setup_url_get.path + }} state: present disable_gpg_check: "{{ cifmw_repo_setup_rhos_release_gpg_check | bool }}" diff --git a/tests/sanity/ignore.txt b/tests/sanity/ignore.txt index d746bf2521..a8db927abb 100644 --- a/tests/sanity/ignore.txt +++ b/tests/sanity/ignore.txt @@ -3,3 +3,4 @@ plugins/modules/generate_make_tasks.py validate-modules:missing-gplv3-license # plugins/modules/tempest_list_allowed.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/tempest_list_skipped.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/cephx_key.py validate-modules:missing-gplv3-license # ignore license check +plugins/modules/url_request.py validate-modules:missing-gplv3-license # ignore license check