From 4562d61e9b769081a84b3ba0c98e35ceed62a614 Mon Sep 17 00:00:00 2001 From: Matej Dujava Date: Mon, 7 Oct 2024 11:19:50 +0200 Subject: [PATCH] Fix FIPS enabled cluster operations Make sure host system is FIPS enabled and proper installer is downloaded --- osia/cli.py | 40 +++++++++++++++- osia/installer/downloader/install.py | 69 +++++++++++++++++++--------- poetry.lock | 13 +++++- pyproject.toml | 1 + 4 files changed, 99 insertions(+), 24 deletions(-) diff --git a/osia/cli.py b/osia/cli.py index 8729c62..09c123f 100644 --- a/osia/cli.py +++ b/osia/cli.py @@ -17,8 +17,11 @@ openshift""" import argparse import logging -from typing import List +from typing import List, Tuple, Optional +from subprocess import Popen +from semantic_version import Version, SimpleSpec import coloredlogs +import distro from .config.config import ARCH_AMD, ARCH_ARM, ARCH_X86_64, ARCH_AARCH64, ARCH_S390X, ARCH_PPC from .installer import install_cluster, delete_cluster, storage, download_installer @@ -84,16 +87,46 @@ def _read_list(in_str: str) -> List[str]: a['proc'] = _identity +def _check_fips_compatible(rhel_version: bool) -> Tuple[bool, Optional[str]]: + if not rhel_version: + return False, "FIPS installs are supported only from RHEL systems" + try: + with Popen(["fips-mode-setup", "--check"]) as proc: + proc.wait() + if proc.returncode != 0: + return False, "FIPS is not enabled on the system" + except FileNotFoundError: + return False, "fips-mode-setup must be installed on the system" + + return True, None + + def _resolve_installer(from_args): if from_args.installer is None and from_args.installer_version is None: raise Exception('Either installer or installer-version must be passed') if from_args.installer: return from_args.installer + rhel_version = None + if distro.id() == "rhel": + rhel_version = distro.major_version() + + if from_args.enable_fips: + supported, msg = _check_fips_compatible(rhel_version) + if not supported: + raise Exception(msg) + + if Version.coerce(from_args.installer_version.split("-")[-1]) in SimpleSpec("<4.16"): + # ocp <4.16 does not have dedicated openshift-install-fips, it is + # fine to run normal installer on FIPS enabled RHEL + from_args.enable_fips = False + return download_installer(from_args.installer_version, from_args.installer_arch, from_args.installers_dir, - from_args.installer_source) + from_args.installer_source, + rhel_version=rhel_version, + fips=from_args.enable_fips) def _merge_dictionaries(from_args): @@ -122,6 +155,9 @@ def _exec_install_cluster(args): def _exec_delete_cluster(args): + # cleanup of fips cluster can be done from anywhere + args.enable_fips = None + conf = _merge_dictionaries(args) if not args.skip_git: diff --git a/osia/installer/downloader/install.py b/osia/installer/downloader/install.py index bb12735..65e88f6 100644 --- a/osia/installer/downloader/install.py +++ b/osia/installer/downloader/install.py @@ -35,12 +35,12 @@ BUILD_ROOT = "https://openshift-release-artifacts.svc.ci.openshift.org/" PREVIEW_ROOT = "http://mirror.openshift.com/pub/openshift-v4/{}/clients/ocp-dev-preview/" -VERSION_RE = re.compile(r"^openshift-install-(?P\w+)" - r"(-(?P\w+))?-(?P\d+.*)\.tar\.gz") +VERSION_RE = re.compile(r"^openshift-install(-rhel(?P\d+))?(-(?P(linux|mac)))?" + r"(-(?P\w+))?(-(?P\d+.*))?\.tar\.gz") EXTRACTION_RE = re.compile(r'.*Extracting tools for .*, may take up to a minute.*') -def _current_platform(): +def _current_platform() -> Tuple[str, str]: if platform.system() == "Linux" and platform.machine() == "x86_64": return "linux", "amd64" if platform.system() == "Linux" and ( @@ -54,10 +54,14 @@ def _current_platform(): raise Exception(f"Unrecognized platform {platform.system()} {platform.machine()}") -def get_url(directory: str, arch: str) -> Tuple[Optional[str], Optional[str]]: +def get_url(directory: str, arch: str, fips: bool = False, + rhel_version: str = None) -> Tuple[Optional[str], Optional[str]]: """Searches the http directory and returns both url to installer and version. """ + if fips and not rhel_version: + raise Exception("Rhel version was not detected. Please download installer separatly.") + logging.debug('Url for installers look-up %s', directory) lst = requests.get(directory, allow_redirects=True) tree = BeautifulSoup(lst.content, 'html.parser') @@ -67,16 +71,29 @@ def get_url(directory: str, arch: str) -> Tuple[Optional[str], Optional[str]]: for k in links: logging.debug('Parsing link: %s', k.get('href')) match = VERSION_RE.match(k.get('href')) - if match and match.group('platform') == os_name: - if (local_arch == match.group('architecture')) \ - or (local_arch == arch and not match.group('architecture')): - installer = lst.url + k.get('href') + + if match: + if match.group("version"): version = match.group('version') + + if fips and match.group("rhel") == rhel_version: + installer = lst.url + k.get('href') break + + if not fips and match.group('platform') == os_name: + if (local_arch == match.group('architecture')) \ + or (local_arch == arch and not match.group('architecture')): + installer = lst.url + k.get('href') + break + else: + if fips: + raise Exception(f"FIPS Installer not found for {rhel_version=}") + raise Exception("Installer not found") return installer, version -def get_devel_url(version: str, arch: str) -> Tuple[Optional[str], Optional[str]]: +def get_devel_url(version: str, arch: str, fips: bool = False, + rhel_version: str = None) -> Tuple[Optional[str], Optional[str]]: """ Searches developement sources and returns url to installer """ @@ -89,17 +106,19 @@ def get_devel_url(version: str, arch: str) -> Tuple[Optional[str], Optional[str] req = requests.get(BUILD_ROOT + version, allow_redirects=True) ast = BeautifulSoup(req.content, 'html.parser') logging.debug('Installer found on page, continuing') - return get_url(req.url, arch) + return get_url(req.url, arch, fips, rhel_version) -def get_prev_url(version: str, arch: str) -> Tuple[Optional[str], Optional[str]]: +def get_prev_url(version: str, arch: str, fips: bool = False, + rhel_version: str = None) -> Tuple[Optional[str], Optional[str]]: """Returns installer url from dev-preview sources""" - return get_url(PREVIEW_ROOT.format(arch) + version + "/", arch) + return get_url(PREVIEW_ROOT.format(arch) + version + "/", arch, fips, rhel_version) -def get_prod_url(version: str, arch: str) -> Tuple[Optional[str], Optional[str]]: +def get_prod_url(version: str, arch: str, fips: bool = False, + rhel_version: str = None) -> Tuple[Optional[str], Optional[str]]: """Returns installer url from production sources""" - return get_url(PROD_ROOT.format(arch) + version + "/", arch) + return get_url(PROD_ROOT.format(arch) + version + "/", arch, fips, rhel_version) def _get_storage_path(version: str, install_base: str) -> str: @@ -115,12 +134,12 @@ def _extract_tar(buffer: NamedTemporaryFile, target: str) -> Path: with tarfile.open(buffer.name) as tar: inst_info = None for i in tar.getmembers(): - if i.name == 'openshift-install': + if i.name in ['openshift-install', 'openshift-install-fips']: inst_info = i if inst_info is None: raise Exception("error") stream = tar.extractfile(inst_info) - result = Path(target).joinpath('openshift-install') + result = Path(target).joinpath(inst_info.name) with result.open('wb') as output: copyfileobj(stream, output) result.chmod(result.stat().st_mode | stat.S_IXUSR) @@ -132,10 +151,13 @@ def get_installer(tar_url: str, target: str): return get_data(tar_url, target, _extract_tar) +# pylint: disable=too-many-arguments def download_installer(installer_version: str, installer_arch: str, dest_directory: str, - source: str) -> str: + source: str, + fips: bool = False, + rhel_version: str = None) -> str: """Starts search and extraction of installer""" logging.debug("Getting version %s of %s, storing to directory %s and devel is %r", installer_version, installer_arch, dest_directory, source) @@ -150,12 +172,17 @@ def download_installer(installer_version: str, else: raise Exception("Error for source profile " + source) - url, version = downloader(installer_version, installer_arch) + url, version = downloader(installer_version, installer_arch, fips, rhel_version) logging.debug('Installer\'s URL is %s and full version is %s', url, version) root = Path(dest_directory).joinpath(version) - if root.exists() and root.joinpath('openshift-install').exists(): + installer_exe_name = 'openshift-install' + + if fips: + installer_exe_name = 'openshift-install-fips' + + if root.exists() and root.joinpath(installer_exe_name).exists(): logging.info('Found installer at %s', root.as_posix()) - return root.joinpath('openshift-install').as_posix() - root.mkdir(parents=True) + return root.joinpath(installer_exe_name).as_posix() + root.mkdir(parents=True, exist_ok=True) return get_installer(url, root.as_posix()) diff --git a/poetry.lock b/poetry.lock index dfde9ea..fdd8828 100644 --- a/poetry.lock +++ b/poetry.lock @@ -865,6 +865,17 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] +[[package]] +name = "distro" +version = "1.9.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, +] + [[package]] name = "docutils" version = "0.21.2" @@ -2296,4 +2307,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "7ce5631296b9ca071ac926d2eda31073893421877ae09f5bc648e1cc09f48974" +content-hash = "66f31363db978f5490b1a6e71245c0863451d59c5fa886a4a3232891ed77819c" diff --git a/pyproject.toml b/pyproject.toml index 17455ba..ad89728 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ beautifulsoup4 = "*" boto3 = "*" coloredlogs = "*" dynaconf = {extras = ["yaml"], version = "*"} +distro = "*" gitpython = "*" jinja2 = "*" openstacksdk = "*"