Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ram and non-containerized storage preflight check #3518

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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 {}
17 changes: 7 additions & 10 deletions roles/openshift_health_checker/openshift_checks/mixins.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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 changes: 155 additions & 0 deletions roles/openshift_health_checker/test/disk_availability_test.py
Original file line number Diff line number Diff line change
@@ -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 changes: 91 additions & 0 deletions roles/openshift_health_checker/test/memory_availability_test.py
Original file line number Diff line number Diff line change
@@ -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')