blob: 888f0f147435bc8413996b667d1586d2e0be7d0a [file] [log] [blame]
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Kludges to support legacy Autotest code.
Autotest imports should be done by calling monkeypatch() first and then
calling load(). monkeypatch() should only be called once from a
script's main function.
chromite imports should be done with chromite_load(), and any third
party packages should be imported with deps_load(). The reason for this
is to present a clear API for these unsafe imports, making it easier to
identify which imports are currently unsafe. Eventually, everything
should be moved to virtualenv, but that will not be in the near future.
As an alternative to calling monkeypatch and load in a small scope wherever
an external module is needed, chromite and autotest imports may also be done at
the top level of a module using deferred_load() and deferred_chromite_load().
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import ast
import contextlib
import imp
import importlib
import logging
import os
import site
import subprocess
import sys
import types
import autotest_lib
AUTOTEST_DIR = autotest_lib.__path__[0]
_SITEPKG_DIR = os.path.join(AUTOTEST_DIR, 'site-packages')
_SYSTEM_PYTHON = '/usr/bin/python2.7'
_setup_done = False
logger = logging.getLogger(__name__)
def monkeypatch():
"""Monkeypatch everything needed to import Autotest.
This should be called before any calls to load(). Only the main
function in scripts should call this function.
"""
with _global_setup():
_monkeypatch_body()
@contextlib.contextmanager
def _global_setup():
"""Context manager for checking and setting global _setup_done variable."""
global _setup_done
assert not _setup_done
try:
yield
except Exception: # pragma: no cover
# We cannot recover from this since we leave the interpreter in
# an unknown state.
logger.exception('Uncaught exception escaped Autotest setup')
sys.exit(1)
else:
_setup_done = True
def _monkeypatch_body():
"""The body of monkeypatch() running within _global_setup() context."""
# Add Autotest's site-packages.
site.addsitedir(_SITEPKG_DIR)
# Dummy out common imports as they may cause problems.
sys.meta_path.insert(0, _CommonRemovingFinder())
# Add chromite's third-party to the import path (chromite does this
# on import).
try:
importlib.import_module('chromite')
except ImportError:
# Moblab does not run build_externals; dependencies like
# chromite are installed system-wide.
logger.info("""\
Could not find chromite; adding system packages and retrying \
(This should only happen on Moblab)""")
for d in _system_site_packages():
site.addsitedir(d)
importlib.import_module('chromite')
# Set up Django environment variables.
importlib.import_module('autotest_lib.frontend.setup_django_environment')
# Make Django app paths absolute.
settings = importlib.import_module('autotest_lib.frontend.settings')
settings.INSTALLED_APPS = (
'autotest_lib.frontend.afe',
'autotest_lib.frontend.tko',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
)
# drone_utility uses this.
common = importlib.import_module('autotest_lib.scheduler.common')
common.autotest_dir = AUTOTEST_DIR
def _system_site_packages():
"""Get list of system site-package directories.
This is needed for Moblab because dependencies are installed
system-wide instead of using build_externals.py.
"""
output = subprocess.check_output([
_SYSTEM_PYTHON, '-c',
'import site; print repr(site.getsitepackages())'])
return ast.literal_eval(output)
class _CommonRemovingFinder(object):
"""Python import finder that neuters Autotest's common.py
The common module is replaced with an empty module everywhere it is
imported. common.py should have only been imported for side
effects, so nothing should actually use the imported module.
See also https://www.python.org/dev/peps/pep-0302/
"""
def find_module(self, fullname, path=None):
"""Find module."""
del path # unused
if not self._is_autotest_common(fullname):
return None
logger.debug('Dummying out %s import', fullname)
return self
def _is_autotest_common(self, fullname):
return (fullname.partition('.')[0] == 'autotest_lib'
and fullname.rpartition('.')[-1] == 'common')
def load_module(self, fullname):
"""Load module."""
if fullname in sys.modules: # pragma: no cover
return sys.modules[fullname]
mod = imp.new_module(fullname)
mod.__file__ = '<removed>'
mod.__loader__ = self
mod.__package__ = fullname.rpartition('.')[0]
sys.modules[fullname] = mod
return mod
def load(name):
"""Import module from autotest.
This enforces that monkeypatch() is called first.
@param name: name of module as string, e.g., 'frontend.afe.models'
"""
return _load('autotest_lib.%s' % name)
def chromite_load(name):
"""Import module from chromite.lib.
This enforces that monkeypatch() is called first.
@param name: name of module as string, e.g., 'metrics'
"""
return _load('chromite.lib.%s' % name)
def deps_load(name):
"""Import module from dependencies, e.g. site-package.
This enforces that monkeypatch() is called first.
@param name: name of module as string, e.g., 'metrics'
"""
assert not name.startswith('autotest_lib')
assert not name.startswith('chromite.lib')
return _load(name)
def _load(name):
"""Import a module.
This enforces that monkeypatch() is called first.
@param name: name of module as string
"""
if not _setup_done:
raise ImportError('cannot load chromite modules before monkeypatching')
return importlib.import_module(name)
def deferred_load(name):
"""Eventually import module from autotest.
This function returns a dummy module that will load the given autotest
module upon its first use (if monkeypatch() has is called first; else
its use will fail).
@param name: name of module as string, e.g., 'frontend.afe.models'
"""
return _DeferredModule('autotest_lib.%s' % name)
def deferred_chromite_load(name):
"""Eventually import module from chromite.lib.
This function returns a dummy module that will load the given chromite
module upon its first use (if monkeypatch() has is called first; else
its use will fail).
@param name: name of module as string, e.g., 'metrics'
"""
return _DeferredModule('chromite.lib.%s' % name)
_UNLOADED_MODULE = object()
class _DeferredModule(types.ModuleType):
"""Module that is loaded upon first usage."""
def __init__(self, name):
super(_DeferredModule, self).__init__(name)
self._name = name
self._module = _UNLOADED_MODULE
def __getattribute__(self, name):
module = object.__getattribute__(self, "_module")
if module is _UNLOADED_MODULE:
module_name = object.__getattribute__(self, "_name")
module = _load(module_name)
self._module = module
return getattr(module, name)