From 63bf58e077c90ce0be02a3aceba9ce008d3e11b7 Mon Sep 17 00:00:00 2001
From: Christian Heimes <cheimes@redhat.com>
Date: Mon, 4 Jan 2021 09:16:03 +0100
Subject: [PATCH 1/2] Better mod_wsgi configuration

* Remove WSGIImportScript
* Configure process-group in WSGIScriptAlias
* Run WSGI app in main interpreter of daemon script

See: https://github.com/GrahamDumpleton/mod_wsgi/issues/642#issuecomment-749498828
Signed-off-by: Christian Heimes <cheimes@redhat.com>
---
 install/share/ipa-kdc-proxy.conf.template | 7 ++-----
 install/share/ipa.conf.template           | 9 +++------
 2 files changed, 5 insertions(+), 11 deletions(-)

diff --git a/install/share/ipa-kdc-proxy.conf.template b/install/share/ipa-kdc-proxy.conf.template
index 143f40fa4ff..986445e449d 100644
--- a/install/share/ipa-kdc-proxy.conf.template
+++ b/install/share/ipa-kdc-proxy.conf.template
@@ -19,14 +19,11 @@
 
 WSGIDaemonProcess kdcproxy processes=2 threads=15 maximum-requests=5000 \
   user=kdcproxy group=kdcproxy display-name=%{GROUP}
-WSGIImportScript /usr/share/ipa/kdcproxy.wsgi \
-  process-group=kdcproxy application-group=kdcproxy
-WSGIScriptAlias /KdcProxy /usr/share/ipa/kdcproxy.wsgi
+WSGIScriptAlias /KdcProxy /usr/share/ipa/kdcproxy.wsgi \
+  process-group=kdcproxy application-group=%{GLOBAL}
 WSGIScriptReloading Off
 
 <Location "/KdcProxy">
   Satisfy Any
   Require all granted
-  WSGIProcessGroup kdcproxy
-  WSGIApplicationGroup kdcproxy
 </Location>
diff --git a/install/share/ipa.conf.template b/install/share/ipa.conf.template
index d74e14d4fa3..e68ced25847 100644
--- a/install/share/ipa.conf.template
+++ b/install/share/ipa.conf.template
@@ -1,5 +1,5 @@
 #
-# VERSION 32 - DO NOT REMOVE THIS LINE
+# VERSION 33 - DO NOT REMOVE THIS LINE
 #
 # This file may be overwritten on upgrades.
 #
@@ -39,13 +39,12 @@ AddOutputFilterByType DEFLATE text/html text/plain text/xml \
 # should really be fixed by adding this its /etc/httpd/conf.d/wsgi.conf:
 WSGISocketPrefix $WSGI_PREFIX_DIR
 
-
 # Configure mod_wsgi handler for /ipa
 WSGIDaemonProcess ipa processes=$WSGI_PROCESSES threads=1 maximum-requests=500 \
   user=ipaapi group=ipaapi display-name=%{GROUP} socket-timeout=2147483647 \
   lang=C.UTF-8 locale=C.UTF-8
-WSGIImportScript /usr/share/ipa/wsgi.py process-group=ipa application-group=ipa
-WSGIScriptAlias /ipa /usr/share/ipa/wsgi.py
+WSGIScriptAlias /ipa /usr/share/ipa/wsgi.py process-group=ipa \
+  application-group=%{GLOBAL}
 WSGIScriptReloading Off
 
 
@@ -81,8 +80,6 @@ WSGIScriptReloading Off
   GssapiAllowedMech krb5
   Require valid-user
   ErrorDocument 401 /ipa/errors/unauthorized.html
-  WSGIProcessGroup ipa
-  WSGIApplicationGroup ipa
   Header always append X-Frame-Options DENY
   Header always append Content-Security-Policy "frame-ancestors 'none'"
 

From 595a8f4219092bc6ca4a3b977da0845663ac447a Mon Sep 17 00:00:00 2001
From: Christian Heimes <cheimes@redhat.com>
Date: Mon, 4 Jan 2021 09:24:56 +0100
Subject: [PATCH 2/2] Improve wsgi app loading

* move WSGI app code to main code base so it can be used with other
  WSGI servers that expect a Python package.
* populate LDAP schema early to speed up first request by ~200ms
* gc.collect() and gc.freeze() to improve memory handling and GC

Signed-off-by: Christian Heimes <cheimes@redhat.com>
---
 install/share/wsgi.py | 41 ++-------------------
 ipaserver/wsgi.py     | 83 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 38 deletions(-)
 create mode 100644 ipaserver/wsgi.py

