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,