Browse Source

Merge pull request #3518 from juanvallejo/jvallejo/ram_disk_space_checker

add ram and non-containerized storage preflight check
Rodolfo Carvalho 8 years ago
parent
commit
3820dab165

+ 65 - 0
roles/openshift_health_checker/openshift_checks/disk_availability.py

@@ -0,0 +1,65 @@
+# pylint: disable=missing-docstring
+from openshift_checks import OpenShiftCheck, OpenShiftCheckException, get_var
+from openshift_checks.mixins import NotContainerizedMixin
+
+
+class DiskAvailability(NotContainerizedMixin, OpenShiftCheck):
+    """Check that recommended disk space is available before a first-time install."""
+
+    name = "disk_availability"
+    tags = ["preflight"]
+
+    # Values taken from the official installation documentation:
+    # https://docs.openshift.org/latest/install_config/install/prerequisites.html#system-requirements
+    recommended_disk_space_bytes = {
+        "masters": 40 * 10**9,
+        "nodes": 15 * 10**9,
+        "etcd": 20 * 10**9,
+    }
+
+    @classmethod
+    def is_active(cls, task_vars):
+        """Skip hosts that do not have recommended disk space requirements."""
+        group_names = get_var(task_vars, "group_names", default=[])
+        has_disk_space_recommendation = bool(set(group_names).intersection(cls.recommended_disk_space_bytes))
+        return super(DiskAvailability, cls).is_active(task_vars) and has_disk_space_recommendation
+
+    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)
+
+        if free_bytes < min_free_bytes:
+            return {
+                'failed': True,
+                'msg': (
+                    'Available disk space ({:.1f} GB) for the volume containing '
+                    '"/var" is below minimum recommended space ({:.1f} GB)'
+                ).format(float(free_bytes) / 10**9, float(min_free_bytes) / 10**9)
+            }
+
+        return {}
+
+    @staticmethod
+    def openshift_available_disk(ansible_mounts):
+        """Determine the available disk space for an OpenShift installation.
+
+        ansible_mounts should be a list of dicts like the 'setup' Ansible module
+        returns.
+        """
+        # priority list in descending order
+        supported_mnt_paths = ["/var", "/"]
+        available_mnts = {mnt.get("mount"): mnt for mnt in ansible_mounts}
+
+        try:
+            for path in supported_mnt_paths:
+                if path in available_mnts:
+                    return available_mnts[path]["size_available"]
+        except KeyError:
+            pass
+
+        paths = ''.join(sorted(available_mnts)) or 'none'
+        msg = "Unable to determine available disk space. Paths mounted: {}.".format(paths)
+        raise OpenShiftCheckException(msg)

+ 44 - 0
roles/openshift_health_checker/openshift_checks/memory_availability.py

@@ -0,0 +1,44 @@
+# pylint: disable=missing-docstring
+from openshift_checks import OpenShiftCheck, get_var
+
+
+class MemoryAvailability(OpenShiftCheck):
+    """Check that recommended memory is available."""
+
+    name = "memory_availability"
+    tags = ["preflight"]
+
+    # 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,
+    }
+
+    @classmethod
+    def is_active(cls, task_vars):
+        """Skip hosts that do not have recommended memory requirements."""
+        group_names = get_var(task_vars, "group_names", default=[])
+        has_memory_recommendation = bool(set(group_names).intersection(cls.recommended_memory_bytes))
+        return super(MemoryAvailability, cls).is_active(task_vars) and has_memory_recommendation
+
+    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
+
+        min_memory_bytes = max(self.recommended_memory_bytes.get(name, 0) for name in group_names)
+
+        if total_memory_bytes < min_memory_bytes:
+            return {
+                'failed': True,
+                'msg': (
+                    'Available memory ({available:.1f} GB) '
+                    'below recommended value ({recommended:.1f} GB)'
+                ).format(
+                    available=float(total_memory_bytes) / 10**9,
+                    recommended=float(min_memory_bytes) / 10**9,
+                ),
+            }
+
+        return {}

