Home | History | Annotate | Download | only in targetprep
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tradefed.targetprep;
     18 
     19 import com.android.tradefed.build.IBuildInfo;
     20 import com.android.tradefed.config.Option;
     21 import com.android.tradefed.config.OptionClass;
     22 import com.android.tradefed.device.DeviceNotAvailableException;
     23 import com.android.tradefed.device.ITestDevice;
     24 import com.android.tradefed.log.LogUtil.CLog;
     25 import com.android.tradefed.util.CommandResult;
     26 import com.android.tradefed.util.CommandStatus;
     27 import com.android.tradefed.util.FileUtil;
     28 import com.android.tradefed.util.IRunUtil;
     29 import com.android.tradefed.util.RunUtil;
     30 import com.android.tradefed.util.StreamUtil;
     31 
     32 import org.json.JSONException;
     33 import org.json.JSONObject;
     34 
     35 import java.io.File;
     36 import java.io.InputStream;
     37 import java.io.IOException;
     38 import java.util.Arrays;
     39 import java.util.Collection;
     40 import java.util.NoSuchElementException;
     41 import java.util.TreeSet;
     42 
     43 /**
     44  * Sets up a Python virtualenv on the host and installs packages. To activate it, the working
     45  * directory is changed to the root of the virtualenv.
     46  *
     47  * This's a fork of PythonVirtualenvPreparer and is forked in order to simplify the change
     48  * deployment process and reduce the deployment time, which are critical for VTS services.
     49  * That means changes here will be upstreamed gradually.
     50  */
     51 @OptionClass(alias = "python-venv")
     52 public class VtsPythonVirtualenvPreparer implements ITargetPreparer, ITargetCleaner {
     53 
     54     private static final String PIP = "pip";
     55     private static final String PATH = "PATH";
     56     private static final String OS_NAME = "os.name";
     57     private static final String WINDOWS = "Windows";
     58     private static final String LOCAL_PYPI_PATH_ENV_VAR_NAME = "VTS_PYPI_PATH";
     59     private static final String VENDOR_TEST_CONFIG_FILE_PATH =
     60             "/config/google-tradefed-vts-config.config";
     61     private static final String LOCAL_PYPI_PATH_KEY = "pypi_packages_path";
     62     protected static final String PYTHONPATH = "PYTHONPATH";
     63     protected static final String VIRTUAL_ENV_PATH = "VIRTUALENVPATH";
     64     private static final int BASE_TIMEOUT = 1000 * 60;
     65     private static final String[] DEFAULT_DEP_MODULES = {"enum", "future", "futures",
     66             "google-api-python-client", "httplib2", "oauth2client", "protobuf", "requests"};
     67 
     68     @Option(name = "venv-dir", description = "path of an existing virtualenv to use")
     69     private File mVenvDir = null;
     70 
     71     @Option(name = "requirements-file", description = "pip-formatted requirements file")
     72     private File mRequirementsFile = null;
     73 
     74     @Option(name = "script-file", description = "scripts which need to be executed in advance")
     75     private Collection<String> mScriptFiles = new TreeSet<>();
     76 
     77     @Option(name = "dep-module", description = "modules which need to be installed by pip")
     78     private Collection<String> mDepModules = new TreeSet<>(Arrays.asList(DEFAULT_DEP_MODULES));
     79 
     80     IRunUtil mRunUtil = new RunUtil();
     81     String mPip = PIP;
     82     String mLocalPypiPath = null;
     83 
     84     /**
     85      * {@inheritDoc}
     86      */
     87     @Override
     88     public void setUp(ITestDevice device, IBuildInfo buildInfo)
     89             throws TargetSetupError, BuildError, DeviceNotAvailableException {
     90         startVirtualenv(buildInfo);
     91         setLocalPypiPath();
     92         installDeps(buildInfo);
     93     }
     94 
     95     /**
     96      * {@inheritDoc}
     97      */
     98     @Override
     99     public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
    100             throws DeviceNotAvailableException {
    101         if (mVenvDir != null) {
    102             FileUtil.recursiveDelete(mVenvDir);
    103             CLog.i("Deleted the virtual env's temp working dir, %s.", mVenvDir);
    104             mVenvDir = null;
    105         }
    106     }
    107 
    108     /**
    109      * This method sets mLocalPypiPath, the local PyPI package directory to
    110      * install python packages from in the installDeps method.
    111      *
    112      * @throws IOException
    113      * @throws JSONException
    114      */
    115     protected void setLocalPypiPath() throws RuntimeException {
    116         CLog.i("Loading vendor test config %s", VENDOR_TEST_CONFIG_FILE_PATH);
    117         InputStream config = getClass().getResourceAsStream(VENDOR_TEST_CONFIG_FILE_PATH);
    118 
    119         // First try to load local PyPI directory path from vendor config file
    120         if (config != null) {
    121             try {
    122                 String content = StreamUtil.getStringFromStream(config);
    123                 CLog.i("Loaded vendor test config %s", content);
    124                 if (content != null) {
    125                     JSONObject vendorConfigJson = new JSONObject(content);
    126                     try {
    127                         String pypiPath = vendorConfigJson.getString(LOCAL_PYPI_PATH_KEY);
    128                         if (pypiPath.length() > 0 && dirExistsAndHaveReadAccess(pypiPath)) {
    129                             mLocalPypiPath = pypiPath;
    130                             CLog.i(String.format(
    131                                     "Loaded %s: %s", LOCAL_PYPI_PATH_KEY, mLocalPypiPath));
    132                         }
    133                     } catch (NoSuchElementException e) {
    134                         CLog.i("Vendor test config file does not define %s", LOCAL_PYPI_PATH_KEY);
    135                     }
    136                 }
    137             } catch (IOException e) {
    138                 throw new RuntimeException("Failed to read vendor config json file");
    139             } catch (JSONException e) {
    140                 throw new RuntimeException("Failed to parse vendor config json data");
    141             }
    142         } else {
    143             CLog.i("Vendor test config file %s does not exist", VENDOR_TEST_CONFIG_FILE_PATH);
    144         }
    145 
    146         // If loading path from vendor config file is unsuccessful,
    147         // check local pypi path defined by LOCAL_PYPI_PATH_ENV_VAR_NAME
    148         if (mLocalPypiPath == null) {
    149             CLog.i("Checking whether local pypi packages directory exists");
    150             String pypiPath = System.getenv(LOCAL_PYPI_PATH_ENV_VAR_NAME);
    151             if (pypiPath == null) {
    152                 CLog.i("Local pypi packages directory not specified by env var %s",
    153                         LOCAL_PYPI_PATH_ENV_VAR_NAME);
    154             } else if (dirExistsAndHaveReadAccess(pypiPath)) {
    155                 mLocalPypiPath = pypiPath;
    156                 CLog.i("Set local pypi packages directory to %s", pypiPath);
    157             }
    158         }
    159 
    160         if (mLocalPypiPath == null) {
    161             CLog.i("Failed to set local pypi packages path. Therefore internet connection to "
    162                     + "https://pypi.python.org/simple/ must be available to run VTS tests.");
    163         }
    164     }
    165 
    166     /**
    167      * This method returns whether the given path is a dir that exists and the user has read access.
    168      */
    169     private boolean dirExistsAndHaveReadAccess(String path) {
    170         File pathDir = new File(path);
    171         if (!pathDir.exists() || !pathDir.isDirectory()) {
    172             CLog.i("Directory %s does not exist.", pathDir);
    173             return false;
    174         }
    175 
    176         if (!isOnWindows()) {
    177             CommandResult c = mRunUtil.runTimedCmd(BASE_TIMEOUT * 5, "ls", path);
    178             if (c.getStatus() != CommandStatus.SUCCESS) {
    179                 CLog.i(String.format("Failed to read dir: %s. Result %s. stdout: %s, stderr: %s",
    180                         path, c.getStatus(), c.getStdout(), c.getStderr()));
    181                 return false;
    182             }
    183             return true;
    184         } else {
    185             try {
    186                 String[] pathDirList = pathDir.list();
    187                 if (pathDirList == null) {
    188                     CLog.i("Failed to read dir: %s. Please check access permission.", pathDir);
    189                     return false;
    190                 }
    191             } catch (SecurityException e) {
    192                 CLog.i(String.format(
    193                         "Failed to read dir %s with SecurityException %s", pathDir, e));
    194                 return false;
    195             }
    196             return true;
    197         }
    198     }
    199 
    200     protected void installDeps(IBuildInfo buildInfo) throws TargetSetupError {
    201         boolean hasDependencies = false;
    202         if (!mScriptFiles.isEmpty()) {
    203             for (String scriptFile : mScriptFiles) {
    204                 CLog.i("Attempting to execute a script, %s", scriptFile);
    205                 CommandResult c = mRunUtil.runTimedCmd(BASE_TIMEOUT * 5, scriptFile);
    206                 if (c.getStatus() != CommandStatus.SUCCESS) {
    207                     CLog.e("Executing script %s failed", scriptFile);
    208                     throw new TargetSetupError("Failed to source a script");
    209                 }
    210             }
    211         }
    212         if (mRequirementsFile != null) {
    213             CommandResult c = mRunUtil.runTimedCmd(BASE_TIMEOUT * 5, mPip,
    214                     "install", "-r", mRequirementsFile.getAbsolutePath());
    215             if (c.getStatus() != CommandStatus.SUCCESS) {
    216                 CLog.e("Installing dependencies from %s failed",
    217                         mRequirementsFile.getAbsolutePath());
    218                 throw new TargetSetupError("Failed to install dependencies with pip");
    219             }
    220             hasDependencies = true;
    221         }
    222         if (!mDepModules.isEmpty()) {
    223             for (String dep : mDepModules) {
    224                 CommandResult result = null;
    225                 if (mLocalPypiPath != null) {
    226                     CLog.i("Attempting installation of %s from local directory", dep);
    227                     result = mRunUtil.runTimedCmd(BASE_TIMEOUT * 5, mPip, "install", dep,
    228                             "--no-index", "--find-links=" + mLocalPypiPath);
    229                     CLog.i(String.format("Result %s. stdout: %s, stderr: %s", result.getStatus(),
    230                             result.getStdout(), result.getStderr()));
    231                     if (result.getStatus() != CommandStatus.SUCCESS) {
    232                         CLog.e(String.format("Installing %s from %s failed", dep, mLocalPypiPath));
    233                     }
    234                 }
    235                 if (mLocalPypiPath == null || result.getStatus() != CommandStatus.SUCCESS) {
    236                     CLog.i("Attempting installation of %s from PyPI", dep);
    237                     result = mRunUtil.runTimedCmd(BASE_TIMEOUT * 5, mPip, "install", dep);
    238                     CLog.i(String.format("Result %s. stdout: %s, stderr: %s", result.getStatus(),
    239                             result.getStdout(), result.getStderr()));
    240                     if (result.getStatus() != CommandStatus.SUCCESS) {
    241                         CLog.e("Installing %s from PyPI failed.", dep);
    242                         CLog.i("Attempting to upgrade %s", dep);
    243                         result = mRunUtil.runTimedCmd(
    244                                 BASE_TIMEOUT * 5, mPip, "install", "--upgrade", dep);
    245                         if (result.getStatus() != CommandStatus.SUCCESS) {
    246                             throw new TargetSetupError(String.format(
    247                                     "Failed to install dependencies with pip. "
    248                                             + "Result %s. stdout: %s, stderr: %s",
    249                                     result.getStatus(), result.getStdout(), result.getStderr()));
    250                         } else {
    251                             CLog.i(String.format("Result %s. stdout: %s, stderr: %s",
    252                                     result.getStatus(), result.getStdout(), result.getStderr()));
    253                         }
    254                     }
    255                 }
    256                 hasDependencies = true;
    257             }
    258         }
    259         if (!hasDependencies) {
    260             CLog.i("No dependencies to install");
    261         } else {
    262             // make the install directory of new packages available to other classes that
    263             // receive the build
    264             buildInfo.setFile(PYTHONPATH, new File(mVenvDir,
    265                     "local/lib/python2.7/site-packages"),
    266                     buildInfo.getBuildId());
    267         }
    268     }
    269 
    270     protected void startVirtualenv(IBuildInfo buildInfo) throws TargetSetupError {
    271         if (mVenvDir != null) {
    272             CLog.i("Using existing virtualenv based at %s", mVenvDir.getAbsolutePath());
    273             activate();
    274             return;
    275         }
    276         try {
    277             mVenvDir = buildInfo.getFile(VIRTUAL_ENV_PATH);
    278             if (mVenvDir == null) {
    279                 mVenvDir = FileUtil.createTempDir(buildInfo.getTestTag() + "-virtualenv");
    280             }
    281             String virtualEnvPath = mVenvDir.getAbsolutePath();
    282             mRunUtil.runTimedCmd(BASE_TIMEOUT, "virtualenv", virtualEnvPath);
    283             CLog.i(VIRTUAL_ENV_PATH + " = " + virtualEnvPath + "\n");
    284             buildInfo.setFile(VIRTUAL_ENV_PATH, new File(virtualEnvPath),
    285                               buildInfo.getBuildId());
    286             activate();
    287         } catch (IOException e) {
    288             CLog.e("Failed to create temp directory for virtualenv");
    289             throw new TargetSetupError("Error creating virtualenv", e);
    290         }
    291     }
    292 
    293     protected void addDepModule(String module) {
    294         mDepModules.add(module);
    295     }
    296 
    297     protected void setRequirementsFile(File f) {
    298         mRequirementsFile = f;
    299     }
    300 
    301     /**
    302      * This method returns whether the OS is Windows.
    303      */
    304     private static boolean isOnWindows() {
    305         return System.getProperty(OS_NAME).contains(WINDOWS);
    306     }
    307 
    308     private void activate() {
    309         File binDir = new File(mVenvDir, isOnWindows() ? "Scripts" : "bin");
    310         mRunUtil.setWorkingDir(binDir);
    311         String path = System.getenv(PATH);
    312         mRunUtil.setEnvVariable(PATH, binDir + File.pathSeparator + path);
    313         File pipFile = new File(binDir, PIP);
    314         pipFile.setExecutable(true);
    315         mPip = pipFile.getAbsolutePath();
    316     }
    317 }
    318