diff options
Diffstat (limited to 'roles/openshift_health_checker')
11 files changed, 577 insertions, 59 deletions
diff --git a/roles/openshift_health_checker/action_plugins/openshift_health_check.py b/roles/openshift_health_checker/action_plugins/openshift_health_check.py index 03c40b78b..a62e4331e 100644 --- a/roles/openshift_health_checker/action_plugins/openshift_health_check.py +++ b/roles/openshift_health_checker/action_plugins/openshift_health_check.py @@ -25,9 +25,11 @@ class ActionModule(ActionBase): def run(self, tmp=None, task_vars=None): result = super(ActionModule, self).run(tmp, task_vars) + task_vars = task_vars or {} - if task_vars is None: - task_vars = {} + # vars are not supportably available in the callback plugin, + # so record any it will need in the result. + result['playbook_context'] = task_vars.get('r_openshift_health_checker_playbook_context') if "openshift" not in task_vars: result["failed"] = True @@ -46,19 +48,27 @@ class ActionModule(ActionBase): result["checks"] = check_results = {} + user_disabled_checks = [ + check.strip() + for check in task_vars.get("openshift_disable_check", "").split(",") + ] + for check_name in resolved_checks: display.banner("CHECK [{} : {}]".format(check_name, task_vars["ansible_host"])) check = known_checks[check_name] - if check.is_active(task_vars): + if not check.is_active(task_vars): + r = dict(skipped=True, skipped_reason="Not active for this host") + elif check_name in user_disabled_checks: + r = dict(skipped=True, skipped_reason="Disabled by user request") + else: try: r = check.run(tmp, task_vars) except OpenShiftCheckException as e: - r = {} - r["failed"] = True - r["msg"] = str(e) - else: - r = {"skipped": True} + r = dict( + failed=True, + msg=str(e), + ) check_results[check_name] = r diff --git a/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py b/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py index 7bce7f107..64c29a8d9 100644 --- a/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py +++ b/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py @@ -2,6 +2,12 @@ Ansible callback plugin. ''' +# Reason: In several locations below we disable pylint protected-access +# for Ansible objects that do not give us any public way +# to access the full details we need to report check failures. +# Status: disabled permanently or until Ansible object has a public API. +# This does leave the code more likely to be broken by future Ansible changes. + from pprint import pformat from ansible.plugins.callback import CallbackBase @@ -20,38 +26,37 @@ class CallbackModule(CallbackBase): CALLBACK_TYPE = 'aggregate' CALLBACK_NAME = 'failure_summary' CALLBACK_NEEDS_WHITELIST = False + _playbook_file = None def __init__(self): super(CallbackModule, self).__init__() self.__failures = [] + def v2_playbook_on_start(self, playbook): + super(CallbackModule, self).v2_playbook_on_start(playbook) + # re: playbook attrs see top comment # pylint: disable=protected-access + self._playbook_file = playbook._file_name + def v2_runner_on_failed(self, result, ignore_errors=False): super(CallbackModule, self).v2_runner_on_failed(result, ignore_errors) self.__failures.append(dict(result=result, ignore_errors=ignore_errors)) def v2_playbook_on_stats(self, stats): super(CallbackModule, self).v2_playbook_on_stats(stats) - # TODO: update condition to consider a host var or env var to - # enable/disable the summary, so that we can control the output from a - # play. if self.__failures: - self._print_failure_summary() + self._print_failure_details(self.__failures) - def _print_failure_summary(self): - '''Print a summary of failed tasks (including ignored failures).''' + def _print_failure_details(self, failures): + '''Print a summary of failed tasks or checks.''' self._display.display(u'\nFailure summary:\n') - # TODO: group failures by host or by task. If grouped by host, it is - # easy to see all problems of a given host. If grouped by task, it is - # easy to see what hosts needs the same fix. - - width = len(str(len(self.__failures))) + width = len(str(len(failures))) initial_indent_format = u' {{:>{width}}}. '.format(width=width) initial_indent_len = len(initial_indent_format.format(0)) subsequent_indent = u' ' * initial_indent_len subsequent_extra_indent = u' ' * (initial_indent_len + 10) - for i, failure in enumerate(self.__failures, 1): + for i, failure in enumerate(failures, 1): entries = _format_failure(failure) self._display.display(u'\n{}{}'.format(initial_indent_format.format(i), entries[0])) for entry in entries[1:]: @@ -59,11 +64,52 @@ class CallbackModule(CallbackBase): indented = u'{}{}'.format(subsequent_indent, entry) self._display.display(indented) - -# Reason: disable pylint protected-access because we need to access _* -# attributes of a task result to implement this method. -# Status: permanently disabled unless Ansible's API changes. -# pylint: disable=protected-access + failed_checks = set() + playbook_context = None + # re: result attrs see top comment # pylint: disable=protected-access + for failure in failures: + # get context from check task result since callback plugins cannot access task vars + playbook_context = playbook_context or failure['result']._result.get('playbook_context') + failed_checks.update( + name + for name, result in failure['result']._result.get('checks', {}).items() + if result.get('failed') + ) + if failed_checks: + self._print_check_failure_summary(failed_checks, playbook_context) + + def _print_check_failure_summary(self, failed_checks, context): + checks = ','.join(sorted(failed_checks)) + # NOTE: context is not set if all failures occurred prior to checks task + summary = ( + '\n' + 'The execution of "{playbook}"\n' + 'includes checks designed to fail early if the requirements\n' + 'of the playbook are not met. One or more of these checks\n' + 'failed. To disregard these results, you may choose to\n' + 'disable failing checks by setting an Ansible variable:\n\n' + ' openshift_disable_check={checks}\n\n' + 'Failing check names are shown in the failure details above.\n' + 'Some checks may be configurable by variables if your requirements\n' + 'are different from the defaults; consult check documentation.\n' + 'Variables can be set in the inventory or passed on the\n' + 'command line using the -e flag to ansible-playbook.\n' + ).format(playbook=self._playbook_file, checks=checks) + if context in ['pre-install', 'health']: + summary = ( + '\n' + 'You may choose to configure or disable failing checks by\n' + 'setting Ansible variables. To disable those above:\n\n' + ' openshift_disable_check={checks}\n\n' + 'Consult check documentation for configurable variables.\n' + 'Variables can be set in the inventory or passed on the\n' + 'command line using the -e flag to ansible-playbook.\n' + ).format(checks=checks) + # other expected contexts: install, upgrade + self._display.display(summary) + + +# re: result attrs see top comment # pylint: disable=protected-access def _format_failure(failure): '''Return a list of pretty-formatted text entries describing a failure, including relevant information about it. Expect that the list of text entries will be joined @@ -100,11 +146,8 @@ def _format_failed_checks(checks): return stringc(pformat(checks), C.COLOR_ERROR) -# Reason: disable pylint protected-access because we need to access _* -# attributes of obj to implement this function. -# This is inspired by ansible.playbook.base.Base.dump_me. -# Status: permanently disabled unless Ansible's API changes. -# pylint: disable=protected-access +# This is inspired by ansible.playbook.base.Base.dump_me. +# re: play/task/block attrs see top comment # pylint: disable=protected-access def _get_play(obj): '''Given a task or block, recursively tries to find its parent play.''' if hasattr(obj, '_play'): diff --git a/roles/openshift_health_checker/library/rpm_version.py b/roles/openshift_health_checker/library/rpm_version.py new file mode 100644 index 000000000..8ea223055 --- /dev/null +++ b/roles/openshift_health_checker/library/rpm_version.py @@ -0,0 +1,127 @@ +#!/usr/bin/python +""" +Ansible module for rpm-based systems determining existing package version information in a host. +""" + +from ansible.module_utils.basic import AnsibleModule + +IMPORT_EXCEPTION = None +try: + import rpm # pylint: disable=import-error +except ImportError as err: + IMPORT_EXCEPTION = err # in tox test env, rpm import fails + + +class RpmVersionException(Exception): + """Base exception class for package version problems""" + def __init__(self, message, problem_pkgs=None): + Exception.__init__(self, message) + self.problem_pkgs = problem_pkgs + + +def main(): + """Entrypoint for this Ansible module""" + module = AnsibleModule( + argument_spec=dict( + package_list=dict(type="list", required=True), + ), + supports_check_mode=True + ) + + if IMPORT_EXCEPTION: + module.fail_json(msg="rpm_version module could not import rpm: %s" % IMPORT_EXCEPTION) + + # determine the packages we will look for + pkg_list = module.params['package_list'] + if not pkg_list: + module.fail_json(msg="package_list must not be empty") + + # get list of packages available and complain if any + # of them are missing or if any errors occur + try: + pkg_versions = _retrieve_expected_pkg_versions(_to_dict(pkg_list)) + _check_pkg_versions(pkg_versions, _to_dict(pkg_list)) + except RpmVersionException as excinfo: + module.fail_json(msg=str(excinfo)) + module.exit_json(changed=False) + + +def _to_dict(pkg_list): + return {pkg["name"]: pkg for pkg in pkg_list} + + +def _retrieve_expected_pkg_versions(expected_pkgs_dict): + """Search for installed packages matching given pkg names + and versions. Returns a dictionary: {pkg_name: [versions]}""" + + transaction = rpm.TransactionSet() + pkgs = {} + + for pkg_name in expected_pkgs_dict: + matched_pkgs = transaction.dbMatch("name", pkg_name) + if not matched_pkgs: + continue + + for header in matched_pkgs: + if header['name'] == pkg_name: + if pkg_name not in pkgs: + pkgs[pkg_name] = [] + + pkgs[pkg_name].append(header['version']) + + return pkgs + + +def _check_pkg_versions(found_pkgs_dict, expected_pkgs_dict): + invalid_pkg_versions = {} + not_found_pkgs = [] + + for pkg_name, pkg in expected_pkgs_dict.items(): + if not found_pkgs_dict.get(pkg_name): + not_found_pkgs.append(pkg_name) + continue + + found_versions = [_parse_version(version) for version in found_pkgs_dict[pkg_name]] + expected_version = _parse_version(pkg["version"]) + if expected_version not in found_versions: + invalid_pkg_versions[pkg_name] = { + "found_versions": found_versions, + "required_version": expected_version, + } + + if not_found_pkgs: + raise RpmVersionException( + '\n'.join([ + "The following packages were not found to be installed: {}".format('\n '.join([ + "{}".format(pkg) + for pkg in not_found_pkgs + ])) + ]), + not_found_pkgs, + ) + + if invalid_pkg_versions: + raise RpmVersionException( + '\n '.join([ + "The following packages were found to be installed with an incorrect version: {}".format('\n'.join([ + " \n{}\n Required version: {}\n Found versions: {}".format( + pkg_name, + pkg["required_version"], + ', '.join([version for version in pkg["found_versions"]])) + for pkg_name, pkg in invalid_pkg_versions.items() + ])) + ]), + invalid_pkg_versions, + ) + + +def _parse_version(version_str): + segs = version_str.split('.') + if not segs or len(segs) <= 2: + return version_str + + return '.'.join(segs[0:2]) + + +if __name__ == '__main__': + main() diff --git a/roles/openshift_health_checker/openshift_checks/disk_availability.py b/roles/openshift_health_checker/openshift_checks/disk_availability.py index c2792a0fe..962148cb8 100644 --- a/roles/openshift_health_checker/openshift_checks/disk_availability.py +++ b/roles/openshift_health_checker/openshift_checks/disk_availability.py @@ -27,10 +27,12 @@ class DiskAvailability(NotContainerizedMixin, OpenShiftCheck): def run(self, tmp, task_vars): group_names = get_var(task_vars, "group_names") ansible_mounts = get_var(task_vars, "ansible_mounts") - - min_free_bytes = max(self.recommended_disk_space_bytes.get(name, 0) for name in group_names) free_bytes = self.openshift_available_disk(ansible_mounts) + recommended_min = max(self.recommended_disk_space_bytes.get(name, 0) for name in group_names) + configured_min = int(get_var(task_vars, "openshift_check_min_host_disk_gb", default=0)) * 10**9 + min_free_bytes = configured_min or recommended_min + if free_bytes < min_free_bytes: return { 'failed': True, diff --git a/roles/openshift_health_checker/openshift_checks/memory_availability.py b/roles/openshift_health_checker/openshift_checks/memory_availability.py index 28805dc37..f4e31065f 100644 --- a/roles/openshift_health_checker/openshift_checks/memory_availability.py +++ b/roles/openshift_health_checker/openshift_checks/memory_availability.py @@ -1,6 +1,9 @@ # pylint: disable=missing-docstring from openshift_checks import OpenShiftCheck, get_var +MIB = 2**20 +GIB = 2**30 + class MemoryAvailability(OpenShiftCheck): """Check that recommended memory is available.""" @@ -11,10 +14,12 @@ class MemoryAvailability(OpenShiftCheck): # Values taken from the official installation documentation: # https://docs.openshift.org/latest/install_config/install/prerequisites.html#system-requirements recommended_memory_bytes = { - "masters": 16 * 10**9, - "nodes": 8 * 10**9, - "etcd": 20 * 10**9, + "masters": 16 * GIB, + "nodes": 8 * GIB, + "etcd": 8 * GIB, } + # https://access.redhat.com/solutions/3006511 physical RAM is partly reserved from memtotal + memtotal_adjustment = 1 * GIB @classmethod def is_active(cls, task_vars): @@ -25,19 +30,21 @@ class MemoryAvailability(OpenShiftCheck): def run(self, tmp, task_vars): group_names = get_var(task_vars, "group_names") - total_memory_bytes = get_var(task_vars, "ansible_memtotal_mb") * 10**6 + total_memory_bytes = get_var(task_vars, "ansible_memtotal_mb") * MIB - min_memory_bytes = max(self.recommended_memory_bytes.get(name, 0) for name in group_names) + recommended_min = max(self.recommended_memory_bytes.get(name, 0) for name in group_names) + configured_min = float(get_var(task_vars, "openshift_check_min_host_memory_gb", default=0)) * GIB + min_memory_bytes = configured_min or recommended_min - if total_memory_bytes < min_memory_bytes: + if total_memory_bytes + self.memtotal_adjustment < min_memory_bytes: return { 'failed': True, 'msg': ( - 'Available memory ({available:.1f} GB) ' - 'below recommended value ({recommended:.1f} GB)' + 'Available memory ({available:.1f} GiB) is too far ' + 'below recommended value ({recommended:.1f} GiB)' ).format( - available=float(total_memory_bytes) / 10**9, - recommended=float(min_memory_bytes) / 10**9, + available=float(total_memory_bytes) / GIB, + recommended=float(min_memory_bytes) / GIB, ), } diff --git a/roles/openshift_health_checker/openshift_checks/ovs_version.py b/roles/openshift_health_checker/openshift_checks/ovs_version.py new file mode 100644 index 000000000..1e45ae3af --- /dev/null +++ b/roles/openshift_health_checker/openshift_checks/ovs_version.py @@ -0,0 +1,78 @@ +""" +Ansible module for determining if an installed version of Open vSwitch is incompatible with the +currently installed version of OpenShift. +""" + +from openshift_checks import OpenShiftCheck, OpenShiftCheckException, get_var +from openshift_checks.mixins import NotContainerizedMixin + + +class OvsVersion(NotContainerizedMixin, OpenShiftCheck): + """Check that packages in a package_list are installed on the host + and are the correct version as determined by an OpenShift installation. + """ + + name = "ovs_version" + tags = ["health"] + + openshift_to_ovs_version = { + "3.6": "2.6", + "3.5": "2.6", + "3.4": "2.4", + } + + # map major release versions across releases + # to a common major version + openshift_major_release_version = { + "1": "3", + } + + @classmethod + def is_active(cls, task_vars): + """Skip hosts that do not have package requirements.""" + group_names = get_var(task_vars, "group_names", default=[]) + master_or_node = 'masters' in group_names or 'nodes' in group_names + return super(OvsVersion, cls).is_active(task_vars) and master_or_node + + def run(self, tmp, task_vars): + args = { + "package_list": [ + { + "name": "openvswitch", + "version": self.get_required_ovs_version(task_vars), + }, + ], + } + return self.execute_module("rpm_version", args, task_vars) + + def get_required_ovs_version(self, task_vars): + """Return the correct Open vSwitch version for the current OpenShift version""" + openshift_version = self._get_openshift_version(task_vars) + + if float(openshift_version) < 3.5: + return self.openshift_to_ovs_version["3.4"] + + ovs_version = self.openshift_to_ovs_version.get(str(openshift_version)) + if ovs_version: + return self.openshift_to_ovs_version[str(openshift_version)] + + msg = "There is no recommended version of Open vSwitch for the current version of OpenShift: {}" + raise OpenShiftCheckException(msg.format(openshift_version)) + + def _get_openshift_version(self, task_vars): + openshift_version = get_var(task_vars, "openshift_image_tag") + if openshift_version and openshift_version[0] == 'v': + openshift_version = openshift_version[1:] + + return self._parse_version(openshift_version) + + def _parse_version(self, version): + components = version.split(".") + if not components or len(components) < 2: + msg = "An invalid version of OpenShift was found for this host: {}" + raise OpenShiftCheckException(msg.format(version)) + + if components[0] in self.openshift_major_release_version: + components[0] = self.openshift_major_release_version[components[0]] + + return '.'.join(components[:2]) diff --git a/roles/openshift_health_checker/test/action_plugin_test.py b/roles/openshift_health_checker/test/action_plugin_test.py index 2693ae37b..6ebf0ebb2 100644 --- a/roles/openshift_health_checker/test/action_plugin_test.py +++ b/roles/openshift_health_checker/test/action_plugin_test.py @@ -67,6 +67,7 @@ def changed(result): return result.get('changed', False) +# tests whether task is skipped, not individual checks def skipped(result): return result.get('skipped', False) @@ -101,7 +102,20 @@ def test_action_plugin_skip_non_active_checks(plugin, task_vars, monkeypatch): result = plugin.run(tmp=None, task_vars=task_vars) - assert result['checks']['fake_check'] == {'skipped': True} + assert result['checks']['fake_check'] == dict(skipped=True, skipped_reason="Not active for this host") + assert not failed(result) + assert not changed(result) + assert not skipped(result) + + +def test_action_plugin_skip_disabled_checks(plugin, task_vars, monkeypatch): + checks = [fake_check('fake_check', is_active=True)] + monkeypatch.setattr('openshift_checks.OpenShiftCheck.subclasses', classmethod(lambda cls: checks)) + + task_vars['openshift_disable_check'] = 'fake_check' + result = plugin.run(tmp=None, task_vars=task_vars) + + assert result['checks']['fake_check'] == dict(skipped=True, skipped_reason="Disabled by user request") assert not failed(result) assert not changed(result) assert not skipped(result) diff --git a/roles/openshift_health_checker/test/disk_availability_test.py b/roles/openshift_health_checker/test/disk_availability_test.py index 970b474d7..b353fa610 100644 --- a/roles/openshift_health_checker/test/disk_availability_test.py +++ b/roles/openshift_health_checker/test/disk_availability_test.py @@ -42,9 +42,10 @@ def test_cannot_determine_available_disk(ansible_mounts, extra_words): assert word in str(excinfo.value) -@pytest.mark.parametrize('group_names,ansible_mounts', [ +@pytest.mark.parametrize('group_names,configured_min,ansible_mounts', [ ( ['masters'], + 0, [{ 'mount': '/', 'size_available': 40 * 10**9 + 1, @@ -52,6 +53,7 @@ def test_cannot_determine_available_disk(ansible_mounts, extra_words): ), ( ['nodes'], + 0, [{ 'mount': '/', 'size_available': 15 * 10**9 + 1, @@ -59,6 +61,7 @@ def test_cannot_determine_available_disk(ansible_mounts, extra_words): ), ( ['etcd'], + 0, [{ 'mount': '/', 'size_available': 20 * 10**9 + 1, @@ -66,6 +69,15 @@ def test_cannot_determine_available_disk(ansible_mounts, extra_words): ), ( ['etcd'], + 1, # configure lower threshold + [{ + 'mount': '/', + 'size_available': 1 * 10**9 + 1, # way smaller than recommended + }], + ), + ( + ['etcd'], + 0, [{ # not enough space on / ... 'mount': '/', @@ -77,9 +89,10 @@ def test_cannot_determine_available_disk(ansible_mounts, extra_words): }], ), ]) -def test_succeeds_with_recommended_disk_space(group_names, ansible_mounts): +def test_succeeds_with_recommended_disk_space(group_names, configured_min, ansible_mounts): task_vars = dict( group_names=group_names, + openshift_check_min_host_disk_gb=configured_min, ansible_mounts=ansible_mounts, ) @@ -89,9 +102,10 @@ def test_succeeds_with_recommended_disk_space(group_names, ansible_mounts): assert not result.get('failed', False) -@pytest.mark.parametrize('group_names,ansible_mounts,extra_words', [ +@pytest.mark.parametrize('group_names,configured_min,ansible_mounts,extra_words', [ ( ['masters'], + 0, [{ 'mount': '/', 'size_available': 1, @@ -99,7 +113,17 @@ def test_succeeds_with_recommended_disk_space(group_names, ansible_mounts): ['0.0 GB'], ), ( + ['masters'], + 100, # set a higher threshold + [{ + 'mount': '/', + 'size_available': 50 * 10**9, # would normally be enough... + }], + ['100.0 GB'], + ), + ( ['nodes'], + 0, [{ 'mount': '/', 'size_available': 1 * 10**9, @@ -108,6 +132,7 @@ def test_succeeds_with_recommended_disk_space(group_names, ansible_mounts): ), ( ['etcd'], + 0, [{ 'mount': '/', 'size_available': 1, @@ -116,6 +141,7 @@ def test_succeeds_with_recommended_disk_space(group_names, ansible_mounts): ), ( ['nodes', 'masters'], + 0, [{ 'mount': '/', # enough space for a node, not enough for a master @@ -125,6 +151,7 @@ def test_succeeds_with_recommended_disk_space(group_names, ansible_mounts): ), ( ['etcd'], + 0, [{ # enough space on / ... 'mount': '/', @@ -137,9 +164,10 @@ def test_succeeds_with_recommended_disk_space(group_names, ansible_mounts): ['0.0 GB'], ), ]) -def test_fails_with_insufficient_disk_space(group_names, ansible_mounts, extra_words): +def test_fails_with_insufficient_disk_space(group_names, configured_min, ansible_mounts, extra_words): task_vars = dict( group_names=group_names, + openshift_check_min_host_disk_gb=configured_min, ansible_mounts=ansible_mounts, ) diff --git a/roles/openshift_health_checker/test/memory_availability_test.py b/roles/openshift_health_checker/test/memory_availability_test.py index e161a5b9e..4fbaea0a9 100644 --- a/roles/openshift_health_checker/test/memory_availability_test.py +++ b/roles/openshift_health_checker/test/memory_availability_test.py @@ -20,27 +20,42 @@ def test_is_active(group_names, is_active): assert MemoryAvailability.is_active(task_vars=task_vars) == is_active -@pytest.mark.parametrize('group_names,ansible_memtotal_mb', [ +@pytest.mark.parametrize('group_names,configured_min,ansible_memtotal_mb', [ ( ['masters'], + 0, 17200, ), ( ['nodes'], + 0, 8200, ), ( + ['nodes'], + 1, # configure lower threshold + 2000, # too low for recommended but not for configured + ), + ( + ['nodes'], + 2, # configure threshold where adjustment pushes it over + 1900, + ), + ( ['etcd'], - 22200, + 0, + 8200, ), ( ['masters', 'nodes'], + 0, 17000, ), ]) -def test_succeeds_with_recommended_memory(group_names, ansible_memtotal_mb): +def test_succeeds_with_recommended_memory(group_names, configured_min, ansible_memtotal_mb): task_vars = dict( group_names=group_names, + openshift_check_min_host_memory_gb=configured_min, ansible_memtotal_mb=ansible_memtotal_mb, ) @@ -50,39 +65,62 @@ def test_succeeds_with_recommended_memory(group_names, ansible_memtotal_mb): assert not result.get('failed', False) -@pytest.mark.parametrize('group_names,ansible_memtotal_mb,extra_words', [ +@pytest.mark.parametrize('group_names,configured_min,ansible_memtotal_mb,extra_words', [ ( ['masters'], 0, - ['0.0 GB'], + 0, + ['0.0 GiB'], ), ( ['nodes'], + 0, 100, - ['0.1 GB'], + ['0.1 GiB'], + ), + ( + ['nodes'], + 24, # configure higher threshold + 20 * 1024, # enough to meet recommended but not configured + ['20.0 GiB'], + ), + ( + ['nodes'], + 24, # configure higher threshold + 22 * 1024, # not enough for adjustment to push over threshold + ['22.0 GiB'], ), ( ['etcd'], - -1, - ['0.0 GB'], + 0, + 6 * 1024, + ['6.0 GiB'], + ), + ( + ['etcd', 'masters'], + 0, + 9 * 1024, # enough memory for etcd, not enough for a master + ['9.0 GiB'], ), ( ['nodes', 'masters'], + 0, # enough memory for a node, not enough for a master - 11000, - ['11.0 GB'], + 11 * 1024, + ['11.0 GiB'], ), ]) -def test_fails_with_insufficient_memory(group_names, ansible_memtotal_mb, extra_words): +def test_fails_with_insufficient_memory(group_names, configured_min, ansible_memtotal_mb, extra_words): task_vars = dict( group_names=group_names, + openshift_check_min_host_memory_gb=configured_min, ansible_memtotal_mb=ansible_memtotal_mb, ) check = MemoryAvailability(execute_module=fake_execute_module) result = check.run(tmp=None, task_vars=task_vars) - assert result['failed'] + assert result.get('failed', False) for word in 'below recommended'.split() + extra_words: assert word in result['msg'] diff --git a/roles/openshift_health_checker/test/ovs_version_test.py b/roles/openshift_health_checker/test/ovs_version_test.py new file mode 100644 index 000000000..6494e1c06 --- /dev/null +++ b/roles/openshift_health_checker/test/ovs_version_test.py @@ -0,0 +1,89 @@ +import pytest + +from openshift_checks.ovs_version import OvsVersion, OpenShiftCheckException + + +def test_openshift_version_not_supported(): + def execute_module(module_name=None, module_args=None, tmp=None, task_vars=None): + return {} + + openshift_release = '111.7.0' + + task_vars = dict( + openshift=dict(common=dict(service_type='origin')), + openshift_release=openshift_release, + openshift_image_tag='v' + openshift_release, + openshift_deployment_type='origin', + ) + + check = OvsVersion(execute_module=execute_module) + with pytest.raises(OpenShiftCheckException) as excinfo: + check.run(tmp=None, task_vars=task_vars) + + assert "no recommended version of Open vSwitch" in str(excinfo.value) + + +def test_invalid_openshift_release_format(): + def execute_module(module_name=None, module_args=None, tmp=None, task_vars=None): + return {} + + task_vars = dict( + openshift=dict(common=dict(service_type='origin')), + openshift_image_tag='v0', + openshift_deployment_type='origin', + ) + + check = OvsVersion(execute_module=execute_module) + with pytest.raises(OpenShiftCheckException) as excinfo: + check.run(tmp=None, task_vars=task_vars) + assert "invalid version" in str(excinfo.value) + + +@pytest.mark.parametrize('openshift_release,expected_ovs_version', [ + ("3.5", "2.6"), + ("3.6", "2.6"), + ("3.4", "2.4"), + ("3.3", "2.4"), + ("1.0", "2.4"), +]) +def test_ovs_package_version(openshift_release, expected_ovs_version): + task_vars = dict( + openshift=dict(common=dict(service_type='origin')), + openshift_release=openshift_release, + openshift_image_tag='v' + openshift_release, + ) + return_value = object() + + def execute_module(module_name=None, module_args=None, tmp=None, task_vars=None): + assert module_name == 'rpm_version' + assert "package_list" in module_args + + for pkg in module_args["package_list"]: + if pkg["name"] == "openvswitch": + assert pkg["version"] == expected_ovs_version + + return return_value + + check = OvsVersion(execute_module=execute_module) + result = check.run(tmp=None, task_vars=task_vars) + assert result is return_value + + +@pytest.mark.parametrize('group_names,is_containerized,is_active', [ + (['masters'], False, True), + # ensure check is skipped on containerized installs + (['masters'], True, False), + (['nodes'], False, True), + (['masters', 'nodes'], False, True), + (['masters', 'etcd'], False, True), + ([], False, False), + (['etcd'], False, False), + (['lb'], False, False), + (['nfs'], False, False), +]) +def test_ovs_version_skip_when_not_master_nor_node(group_names, is_containerized, is_active): + task_vars = dict( + group_names=group_names, + openshift=dict(common=dict(is_containerized=is_containerized)), + ) + assert OvsVersion.is_active(task_vars=task_vars) == is_active diff --git a/roles/openshift_health_checker/test/rpm_version_test.py b/roles/openshift_health_checker/test/rpm_version_test.py new file mode 100644 index 000000000..2f09ef965 --- /dev/null +++ b/roles/openshift_health_checker/test/rpm_version_test.py @@ -0,0 +1,82 @@ +import pytest +import rpm_version + +expected_pkgs = { + "spam": { + "name": "spam", + "version": "3.2.1", + }, + "eggs": { + "name": "eggs", + "version": "3.2.1", + }, +} + + +@pytest.mark.parametrize('pkgs, expect_not_found', [ + ( + {}, + ["spam", "eggs"], # none found + ), + ( + {"spam": ["3.2.1", "4.5.1"]}, + ["eggs"], # completely missing + ), + ( + { + "spam": ["3.2.1", "4.5.1"], + "eggs": ["3.2.1"], + }, + [], # all found + ), +]) +def test_check_pkg_found(pkgs, expect_not_found): + if expect_not_found: + with pytest.raises(rpm_version.RpmVersionException) as e: + rpm_version._check_pkg_versions(pkgs, expected_pkgs) + + assert "not found to be installed" in str(e.value) + assert set(expect_not_found) == set(e.value.problem_pkgs) + else: + rpm_version._check_pkg_versions(pkgs, expected_pkgs) + + +@pytest.mark.parametrize('pkgs, expect_not_found', [ + ( + { + 'spam': ['3.2.1'], + 'eggs': ['3.3.2'], + }, + { + "eggs": { + "required_version": "3.2", + "found_versions": ["3.3"], + } + }, # not the right version + ), + ( + { + 'spam': ['3.1.2', "3.3.2"], + 'eggs': ['3.3.2', "1.2.3"], + }, + { + "eggs": { + "required_version": "3.2", + "found_versions": ["3.3", "1.2"], + }, + "spam": { + "required_version": "3.2", + "found_versions": ["3.1", "3.3"], + } + }, # not the right version + ), +]) +def test_check_pkg_version_found(pkgs, expect_not_found): + if expect_not_found: + with pytest.raises(rpm_version.RpmVersionException) as e: + rpm_version._check_pkg_versions(pkgs, expected_pkgs) + + assert "found to be installed with an incorrect version" in str(e.value) + assert expect_not_found == e.value.problem_pkgs + else: + rpm_version._check_pkg_versions(pkgs, expected_pkgs) |