From cfad7af35dd5a2cdd4081d1e9ac7c245f47f1dce Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 27 Aug 2020 15:22:12 -0400 Subject: [PATCH] Require at least 1.6Gb of available RAM to install the server Verify that there is at least 1.6Gb of usable RAM on the system. Swap is not considered. While swap would allow a user to minimally install IPA it would not be a great experience. Using any proc-based method to check for available RAM does not work in containers unless /proc is re-mounted so use cgroups instead. This also handles the case if the container has memory constraints on it (-m). There are envs which mount 'proc' with enabled hidepid option 1 so don't assume that is readable. Add a switch to skip this memory test if the user is sure they know what they are doing. is_hidepid() contributed by Stanislav Levin https://pagure.io/freeipa/issue/8404 Reviewed-By: Alexander Bokovoy Reviewed-By: Stanislav Levin --- freeipa.spec.in | 2 + ipaserver/install/installutils.py | 80 ++++++++++++++++++++++ ipaserver/install/server/__init__.py | 6 ++ ipaserver/install/server/install.py | 2 + ipaserver/install/server/replicainstall.py | 6 +- 5 files changed, 94 insertions(+), 2 deletions(-) diff --git a/freeipa.spec.in b/freeipa.spec.in index 0e7a51f44..8609f7ff4 100755 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -242,6 +242,7 @@ BuildRequires: python3-netaddr >= %{python_netaddr_version} BuildRequires: python3-pyasn1 BuildRequires: python3-pyasn1-modules BuildRequires: python3-six +BuildRequires: python3-psutil # # Build dependencies for wheel packaging and PyPI upload @@ -444,6 +445,7 @@ Requires: python3-lxml Requires: python3-pki >= %{pki_version} Requires: python3-pyasn1 >= 0.3.2-2 Requires: python3-sssdconfig >= %{sssd_version} +Requires: python3-psutil Requires: rpm-libs # Indirect dependency: use newer urllib3 with TLS 1.3 PHA support %if 0%{?rhel} diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py index 60dd6dceb..6be7669c2 100644 --- a/ipaserver/install/installutils.py +++ b/ipaserver/install/installutils.py @@ -18,6 +18,7 @@ # from __future__ import absolute_import +from __future__ import division from __future__ import print_function import logging @@ -28,6 +29,7 @@ import ldif import os import re import fileinput +import psutil import sys import tempfile import shutil @@ -963,6 +965,84 @@ def check_entropy(): logger.debug("Invalid value in %s %s", paths.ENTROPY_AVAIL, e) +def is_hidepid(): + """Determine if /proc is mounted with hidepid=1/2 option""" + try: + os.lstat('/proc/1/stat') + except (FileNotFoundError, PermissionError): + return True + return False + + +def in_container(): + """Determine if we're running in a container. + + virt-what will return the underlying machine information so + isn't usable here. + + systemd-detect-virt requires the whole systemd subsystem which + isn't a reasonable require in a container. + """ + if not is_hidepid(): + with open('/proc/1/sched', 'r') as sched: + data_sched = sched.readline() + else: + data_sched = [] + + with open('/proc/self/cgroup', 'r') as cgroup: + data_cgroup = cgroup.readline() + + checks = [ + data_sched.split()[0] not in ('systemd', 'init',), + data_cgroup.split()[0] in ('libpod'), + os.path.exists('/.dockerenv'), + os.path.exists('/.dockerinit'), + os.getenv('container', None) is not None + ] + + return any(checks) + + +def check_available_memory(ca=False): + """ + Raise an exception if there isn't enough memory for IPA to install. + + In a container then psutil will most likely return the host memory + and not the container. If in a container use the cgroup values which + also may not be constrained but it's the best approximation. + + 2GB is the rule-of-thumb minimum but the server is installable with + less. + + The CA uses ~150MB in a fresh install. + + Use Kb instead of KiB to leave a bit of slush for the OS + """ + minimum_suggested = 1000 * 1000 * 1000 * 1.6 + if not ca: + minimum_suggested -= 150 * 1000 * 1000 + if in_container(): + if os.path.exists( + '/sys/fs/cgroup/memory/memory.limit_in_bytes' + ) and os.path.exists('/sys/fs/cgroup/memory/memory.usage_in_bytes'): + with open('/sys/fs/cgroup/memory/memory.limit_in_bytes') as fd: + limit = int(fd.readline()) + with open('/sys/fs/cgroup/memory/memory.usage_in_bytes') as fd: + used = int(fd.readline()) + available = limit - used + else: + raise ScriptError( + "Unable to determine the amount of available RAM" + ) + else: + available = psutil.virtual_memory().available + logger.debug("Available memory is %sB", available) + if available < minimum_suggested: + raise ScriptError( + "Less than the minimum 1.6GB of RAM is available, " + "%.2fGB available" % (available / (1024 * 1024 * 1024)) + ) + def load_external_cert(files, ca_subject): """ Load and verify external CA certificate chain from multiple files. diff --git a/ipaserver/install/server/__init__.py b/ipaserver/install/server/__init__.py index 20b519a44..f9150ee4e 100644 --- a/ipaserver/install/server/__init__.py +++ b/ipaserver/install/server/__init__.py @@ -330,6 +330,12 @@ class ServerInstallInterface(ServerCertificateInstallInterface, ) dirsrv_config_file = enroll_only(dirsrv_config_file) + skip_mem_check = knob( + None, + description="Skip checking for minimum required memory", + ) + skip_mem_check = enroll_only(skip_mem_check) + @dirsrv_config_file.validator def dirsrv_config_file(self, value): if not os.path.exists(value): diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py index 50a0a60dd..2d632576f 100644 --- a/ipaserver/install/server/install.py +++ b/ipaserver/install/server/install.py @@ -346,6 +346,8 @@ def install_check(installer): dirsrv_ca_cert = None pkinit_ca_cert = None + if not options.skip_mem_check: + installutils.check_available_memory(ca=options.setup_ca) tasks.check_ipv6_stack_enabled() tasks.check_selinux_status() check_ldap_conf() diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py index c6c438a04..f75f5fd99 100644 --- a/ipaserver/install/server/replicainstall.py +++ b/ipaserver/install/server/replicainstall.py @@ -569,7 +569,9 @@ def check_remote_version(client, local_version): "the local version ({})".format(remote_version, local_version)) -def common_check(no_ntp): +def common_check(no_ntp, skip_mem_check, setup_ca): + if not skip_mem_check: + installutils.check_available_memory(ca=setup_ca) tasks.check_ipv6_stack_enabled() tasks.check_selinux_status() check_ldap_conf() @@ -776,7 +778,7 @@ def promote_check(installer): installer._top_dir = tempfile.mkdtemp("ipa") # check selinux status, http and DS ports, NTP conflicting services - common_check(options.no_ntp) + common_check(options.no_ntp, options.skip_mem_check, options.setup_ca) if options.setup_ca and any([options.dirsrv_cert_files, options.http_cert_files,