1# Copyright 2015 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 5from __future__ import absolute_import 6from __future__ import division 7from __future__ import print_function 8 9import logging 10import os 11import tempfile 12 13import common 14from autotest_lib.client.bin import utils as common_utils 15from autotest_lib.client.common_lib import error 16from autotest_lib.client.common_lib.cros import dev_server 17from autotest_lib.client.common_lib.cros import retry 18from autotest_lib.server import utils as server_utils 19from autotest_lib.site_utils.lxc import constants 20import six 21from six.moves import zip 22 23try: 24 from autotest_lib.utils.frozen_chromite.lib import metrics 25except ImportError: 26 metrics = common_utils.metrics_mock 27 28 29def get_container_info(container_path, **filters): 30 """Get a collection of container information in the given container path. 31 32 This method parse the output of lxc-ls to get a list of container 33 information. The lxc-ls command output looks like: 34 NAME STATE IPV4 IPV6 AUTOSTART PID MEMORY RAM SWAP 35 -------------------------------------------------------------------------- 36 base STOPPED - - NO - - - - 37 test_123 RUNNING 10.0.3.27 - NO 8359 6.28MB 6.28MB 0.0MB 38 39 @param container_path: Path to look for containers. 40 @param filters: Key value to filter the containers, e.g., name='base' 41 42 @return: A list of dictionaries that each dictionary has the information of 43 a container. The keys are defined in ATTRIBUTES. 44 """ 45 cmd = 'sudo lxc-ls -P %s -f -F %s' % (os.path.realpath(container_path), 46 ','.join(constants.ATTRIBUTES)) 47 output = common_utils.run(cmd).stdout 48 info_collection = [] 49 50 logging.info('cmd [%s] output:\n%s', cmd, output) 51 52 for line in output.splitlines()[1:]: 53 # Only LXC 1.x has the second line of '-' as a separator. 54 if line.startswith('------'): 55 continue 56 info_collection.append( 57 dict(list(zip(constants.ATTRIBUTES, line.split())))) 58 if filters: 59 filtered_collection = [] 60 for key, value in six.iteritems(filters): 61 for info in info_collection: 62 if key in info and info[key] == value: 63 filtered_collection.append(info) 64 info_collection = filtered_collection 65 return info_collection 66 67 68def download_extract(url, target, extract_dir): 69 """Download the file from given url and save it to the target, then extract. 70 71 @param url: Url to download the file. 72 @param target: Path of the file to save to. 73 @param extract_dir: Directory to extract the content of the file to. 74 """ 75 remote_url = dev_server.DevServer.get_server_url(url) 76 # This can be run in multiple threads, pick a unique tmp_file.name. 77 with tempfile.NamedTemporaryFile(prefix=os.path.basename(target) + '_', 78 delete=False) as tmp_file: 79 if remote_url in dev_server.ImageServerBase.servers(): 80 _download_via_devserver(url, tmp_file.name) 81 else: 82 _download_via_curl(url, tmp_file.name) 83 common_utils.run('sudo mv %s %s' % (tmp_file.name, target)) 84 common_utils.run('sudo tar -xvf %s -C %s' % (target, extract_dir)) 85 86 87# Make sure retries only happen in the non-timeout case. 88@retry.retry((error.CmdError), 89 raiselist=[error.CmdTimeoutError], 90 timeout_min=3*2, 91 delay_sec=10) 92def _download_via_curl(url, target_file_path): 93 # We do not want to retry on CmdTimeoutError but still retry on 94 # CmdError. Hence we can't use curl --timeout=... 95 common_utils.run('sudo curl -s %s -o %s' % (url, target_file_path), 96 stderr_tee=common_utils.TEE_TO_LOGS, timeout=3*60) 97 98 99# Make sure retries only happen in the non-timeout case. 100@retry.retry((error.CmdError), 101 raiselist=[error.CmdTimeoutError], 102 timeout_min=(constants.DEVSERVER_CALL_TIMEOUT * 103 constants.DEVSERVER_CALL_RETRY / 60), 104 delay_sec=constants.DEVSERVER_CALL_DELAY) 105def _download_via_devserver(url, target_file_path): 106 dev_server.ImageServerBase.download_file( 107 url, target_file_path, timeout=constants.DEVSERVER_CALL_TIMEOUT) 108 109 110def _install_package_precheck(packages): 111 """If SSP is not enabled or the test is running in chroot (using test_that), 112 packages installation should be skipped. 113 114 The check does not raise exception so tests started by test_that or running 115 in an Autotest setup with SSP disabled can continue. That assume the running 116 environment, chroot or a machine, has the desired packages installed 117 already. 118 119 @param packages: A list of names of the packages to install. 120 121 @return: True if package installation can continue. False if it should be 122 skipped. 123 124 """ 125 if server_utils.is_inside_chroot(): 126 logging.info('Test is running inside chroot. Install package %s is ' 127 'skipped.', packages) 128 return False 129 130 if not common_utils.is_in_container(): 131 raise error.ContainerError('Package installation is only supported ' 132 'when test is running inside container.') 133 134 return True 135 136 137def _remove_banned_packages(packages, banned_packages): 138 """Filter out packages. 139 140 @param packages: A set of packages names that have been requested. 141 @param items: A list of package names that are not to be installed. 142 143 @return: A sanatized set of packages names to install. 144 """ 145 return {package for package in packages if package not in banned_packages} 146 147 148def _ensure_pip(target_setting): 149 """ Ensure pip is installed, if not install it. 150 151 @param target_setting: target command param specifying the path to where 152 python packages should be installed. 153 """ 154 try: 155 import pip 156 except ImportError: 157 common_utils.run( 158 'wget https://bootstrap.pypa.io/get-pip.py -O /tmp/get-pip.py') 159 common_utils.run('python /tmp/get-pip.py %s' % target_setting) 160 161 162@metrics.SecondsTimerDecorator( 163 '%s/install_packages_duration' % constants.STATS_KEY) 164@retry.retry(error.CmdError, timeout_min=30) 165def install_packages(packages=[], python_packages=[], force_latest=False): 166 """Install the given package inside container. 167 168 !!! WARNING !!! 169 This call may introduce several minutes of delay in test run. The best way 170 to avoid such delay is to update the base container used for the test run. 171 File a bug for infra deputy to update the base container with the new 172 package a test requires. 173 174 @param packages: A list of names of the packages to install. 175 @param python_packages: A list of names of the python packages to install 176 using pip. 177 @param force_latest: True to force to install the latest version of the 178 package. Default to False, which means skip installing 179 the package if it's installed already, even with an old 180 version. 181 182 @raise error.ContainerError: If package is attempted to be installed outside 183 a container. 184 @raise error.CmdError: If the package doesn't exist or failed to install. 185 186 """ 187 if not _install_package_precheck(packages or python_packages): 188 return 189 190 # If force_latest is False, only install packages that are not already 191 # installed. 192 if not force_latest: 193 packages = [p for p in packages 194 if not common_utils.is_package_installed(p)] 195 python_packages = [p for p in python_packages 196 if not common_utils.is_python_package_installed(p)] 197 if not packages and not python_packages: 198 logging.debug( 199 'All packages are installed already, skip reinstall.') 200 return 201 202 # Always run apt-get update before installing any container. The base 203 # container may have outdated cache. 204 common_utils.run('sudo apt-get update') 205 206 # Make sure the lists are not None for iteration. 207 packages = [] if not packages else packages 208 # Remove duplicates. 209 packages = set(packages) 210 211 # Ubuntu distribution of pip is very old, do not use it as it causes 212 # segmentation faults. Some tests request these packages, ensure they 213 # are not installed. 214 packages = _remove_banned_packages(packages, ['python-pip', 'python-dev']) 215 216 if packages: 217 common_utils.run( 218 'sudo DEBIAN_FRONTEND=noninteractive apt-get install %s -y ' 219 '--force-yes' % ' '.join(packages)) 220 logging.debug('Packages are installed: %s.', packages) 221 222 target_setting = '' 223 # For containers running in Moblab, /usr/local/lib/python2.7/dist-packages/ 224 # is a readonly mount from the host. Therefore, new python modules have to 225 # be installed in /usr/lib/python2.7/dist-packages/ 226 # Containers created in Moblab does not have autotest/site-packages folder. 227 if not os.path.exists('/usr/local/autotest/site-packages'): 228 target_setting = '--target="/usr/lib/python2.7/dist-packages/"' 229 # Pip should be installed in the base container, if not install it. 230 if python_packages: 231 _ensure_pip(target_setting) 232 common_utils.run('python -m pip install pip --upgrade') 233 common_utils.run('python -m pip install %s %s' % (target_setting, 234 ' '.join(python_packages))) 235 logging.debug('Python packages are installed: %s.', python_packages) 236