blob: 888f0f147435bc8413996b667d1586d2e0be7d0a [file] [log] [blame]
Allen Li464220f2017-09-12 17:14:22 -07001# 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
Allen Li75f88082017-11-15 15:24:33 -08007Autotest imports should be done by calling monkeypatch() first and then
8calling load(). monkeypatch() should only be called once from a
9script's main function.
Allen Li0459da32017-11-10 12:11:42 -080010
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.
Aviv Keshet7ee12c52019-05-21 14:53:15 -070016
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().
Allen Li464220f2017-09-12 17:14:22 -070020"""
21
22from __future__ import absolute_import
23from __future__ import division
24from __future__ import print_function
25
Allen Li37f67e62018-01-25 14:16:43 -080026import ast
Allen Li75f88082017-11-15 15:24:33 -080027import contextlib
Allen Li464220f2017-09-12 17:14:22 -070028import imp
29import importlib
30import logging
31import os
32import site
Allen Li37f67e62018-01-25 14:16:43 -080033import subprocess
Allen Li464220f2017-09-12 17:14:22 -070034import sys
Aviv Keshet7ee12c52019-05-21 14:53:15 -070035import types
Allen Li464220f2017-09-12 17:14:22 -070036
37import autotest_lib
38
Allen Li45c2fdf2018-02-14 18:47:40 -080039AUTOTEST_DIR = autotest_lib.__path__[0]
40_SITEPKG_DIR = os.path.join(AUTOTEST_DIR, 'site-packages')
Allen Li37f67e62018-01-25 14:16:43 -080041_SYSTEM_PYTHON = '/usr/bin/python2.7'
Allen Li464220f2017-09-12 17:14:22 -070042
43_setup_done = False
44
45logger = logging.getLogger(__name__)
46
47
Allen Li75f88082017-11-15 15:24:33 -080048def monkeypatch():
49 """Monkeypatch everything needed to import Autotest.
Allen Libca5d012017-10-20 13:44:36 -070050
51 This should be called before any calls to load(). Only the main
52 function in scripts should call this function.
Allen Libca5d012017-10-20 13:44:36 -070053 """
Allen Li75f88082017-11-15 15:24:33 -080054 with _global_setup():
55 _monkeypatch_body()
Allen Libca5d012017-10-20 13:44:36 -070056
Allen Lif4b62ae2017-11-09 15:48:05 -080057
Allen Li75f88082017-11-15 15:24:33 -080058@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
Allen Libca5d012017-10-20 13:44:36 -070072
Allen Li75f88082017-11-15 15:24:33 -080073
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).
Allen Li37f67e62018-01-25 14:16:43 -080084 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')
Allen Li75f88082017-11-15 15:24:33 -080095
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')
Allen Libca5d012017-10-20 13:44:36 -0700101 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
Allen Li75f88082017-11-15 15:24:33 -0800111 # drone_utility uses this.
112 common = importlib.import_module('autotest_lib.scheduler.common')
Allen Li45c2fdf2018-02-14 18:47:40 -0800113 common.autotest_dir = AUTOTEST_DIR
Allen Lif4b62ae2017-11-09 15:48:05 -0800114
Allen Libca5d012017-10-20 13:44:36 -0700115
Allen Li37f67e62018-01-25 14:16:43 -0800116def _system_site_packages():
117 """Get list of system site-package directories.
118
119 This is needed for Moblab because dependencies are installed
120 system-wide instead of using build_externals.py.
121 """
122 output = subprocess.check_output([
123 _SYSTEM_PYTHON, '-c',
124 'import site; print repr(site.getsitepackages())'])
125 return ast.literal_eval(output)
126
127
Allen Li464220f2017-09-12 17:14:22 -0700128class _CommonRemovingFinder(object):
129 """Python import finder that neuters Autotest's common.py
130
131 The common module is replaced with an empty module everywhere it is
132 imported. common.py should have only been imported for side
133 effects, so nothing should actually use the imported module.
134
135 See also https://www.python.org/dev/peps/pep-0302/
136 """
137
138 def find_module(self, fullname, path=None):
139 """Find module."""
140 del path # unused
141 if not self._is_autotest_common(fullname):
142 return None
143 logger.debug('Dummying out %s import', fullname)
144 return self
145
146 def _is_autotest_common(self, fullname):
147 return (fullname.partition('.')[0] == 'autotest_lib'
148 and fullname.rpartition('.')[-1] == 'common')
149
150 def load_module(self, fullname):
151 """Load module."""
Allen Libca5d012017-10-20 13:44:36 -0700152 if fullname in sys.modules: # pragma: no cover
Allen Li464220f2017-09-12 17:14:22 -0700153 return sys.modules[fullname]
154 mod = imp.new_module(fullname)
155 mod.__file__ = '<removed>'
156 mod.__loader__ = self
157 mod.__package__ = fullname.rpartition('.')[0]
158 sys.modules[fullname] = mod
159 return mod
160
161
162def load(name):
163 """Import module from autotest.
164
Allen Li2678d362017-10-11 16:59:07 -0700165 This enforces that monkeypatch() is called first.
Allen Licc153502017-09-15 16:08:25 -0700166
167 @param name: name of module as string, e.g., 'frontend.afe.models'
Allen Li464220f2017-09-12 17:14:22 -0700168 """
Allen Li0459da32017-11-10 12:11:42 -0800169 return _load('autotest_lib.%s' % name)
170
171
172def chromite_load(name):
173 """Import module from chromite.lib.
174
175 This enforces that monkeypatch() is called first.
176
177 @param name: name of module as string, e.g., 'metrics'
178 """
179 return _load('chromite.lib.%s' % name)
180
181
182def deps_load(name):
Xixuan Wudb053c82019-01-31 20:07:06 -0800183 """Import module from dependencies, e.g. site-package.
Allen Li0459da32017-11-10 12:11:42 -0800184
185 This enforces that monkeypatch() is called first.
186
187 @param name: name of module as string, e.g., 'metrics'
188 """
189 assert not name.startswith('autotest_lib')
190 assert not name.startswith('chromite.lib')
191 return _load(name)
192
193
194def _load(name):
195 """Import a module.
196
197 This enforces that monkeypatch() is called first.
198
199 @param name: name of module as string
200 """
Allen Li464220f2017-09-12 17:14:22 -0700201 if not _setup_done:
Allen Li0459da32017-11-10 12:11:42 -0800202 raise ImportError('cannot load chromite modules before monkeypatching')
203 return importlib.import_module(name)
Aviv Keshet7ee12c52019-05-21 14:53:15 -0700204
205
206def deferred_load(name):
207 """Eventually import module from autotest.
208
209 This function returns a dummy module that will load the given autotest
210 module upon its first use (if monkeypatch() has is called first; else
211 its use will fail).
212
213 @param name: name of module as string, e.g., 'frontend.afe.models'
214 """
215 return _DeferredModule('autotest_lib.%s' % name)
216
217
218def deferred_chromite_load(name):
219 """Eventually import module from chromite.lib.
220
221 This function returns a dummy module that will load the given chromite
222 module upon its first use (if monkeypatch() has is called first; else
223 its use will fail).
224
225 @param name: name of module as string, e.g., 'metrics'
226 """
227 return _DeferredModule('chromite.lib.%s' % name)
228
229
230_UNLOADED_MODULE = object()
231
232
233class _DeferredModule(types.ModuleType):
234 """Module that is loaded upon first usage."""
235
236 def __init__(self, name):
237 super(_DeferredModule, self).__init__(name)
238 self._name = name
239 self._module = _UNLOADED_MODULE
240
241 def __getattribute__(self, name):
242 module = object.__getattribute__(self, "_module")
243 if module is _UNLOADED_MODULE:
244 module_name = object.__getattribute__(self, "_name")
245 module = _load(module_name)
246 self._module = module
247
248 return getattr(module, name)