1# Copyright 2017 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Kludges to support legacy Autotest code. 6 7Autotest imports should be done by calling monkeypatch() first and then 8calling load(). monkeypatch() should only be called once from a 9script's main function. 10 11chromite imports should be done with chromite_load(), and any third 12party packages should be imported with deps_load(). The reason for this 13is to present a clear API for these unsafe imports, making it easier to 14identify which imports are currently unsafe. Eventually, everything 15should be moved to virtualenv, but that will not be in the near future. 16 17As an alternative to calling monkeypatch and load in a small scope wherever 18an external module is needed, chromite and autotest imports may also be done at 19the top level of a module using deferred_load() and deferred_chromite_load(). 20""" 21 22from __future__ import absolute_import 23from __future__ import division 24from __future__ import print_function 25 26import ast 27import contextlib 28import imp 29import importlib 30import logging 31import os 32import site 33import subprocess 34import sys 35import types 36 37import autotest_lib 38 39AUTOTEST_DIR = autotest_lib.__path__[0] 40_SITEPKG_DIR = os.path.join(AUTOTEST_DIR, 'site-packages') 41_SYSTEM_PYTHON = '/usr/bin/python2.7' 42 43_setup_done = False 44 45logger = logging.getLogger(__name__) 46 47 48def monkeypatch(): 49 """Monkeypatch everything needed to import Autotest. 50 51 This should be called before any calls to load(). Only the main 52 function in scripts should call this function. 53 """ 54 with _global_setup(): 55 _monkeypatch_body() 56 57 58@contextlib.contextmanager 59def _global_setup(): 60 """Context manager for checking and setting global _setup_done variable.""" 61 global _setup_done 62 assert not _setup_done 63 try: 64 yield 65 except Exception: # pragma: no cover 66 # We cannot recover from this since we leave the interpreter in 67 # an unknown state. 68 logger.exception('Uncaught exception escaped Autotest setup') 69 sys.exit(1) 70 else: 71 _setup_done = True 72 73 74def _monkeypatch_body(): 75 """The body of monkeypatch() running within _global_setup() context.""" 76 # Add Autotest's site-packages. 77 site.addsitedir(_SITEPKG_DIR) 78 79 # Dummy out common imports as they may cause problems. 80 sys.meta_path.insert(0, _CommonRemovingFinder()) 81 82 # Add chromite's third-party to the import path (chromite does this 83 # on import). 84 try: 85 importlib.import_module('chromite') 86 except ImportError: 87 # Moblab does not run build_externals; dependencies like 88 # chromite are installed system-wide. 89 logger.info("""\ 90Could not find chromite; adding system packages and retrying \ 91(This should only happen on Moblab)""") 92 for d in _system_site_packages(): 93 site.addsitedir(d) 94 importlib.import_module('chromite') 95 96 # Set up Django environment variables. 97 importlib.import_module('autotest_lib.frontend.setup_django_environment') 98 99 # Make Django app paths absolute. 100 settings = importlib.import_module('autotest_lib.frontend.settings') 101 settings.INSTALLED_APPS = ( 102 'autotest_lib.frontend.afe', 103 'autotest_lib.frontend.tko', 104 'django.contrib.admin', 105 'django.contrib.auth', 106 'django.contrib.contenttypes', 107 'django.contrib.sessions', 108 'django.contrib.sites', 109 ) 110 111 112def _system_site_packages(): 113 """Get list of system site-package directories. 114 115 This is needed for Moblab because dependencies are installed 116 system-wide instead of using build_externals.py. 117 """ 118 output = subprocess.check_output([ 119 _SYSTEM_PYTHON, '-c', 120 'import site; print repr(site.getsitepackages())']) 121 return ast.literal_eval(output) 122 123 124class _CommonRemovingFinder(object): 125 """Python import finder that neuters Autotest's common.py 126 127 The common module is replaced with an empty module everywhere it is 128 imported. common.py should have only been imported for side 129 effects, so nothing should actually use the imported module. 130 131 See also https://www.python.org/dev/peps/pep-0302/ 132 """ 133 134 def find_module(self, fullname, path=None): 135 """Find module.""" 136 del path # unused 137 if not self._is_autotest_common(fullname): 138 return None 139 logger.debug('Dummying out %s import', fullname) 140 return self 141 142 def _is_autotest_common(self, fullname): 143 return (fullname.partition('.')[0] == 'autotest_lib' 144 and fullname.rpartition('.')[-1] == 'common') 145 146 def load_module(self, fullname): 147 """Load module.""" 148 if fullname in sys.modules: # pragma: no cover 149 return sys.modules[fullname] 150 mod = imp.new_module(fullname) 151 mod.__file__ = '<removed>' 152 mod.__loader__ = self 153 mod.__package__ = fullname.rpartition('.')[0] 154 sys.modules[fullname] = mod 155 return mod 156 157 158def load(name): 159 """Import module from autotest. 160 161 This enforces that monkeypatch() is called first. 162 163 @param name: name of module as string, e.g., 'frontend.afe.models' 164 """ 165 return _load('autotest_lib.%s' % name) 166 167 168def chromite_load(name): 169 """Import module from chromite.lib. 170 171 This enforces that monkeypatch() is called first. 172 173 @param name: name of module as string, e.g., 'metrics' 174 """ 175 return _load('chromite.lib.%s' % name) 176 177 178def deps_load(name): 179 """Import module from dependencies, e.g. site-package. 180 181 This enforces that monkeypatch() is called first. 182 183 @param name: name of module as string, e.g., 'metrics' 184 """ 185 assert not name.startswith('autotest_lib') 186 assert not name.startswith('chromite.lib') 187 return _load(name) 188 189 190def _load(name): 191 """Import a module. 192 193 This enforces that monkeypatch() is called first. 194 195 @param name: name of module as string 196 """ 197 if not _setup_done: 198 raise ImportError('cannot load chromite modules before monkeypatching') 199 return importlib.import_module(name) 200 201 202def deferred_load(name): 203 """Eventually import module from autotest. 204 205 This function returns a dummy module that will load the given autotest 206 module upon its first use (if monkeypatch() has is called first; else 207 its use will fail). 208 209 @param name: name of module as string, e.g., 'frontend.afe.models' 210 """ 211 return _DeferredModule('autotest_lib.%s' % name) 212 213 214def deferred_chromite_load(name): 215 """Eventually import module from chromite.lib. 216 217 This function returns a dummy module that will load the given chromite 218 module upon its first use (if monkeypatch() has is called first; else 219 its use will fail). 220 221 @param name: name of module as string, e.g., 'metrics' 222 """ 223 return _DeferredModule('chromite.lib.%s' % name) 224 225 226_UNLOADED_MODULE = object() 227 228 229class _DeferredModule(types.ModuleType): 230 """Module that is loaded upon first usage.""" 231 232 def __init__(self, name): 233 super(_DeferredModule, self).__init__(name) 234 self._name = name 235 self._module = _UNLOADED_MODULE 236 237 def __getattribute__(self, name): 238 module = object.__getattribute__(self, "_module") 239 if module is _UNLOADED_MODULE: 240 module_name = object.__getattribute__(self, "_name") 241 module = _load(module_name) 242 self._module = module 243 244 return getattr(module, name) 245