2019-12-03 03:35:48 -06:00
|
|
|
#!/usr/bin/env python3
|
2017-01-21 12:34:12 -06:00
|
|
|
#
|
|
|
|
# Copyright (C) 2017 FreeIPA Contributors see COPYING for license
|
|
|
|
#
|
|
|
|
"""In-tree development server
|
|
|
|
|
2020-01-17 05:53:31 -06:00
|
|
|
See README.md for more details.
|
2017-01-21 12:34:12 -06:00
|
|
|
"""
|
2017-05-24 08:42:23 -05:00
|
|
|
import logging
|
2019-12-03 03:25:54 -06:00
|
|
|
import linecache
|
2017-01-21 12:34:12 -06:00
|
|
|
import os
|
|
|
|
import optparse # pylint: disable=deprecated-module
|
|
|
|
import ssl
|
|
|
|
import sys
|
2017-02-20 04:58:17 -06:00
|
|
|
import time
|
2019-12-03 03:25:54 -06:00
|
|
|
import tracemalloc
|
2017-01-21 12:34:12 -06:00
|
|
|
import warnings
|
|
|
|
|
2019-12-03 03:25:54 -06:00
|
|
|
# Don't import any ipa modules here so tracemalloc can trace memory usage.
|
2017-01-21 12:34:12 -06:00
|
|
|
|
2019-12-03 03:35:48 -06:00
|
|
|
import gssapi
|
2017-01-21 12:34:12 -06:00
|
|
|
# pylint: disable=import-error
|
2020-06-05 18:01:12 -05:00
|
|
|
from werkzeug.middleware.profiler import ProfilerMiddleware
|
2017-01-21 12:34:12 -06:00
|
|
|
from werkzeug.exceptions import NotFound
|
|
|
|
from werkzeug.serving import run_simple
|
|
|
|
from werkzeug.utils import redirect, append_slash_redirect
|
2020-06-05 18:01:12 -05:00
|
|
|
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
|
|
|
from werkzeug.middleware.shared_data import SharedDataMiddleware
|
2017-01-21 12:34:12 -06:00
|
|
|
# pylint: enable=import-error
|
|
|
|
|
2017-05-24 08:42:23 -05:00
|
|
|
logger = logging.getLogger(os.path.basename(__file__))
|
|
|
|
|
2017-01-21 12:34:12 -06:00
|
|
|
|
|
|
|
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',
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-12-03 03:25:54 -06:00
|
|
|
def display_tracemalloc(snapshot, key_type='lineno', limit=10):
|
|
|
|
snapshot = snapshot.filter_traces((
|
|
|
|
tracemalloc.Filter(False, "<frozen importlib._bootstrap*"),
|
|
|
|
tracemalloc.Filter(False, "<unknown>"),
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
2017-01-21 12:34:12 -06:00
|
|
|
def get_ccname():
|
|
|
|
"""Retrieve and validate Kerberos credential cache
|
|
|
|
|
|
|
|
Only FILE schema is supported.
|
|
|
|
"""
|
2019-12-03 03:25:54 -06:00
|
|
|
from ipalib import krb_utils
|
|
|
|
|
2017-01-21 12:34:12 -06:00
|
|
|
ccname = os.environ.get('KRB5CCNAME')
|
|
|
|
if ccname is None:
|
|
|
|
raise ValueError("KRB5CCNAME env var is not set.")
|
2019-12-03 03:25:54 -06:00
|
|
|
scheme, location = krb_utils.krb5_parse_ccache(ccname)
|
2017-01-21 12:34:12 -06:00
|
|
|
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))
|
2019-12-03 03:25:54 -06:00
|
|
|
return krb_utils.krb5_unparse_ccache(scheme, location)
|
2017-01-21 12:34:12 -06:00
|
|
|
|
|
|
|
|
2018-09-26 04:59:50 -05:00
|
|
|
class KRBCheater:
|
2019-12-03 03:35:48 -06:00
|
|
|
"""Add KRB5CCNAME and GSS_NAME to WSGI environ
|
2017-01-21 12:34:12 -06:00
|
|
|
"""
|
|
|
|
def __init__(self, app, ccname):
|
|
|
|
self.app = app
|
|
|
|
self.ccname = ccname
|
2019-12-03 03:35:48 -06:00
|
|
|
self.creds = gssapi.Credentials(
|
|
|
|
usage='initiate',
|
|
|
|
store={'ccache': ccname}
|
|
|
|
)
|
2017-01-21 12:34:12 -06:00
|
|
|
|
|
|
|
def __call__(self, environ, start_response):
|
|
|
|
environ['KRB5CCNAME'] = self.ccname
|
2019-12-03 03:35:48 -06:00
|
|
|
environ['GSS_NAME'] = self.creds.name
|
2017-01-21 12:34:12 -06:00
|
|
|
return self.app(environ, start_response)
|
|
|
|
|
|
|
|
|
2019-12-03 03:25:54 -06:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2017-01-21 12:34:12 -06:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2017-02-20 04:58:17 -06:00
|
|
|
def init_api(ccname):
|
2017-01-21 12:34:12 -06:00
|
|
|
"""Initialize FreeIPA API from command line
|
|
|
|
"""
|
2019-12-03 03:25:54 -06:00
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2017-01-21 12:34:12 -06:00
|
|
|
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',
|
|
|
|
)
|
2019-12-03 03:25:54 -06:00
|
|
|
parser.add_option(
|
|
|
|
'--enable-tracemalloc',
|
|
|
|
help="Enable memory tracer",
|
|
|
|
default=0,
|
|
|
|
type='int',
|
|
|
|
)
|
2017-01-21 12:34:12 -06:00
|
|
|
|
|
|
|
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'
|
|
|
|
|
2017-02-20 04:58:17 -06:00
|
|
|
start_time = time.time()
|
2017-01-21 12:34:12 -06:00
|
|
|
# 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,
|
2019-12-03 03:25:54 -06:00
|
|
|
lite_tracemalloc=options.enable_tracemalloc,
|
2017-01-21 12:34:12 -06:00
|
|
|
lite_pem=api.env._join('dot_ipa', 'lite.pem'),
|
|
|
|
)
|
|
|
|
api.finalize()
|
2017-02-20 04:58:17 -06:00
|
|
|
api_time = time.time()
|
2020-01-17 05:53:31 -06:00
|
|
|
logger.info("API initialized in %0.3f sec", api_time - start_time)
|
2017-02-20 04:58:17 -06:00
|
|
|
|
|
|
|
# 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:
|
2017-05-24 08:42:23 -05:00
|
|
|
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)
|
2017-02-20 04:58:17 -06:00
|
|
|
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()
|
2020-01-17 05:53:31 -06:00
|
|
|
logger.info("LDAP schema retrieved %0.3f sec", ldap_time - api_time)
|
2017-01-21 12:34:12 -06:00
|
|
|
|
2019-12-03 03:25:54 -06:00
|
|
|
return api
|
|
|
|
|
2017-01-21 12:34:12 -06:00
|
|
|
|
|
|
|
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():
|
2019-12-03 03:25:54 -06:00
|
|
|
# workaround, start tracing IPA imports and API init ASAP
|
|
|
|
if any('--enable-tracemalloc' in arg for arg in sys.argv):
|
|
|
|
tracemalloc.start()
|
|
|
|
|
2017-01-21 12:34:12 -06:00
|
|
|
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)
|
|
|
|
|
2019-12-03 03:25:54 -06:00
|
|
|
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()
|
2017-01-21 12:34:12 -06:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2019-12-03 03:25:54 -06:00
|
|
|
if api.env.lite_tracemalloc:
|
|
|
|
app = TracemallocMiddleware(app, api)
|
|
|
|
|
2017-01-21 12:34:12 -06:00
|
|
|
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()
|