+ 7 - 10
roles/openshift_health_checker/openshift_checks/mixins.py

@@ -1,4 +1,8 @@
-# pylint: disable=missing-docstring
+# pylint: disable=missing-docstring,too-few-public-methods
+"""
+Mixin classes meant to be used with subclasses of OpenShiftCheck.
+"""
+
 from openshift_checks import get_var
 
 
@@ -7,12 +11,5 @@ class NotContainerizedMixin(object):
 
     @classmethod
     def is_active(cls, task_vars):
-        return (
-            # This mixin is meant to be used with subclasses of OpenShiftCheck.
-            super(NotContainerizedMixin, cls).is_active(task_vars) and
-            not cls.is_containerized(task_vars)
-        )
-
-    @staticmethod
-    def is_containerized(task_vars):
-        return get_var(task_vars, "openshift", "common", "is_containerized")
+        is_containerized = get_var(task_vars, "openshift", "common", "is_containerized")
+        return super(NotContainerizedMixin, cls).is_active(task_vars) and not is_containerized

+ 155 - 0
roles/openshift_health_checker/test/disk_availability_test.py

@@ -0,0 +1,155 @@
+import pytest
+
+from openshift_checks.disk_availability import DiskAvailability, OpenShiftCheckException
+
+
+@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),
+    (['etcd'], False, True),
+    (['masters', 'nodes'], False, True),
+    (['masters', 'etcd'], False, True),
+    ([], False, False),
+    (['lb'], False, False),
+    (['nfs'], False, False),
+])
+def test_is_active(group_names, is_containerized, is_active):
+    task_vars = dict(
+        group_names=group_names,
+        openshift=dict(common=dict(is_containerized=is_containerized)),
+    )
+    assert DiskAvailability.is_active(task_vars=task_vars) == is_active
+
+
+@pytest.mark.parametrize('ansible_mounts,extra_words', [
+    ([], ['none']),  # empty ansible_mounts
+    ([{'mount': '/mnt'}], ['/mnt']),  # missing relevant mount paths
+    ([{'mount': '/var'}], ['/var']),  # missing size_available
+])
+def test_cannot_determine_available_disk(ansible_mounts, extra_words):
+    task_vars = dict(
+        group_names=['masters'],
+        ansible_mounts=ansible_mounts,
+    )
+    check = DiskAvailability(execute_module=fake_execute_module)
+
+    with pytest.raises(OpenShiftCheckException) as excinfo:
+        check.run(tmp=None, task_vars=task_vars)
+
+    for word in 'determine available disk'.split() + extra_words:
+        assert word in str(excinfo.value)
+
+
+@pytest.mark.parametrize('group_names,ansible_mounts', [
+    (
+        ['masters'],
+        [{
+            'mount': '/',
+            'size_available': 40 * 10**9 + 1,
+        }],
+    ),
+    (
+        ['nodes'],
+        [{
+            'mount': '/',
+            'size_available': 15 * 10**9 + 1,
+        }],
+    ),
+    (
+        ['etcd'],
+        [{
+            'mount': '/',
+            'size_available': 20 * 10**9 + 1,
+        }],
+    ),
+    (
+        ['etcd'],
+        [{
+            # not enough space on / ...
+            'mount': '/',
+            'size_available': 0,
+        }, {
+            # ... but enough on /var
+            'mount': '/var',
+            'size_available': 20 * 10**9 + 1,
+        }],
+    ),
+])
+def test_succeeds_with_recommended_disk_space(group_names, ansible_mounts):
+    task_vars = dict(
+        group_names=group_names,
+        ansible_mounts=ansible_mounts,
+    )
+
+    check = DiskAvailability(execute_module=fake_execute_module)
+    result = check.run(tmp=None, task_vars=task_vars)
+
+    assert not result.get('failed', False)
+
+
+@pytest.mark.parametrize('group_names,ansible_mounts,extra_words', [
+    (
+        ['masters'],
+        [{
+            'mount': '/',
+            'size_available': 1,
+        }],
+        ['0.0 GB'],
+    ),
+    (
+        ['nodes'],
+        [{
+            'mount': '/',
+            'size_available': 1 * 10**9,
+        }],
+        ['1.0 GB'],
+    ),
+    (
+        ['etcd'],
+        [{
+            'mount': '/',
+            'size_available': 1,
+        }],
+        ['0.0 GB'],
+    ),
+    (
+        ['nodes', 'masters'],
+        [{
+            'mount': '/',
+            # enough space for a node, not enough for a master
+            'size_available': 15 * 10**9 + 1,
+        }],
+        ['15.0 GB'],
+    ),
+    (
+        ['etcd'],
+        [{
+            # enough space on / ...
+            'mount': '/',
+            'size_available': 20 * 10**9 + 1,
+        }, {
+            # .. but not enough on /var
+            'mount': '/var',
+            'size_available': 0,
+        }],
+        ['0.0 GB'],
+    ),
+])
+def test_fails_with_insufficient_disk_space(group_names, ansible_mounts, extra_words):
+    task_vars = dict(
+        group_names=group_names,
+        ansible_mounts=ansible_mounts,
+    )
+
+    check = DiskAvailability(execute_module=fake_execute_module)
+    result = check.run(tmp=None, task_vars=task_vars)
+
+    assert result['failed']
+    for word in 'below recommended'.split() + extra_words:
+        assert word in result['msg']
+
+
+def fake_execute_module(*args):
+    raise AssertionError('this function should not be called')

