#!/usr/bin/env python3 # # Copyright (C) 2017 FreeIPA Contributors see COPYING for license # """In-tree development server See README.md for more details. """ import logging import linecache import os import optparse # pylint: disable=deprecated-module import ssl import sys import time import tracemalloc import warnings # Don't import any ipa modules here so tracemalloc can trace memory usage. import gssapi # pylint: disable=import-error from werkzeug.middleware.profiler import ProfilerMiddleware from werkzeug.exceptions import NotFound from werkzeug.serving import run_simple from werkzeug.utils import redirect, append_slash_redirect from werkzeug.middleware.dispatcher import DispatcherMiddleware from werkzeug.middleware.shared_data import SharedDataMiddleware # pylint: enable=import-error logger = logging.getLogger(os.path.basename(__file__)) BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) STATIC_FILES = { '/ipa/ui': os.path.join(BASEDIR, 'install/ui'), '/ipa/ui/js': os.path.join(BASEDIR, 'install/ui/src'), '/ipa/ui/js/dojo': os.path.join(BASEDIR, 'install/ui/build/dojo'), '/ipa/ui/fonts': '/usr/share/fonts', } def display_tracemalloc(snapshot, key_type='lineno', limit=10): snapshot = snapshot.filter_traces(( tracemalloc.Filter(False, ""), tracemalloc.Filter(False, "*/idna/*.py"), )) top_stats = snapshot.statistics(key_type) print("Top {} lines".format(limit)) for index, stat in enumerate(top_stats[:limit], 1): frame = stat.traceback[0] # replace "/path/to/module/file.py" with "module/file.py" filename = os.sep.join(frame.filename.split(os.sep)[-2:]) print("#{}: {}:{}: {:.1f} KiB".format( index, filename, frame.lineno, stat.size // 1024)) line = linecache.getline(frame.filename, frame.lineno).strip() if line: print(' {}'.format(line)) other = top_stats[limit:] if other: size = sum(stat.size for stat in other) print("{} other: {:.1f} KiB".format(len(other), size // 1024)) total = sum(stat.size for stat in top_stats) current, peak = tracemalloc.get_traced_memory() print("Total allocated size: {:8.1f} KiB".format(total // 1024)) print("Current size: {:8.1f} KiB".format(current // 1024)) print("Peak size: {:8.1f} KiB".format(peak // 1024)) def get_ccname(): """Retrieve and validate Kerberos credential cache Only FILE schema is supported. """ from ipalib import krb_utils ccname = os.environ.get('KRB5CCNAME') if ccname is None: raise ValueError("KRB5CCNAME env var is not set.") scheme, location = krb_utils.krb5_parse_ccache(ccname) if scheme != 'FILE': # MEMORY makes no sense raise ValueError("Unsupported KRB5CCNAME scheme {}".format(scheme)) if not os.path.isfile(location): raise ValueError("KRB5CCNAME file '{}' does not exit".format(location)) return krb_utils.krb5_unparse_ccache(scheme, location) class KRBCheater: """Add KRB5CCNAME and GSS_NAME to WSGI environ """ def __init__(self, app, ccname): self.app = app self.ccname = ccname self.creds = gssapi.Credentials( usage='initiate', store={'ccache': ccname} ) def __call__(self, environ, start_response): environ['KRB5CCNAME'] = self.ccname environ['GSS_NAME'] = self.creds.name return self.app(environ, start_response) class TracemallocMiddleware: def __init__(self, app, api): self.app = app self.api = api def __call__(self, environ, start_response): # We are only interested in request traces. # Each request is handled in a new process. tracemalloc.clear_traces() try: return self.app(environ, start_response) finally: snapshot = tracemalloc.take_snapshot() display_tracemalloc(snapshot, limit=self.api.env.lite_tracemalloc) class StaticFilesMiddleware(SharedDataMiddleware): def get_directory_loader(self, directory): # override directory loader to support index.html def loader(path): if path is not None: path = os.path.join(directory, path) else: path = directory # use index.html for directory views if os.path.isdir(path): path = os.path.join(path, 'index.html') if os.path.isfile(path): return os.path.basename(path), self._opener(path) return None, None return loader def init_api(ccname): """Initialize FreeIPA API from command line """ from ipalib import __file__ as ipalib_file from ipalib import api from ipalib.errors import NetworkError importdir = os.path.dirname(os.path.dirname(os.path.abspath(ipalib_file))) if importdir != BASEDIR: warnings.warn( "ipalib was imported from '{}' instead of '{}'!".format( importdir, BASEDIR), RuntimeWarning ) parser = optparse.OptionParser() parser.add_option( '--dev', help='Run WebUI in development mode', default=True, action='store_false', dest='prod', ) parser.add_option( '--host', help='Listen on address HOST (default 127.0.0.1)', default='127.0.0.1', ) parser.add_option( '--port', help='Listen on PORT (default 8888)', default=8888, type='int', ) parser.add_option( '--enable-profiler', help="Path to WSGI profiler directory or '-' for stderr", default=None, type='str', ) parser.add_option( '--enable-tracemalloc', help="Enable memory tracer", default=0, type='int', ) api.env.in_server = True api.env.startup_traceback = True # workaround for RefererError in rpcserver api.env.in_tree = True # workaround: AttributeError: locked: cannot set ldap2.time_limit to None api.env.mode = 'production' start_time = time.time() # pylint: disable=unused-variable options, args = api.bootstrap_with_global_options(parser, context='lite') api.env._merge( lite_port=options.port, lite_host=options.host, webui_prod=options.prod, lite_profiler=options.enable_profiler, lite_tracemalloc=options.enable_tracemalloc, lite_pem=api.env._join('dot_ipa', 'lite.pem'), ) api.finalize() api_time = time.time() logger.info("API initialized in %0.3f sec", api_time - start_time) # Validate LDAP connection and pre-fetch schema # Pre-fetching makes the lite-server behave similar to mod_wsgi. werkzeug's # multi-process WSGI server forks a new process for each request while # mod_wsgi handles multiple request in a daemon process. Without schema # cache, every lite server request would download the LDAP schema and # distort performance profiles. ldap2 = api.Backend.ldap2 try: if not ldap2.isconnected(): ldap2.connect(ccache=ccname) except NetworkError as e: logger.error("Unable to connect to LDAP: %s", e) logger.error("lite-server needs a working LDAP connect. Did you " "configure ldap_uri in '%s'?", api.env.conf_default) sys.exit(2) else: # prefetch schema assert ldap2.schema # Disconnect main process, each WSGI request handler subprocess will # must have its own connection. ldap2.disconnect() ldap_time = time.time() logger.info("LDAP schema retrieved %0.3f sec", ldap_time - api_time) return api def redirect_ui(app): """Redirects for UI """ def wsgi(environ, start_response): path_info = environ['PATH_INFO'] if path_info in {'/', '/ipa', '/ipa/'}: response = redirect('/ipa/ui/') return response(environ, start_response) # Redirect to append slash to some routes if path_info in {'/ipa/ui', '/ipa/ui/test'}: response = append_slash_redirect(environ) return response(environ, start_response) if path_info == '/favicon.ico': response = redirect('/ipa/ui/favicon.ico') return response(environ, start_response) return app(environ, start_response) return wsgi def main(): # workaround, start tracing IPA imports and API init ASAP if any('--enable-tracemalloc' in arg for arg in sys.argv): tracemalloc.start() try: ccname = get_ccname() except ValueError as e: print("ERROR:", e, file=sys.stderr) print("\nliteserver requires a KRB5CCNAME env var and " "a valid Kerberos TGT:\n", file=sys.stderr) print(" export KRB5CCNAME=~/.ipa/ccache", file=sys.stderr) print(" kinit\n", file=sys.stderr) sys.exit(1) api = init_api(ccname) if api.env.lite_tracemalloc: # print memory snapshot of import + init snapshot = tracemalloc.take_snapshot() display_tracemalloc(snapshot, limit=api.env.lite_tracemalloc) del snapshot # From here on, only trace requests. tracemalloc.clear_traces() if os.path.isfile(api.env.lite_pem): ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) ctx.load_cert_chain(api.env.lite_pem) else: ctx = None app = NotFound() app = DispatcherMiddleware(app, { '/ipa': KRBCheater(api.Backend.wsgi_dispatch, ccname), }) # only profile api calls if api.env.lite_profiler == '-': print('Profiler enable, stats are written to stderr.') app = ProfilerMiddleware(app, stream=sys.stderr, restrictions=(30,)) elif api.env.lite_profiler: profile_dir = os.path.abspath(api.env.lite_profiler) print("Profiler enable, profiles are stored in '{}'.".format( profile_dir )) app = ProfilerMiddleware(app, profile_dir=profile_dir) if api.env.lite_tracemalloc: app = TracemallocMiddleware(app, api) app = StaticFilesMiddleware(app, STATIC_FILES) app = redirect_ui(app) run_simple( hostname=api.env.lite_host, port=api.env.lite_port, application=app, processes=5, ssl_context=ctx, use_reloader=True, # debugger doesn't work because framework catches all exceptions # use_debugger=not api.env.webui_prod, # use_evalex=not api.env.webui_prod, ) if __name__ == '__main__': main()