summaryrefslogtreecommitdiffstats
path: root/roles/openshift_health_checker/openshift_checks/disk_availability.py
blob: e93e81efab8638b287bcc2e274d337b27a00e3fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"""Check that there is enough disk space in predefined paths."""

import os.path
import tempfile

from openshift_checks import OpenShiftCheck, OpenShiftCheckException, get_var


class DiskAvailability(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 = {
        '/var': {
            'masters': 40 * 10**9,
            'nodes': 15 * 10**9,
            'etcd': 20 * 10**9,
        },
        # Used to copy client binaries into,
        # see roles/openshift_cli/library/openshift_container_binary_sync.py.
        '/usr/local/bin': {
            'masters': 1 * 10**9,
            'nodes': 1 * 10**9,
            'etcd': 1 * 10**9,
        },
        # Used as temporary storage in several cases.
        tempfile.gettempdir(): {
            'masters': 1 * 10**9,
            'nodes': 1 * 10**9,
            'etcd': 1 * 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=[])
        active_groups = set()
        for recommendation in cls.recommended_disk_space_bytes.values():
            active_groups.update(recommendation.keys())
        has_disk_space_recommendation = bool(active_groups.intersection(group_names))
        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")
        ansible_mounts = {mount['mount']: mount for mount in ansible_mounts}

        user_config = get_var(task_vars, "openshift_check_min_host_disk_gb", default={})
        try:
            # For backwards-compatibility, if openshift_check_min_host_disk_gb
            # is a number, then it overrides the required config for '/var'.
            number = float(user_config)
            user_config = {
                '/var': {
                    'masters': number,
                    'nodes': number,
                    'etcd': number,
                },
            }
        except TypeError:
            # If it is not a number, then it should be a nested dict.
            pass

        # TODO: as suggested in
        # https://github.com/openshift/openshift-ansible/pull/4436#discussion_r122180021,
        # maybe we could support checking disk availability in paths that are
        # not part of the official recommendation but present in the user
        # configuration.
        for path, recommendation in self.recommended_disk_space_bytes.items():
            free_bytes = self.free_bytes(path, ansible_mounts)
            recommended_bytes = max(recommendation.get(name, 0) for name in group_names)

            config = user_config.get(path, {})
            # NOTE: the user config is in GB, but we compare bytes, thus the
            # conversion.
            config_bytes = max(config.get(name, 0) for name in group_names) * 10**9
            recommended_bytes = config_bytes or recommended_bytes

            if free_bytes < recommended_bytes:
                free_gb = float(free_bytes) / 10**9
                recommended_gb = float(recommended_bytes) / 10**9
                return {
                    'failed': True,
                    'msg': (
                        'Available disk space in "{}" ({:.1f} GB) '
                        'is below minimum recommended ({:.1f} GB)'
                    ).format(path, free_gb, recommended_gb)
                }

        return {}

    @staticmethod
    def free_bytes(path, ansible_mounts):
        """Return the size available in path based on ansible_mounts."""
        mount_point = path
        # arbitry value to prevent an infinite loop, in the unlike case that '/'
        # is not in ansible_mounts.
        max_depth = 32
        while mount_point not in ansible_mounts and max_depth > 0:
            mount_point = os.path.dirname(mount_point)
            max_depth -= 1

        try:
            free_bytes = ansible_mounts[mount_point]['size_available']
        except KeyError:
            known_mounts = ', '.join('"{}"'.format(mount) for mount in sorted(ansible_mounts)) or 'none'
            msg = 'Unable to determine disk availability for "{}". Known mount points: {}.'
            raise OpenShiftCheckException(msg.format(path, known_mounts))

        return free_bytes