+ 91 - 0
roles/openshift_health_checker/test/memory_availability_test.py

@@ -0,0 +1,91 @@
+import pytest
+
+from openshift_checks.memory_availability import MemoryAvailability
+
+
+@pytest.mark.parametrize('group_names,is_active', [
+    (['masters'], True),
+    (['nodes'], True),
+    (['etcd'], True),
+    (['masters', 'nodes'], True),
+    (['masters', 'etcd'], True),
+    ([], False),
+    (['lb'], False),
+    (['nfs'], False),
+])
+def test_is_active(group_names, is_active):
+    task_vars = dict(
+        group_names=group_names,
+    )
+    assert MemoryAvailability.is_active(task_vars=task_vars) == is_active
+
+
+@pytest.mark.parametrize('group_names,ansible_memtotal_mb', [
+    (
+        ['masters'],
+        17200,
+    ),
+    (
+        ['nodes'],
+        8200,
+    ),
+    (
+        ['etcd'],
+        22200,
+    ),
+    (
+        ['masters', 'nodes'],
+        17000,
+    ),
+])
+def test_succeeds_with_recommended_memory(group_names, ansible_memtotal_mb):
+    task_vars = dict(
+        group_names=group_names,
+        ansible_memtotal_mb=ansible_memtotal_mb,
+    )
+
+    check = MemoryAvailability(execute_module=fake_execute_module)
+    result = check.run(tmp=None, task_vars=task_vars)
+
+    assert not result.get('failed', False)
+
+
+@pytest.mark.parametrize('group_names,ansible_memtotal_mb,extra_words', [
+    (
+        ['masters'],
+        0,
+        ['0.0 GB'],
+    ),
+    (
+        ['nodes'],
+        100,
+        ['0.1 GB'],
+    ),
+    (
+        ['etcd'],
+        -1,
+        ['0.0 GB'],
+    ),
+    (
+        ['nodes', 'masters'],
+        # enough memory for a node, not enough for a master
+        11000,
+        ['11.0 GB'],
+    ),
+])
+def test_fails_with_insufficient_memory(group_names, ansible_memtotal_mb, extra_words):
+    task_vars = dict(
+        group_names=group_names,
+        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']
+    for word in 'below recommended'.split() + extra_words:
+        assert word in result['msg']
+
+
+def fake_execute_module(*args):
+    raise AssertionError('this function should not be called')