diff --git a/install/share/wsgi.py b/install/share/wsgi.py
index cbdb0113250..debedbdc776 100644
--- a/install/share/wsgi.py
+++ b/install/share/wsgi.py
@@ -21,43 +21,8 @@
 #
 
 """
-WSGI appliction for IPA server.
+WSGI application for IPA server.
 """
-from __future__ import absolute_import
+from ipaserver.wsgi import create_application
 
-import logging
-import os
-import sys
-
-# Some dependencies like Dogtag's pki.client library and custodia use
-# python-requsts to make HTTPS connection. python-requests prefers
-# PyOpenSSL over Python's stdlib ssl module. PyOpenSSL is build on top
-# of python-cryptography which trigger a execmem SELinux violation
-# in the context of Apache HTTPD (httpd_execmem).
-# When requests is imported, it always tries to import pyopenssl glue
-# code from urllib3's contrib directory. The import of PyOpenSSL is
-# enough to trigger the SELinux denial.
-# Block any import of PyOpenSSL's SSL module by raising an ImportError
-sys.modules['OpenSSL.SSL'] = None
-
-from ipaplatform.paths import paths
-from ipalib import api
-
-logger = logging.getLogger(os.path.basename(__file__))
-
-api.bootstrap(context='server', confdir=paths.ETC_IPA, log=None)
-try:
-    api.finalize()
-except Exception as e:
-    logger.error('Failed to start IPA: %s', e)
-else:
-    logger.info('*** PROCESS START ***')
-
-    # This is the WSGI callable:
-    def application(environ, start_response):
-        if not environ['wsgi.multithread']:
-            return api.Backend.wsgi_dispatch(environ, start_response)
-        else:
-            logger.error("IPA does not work with the threaded MPM, "
-                         "use the pre-fork MPM")
-            raise RuntimeError('threaded MPM detected')
+application = create_application()
diff --git a/ipaserver/wsgi.py b/ipaserver/wsgi.py
new file mode 100644
index 00000000000..485ec07b34e
--- /dev/null
+++ b/ipaserver/wsgi.py
@@ -0,0 +1,83 @@
+#
+# Copyright (C) 2021  FreeIPA Contributors see COPYING for license
+#
+"""WSGI server application
+"""
+import gc
+import logging
+import os
+import sys
+
+# Some dependencies like Dogtag's pki.client library and custodia use
+# python-requsts to make HTTPS connection. python-requests prefers
+# PyOpenSSL over Python's stdlib ssl module. PyOpenSSL is build on top
+# of python-cryptography which trigger a execmem SELinux violation
+# in the context of Apache HTTPD (httpd_execmem).
+# When requests is imported, it always tries to import pyopenssl glue
+# code from urllib3's contrib directory. The import of PyOpenSSL is
+# enough to trigger the SELinux denial.
+# Block any import of PyOpenSSL's SSL module by raising an ImportError
+sys.modules["OpenSSL.SSL"] = None
+
+from ipaplatform.paths import paths
+from ipalib import api
+from ipapython import ipaldap
+
+logger = logging.getLogger(os.path.basename(__file__))
+
+
+def populate_schema_cache(api=api):
+    """populate schema cache in parent process
+
+    LDAP server schema is available for anonymous binds.
+    """
+    conn = ipaldap.ldap_initialize(api.env.ldap_uri)
+    try:
+        ipaldap.schema_cache.get_schema(api.env.ldap_uri, conn)
+    except Exception as e:
+        logger.error("Failed to pre-populate LDAP schema cache: %s", e)
+    finally:
+        try:
+            conn.unbind_s()
+        except AttributeError:
+            # SimpleLDAPObject has no attribute '_l'
+            pass
+
+
+def create_application():
+    api.bootstrap(context="server", confdir=paths.ETC_IPA, log=None)
+
+    try:
+        api.finalize()
+    except Exception as e:
+        logger.error("Failed to start IPA: %s", e)
+        raise
+
+    # speed up first request to each worker by 200ms
+    populate_schema_cache()
+
+    # collect garbage and freeze all objects that are currently tracked by
+    # cyclic garbage collector. We assume that vast majority of currently
+    # loaded objects won't be removed in requests. This speeds up GC
+    # collections and improve CoW memory handling.
+    gc.collect()
+    if hasattr(gc, "freeze"):
+        # Python 3.7+
+        gc.freeze()
+
+    # This is the WSGI callable:
+    def application(environ, start_response):
+        if not environ["wsgi.multithread"]:
+            return api.Backend.wsgi_dispatch(environ, start_response)
+        else:
+            logger.error(
+                "IPA does not work with the threaded MPM, "
+                "use the pre-fork MPM"
+            )
+            raise RuntimeError("threaded MPM detected")
+
+    return application
+
+
+if __name__ == "__main__":
+    application = create_application()
