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 5import logging 6import os 7 8from dependency_manager import base_config 9from dependency_manager import exceptions 10 11 12DEFAULT_TYPE = 'default' 13 14 15class DependencyManager(object): 16 def __init__(self, configs, supported_config_types=None): 17 """Manages file dependencies found locally or in cloud_storage. 18 19 Args: 20 configs: A list of instances of BaseConfig or it's subclasses, passed 21 in decreasing order of precedence. 22 supported_config_types: A list of whitelisted config_types. 23 No restrictions if None is specified. 24 25 Raises: 26 ValueError: If |configs| is not a list of instances of BaseConfig or 27 its subclasses. 28 UnsupportedConfigFormatError: If supported_config_types is specified and 29 configs contains a config not in the supported config_types. 30 31 Example: DependencyManager([config1, config2, config3]) 32 No requirements on the type of Config, and any dependencies that have 33 local files for the same platform will first look in those from 34 config1, then those from config2, and finally those from config3. 35 """ 36 if configs is None or type(configs) != list: 37 raise ValueError( 38 'Must supply a list of config files to DependencyManager') 39 # self._lookup_dict is a dictionary with the following format: 40 # { dependency1: {platform1: dependency_info1, 41 # platform2: dependency_info2} 42 # dependency2: {platform1: dependency_info3, 43 # ...} 44 # ...} 45 # 46 # Where the dependencies and platforms are strings, and the 47 # dependency_info's are DependencyInfo instances. 48 self._lookup_dict = {} 49 self.supported_configs = supported_config_types or [] 50 for config in configs: 51 self._UpdateDependencies(config) 52 53 54 def FetchPathWithVersion(self, dependency, platform): 55 """Get a path to an executable for |dependency|, downloading as needed. 56 57 A path to a default executable may be returned if a platform specific 58 version is not specified in the config(s). 59 60 Args: 61 dependency: Name of the desired dependency, as given in the config(s) 62 used in this DependencyManager. 63 platform: Name of the platform the dependency will run on. Often of the 64 form 'os_architecture'. Must match those specified in the config(s) 65 used in this DependencyManager. 66 Returns: 67 <path>, <version> where: 68 <path> is the path to an executable of |dependency| that will run 69 on |platform|, downloading from cloud storage if needed. 70 <version> is the version of the executable at <path> or None. 71 72 Raises: 73 NoPathFoundError: If a local copy of the executable cannot be found and 74 a remote path could not be downloaded from cloud_storage. 75 CredentialsError: If cloud_storage credentials aren't configured. 76 PermissionError: If cloud_storage credentials are configured, but not 77 with an account that has permission to download the remote file. 78 NotFoundError: If the remote file does not exist where expected in 79 cloud_storage. 80 ServerError: If an internal server error is hit while downloading the 81 remote file. 82 CloudStorageError: If another error occured while downloading the remote 83 path. 84 FileNotFoundError: If an attempted download was otherwise unsuccessful. 85 86 """ 87 dependency_info = self._GetDependencyInfo(dependency, platform) 88 if not dependency_info: 89 raise exceptions.NoPathFoundError(dependency, platform) 90 path = dependency_info.GetLocalPath() 91 version = None 92 if not path or not os.path.exists(path): 93 path = dependency_info.GetRemotePath() 94 if not path or not os.path.exists(path): 95 raise exceptions.NoPathFoundError(dependency, platform) 96 version = dependency_info.GetRemotePathVersion() 97 return path, version 98 99 def FetchPath(self, dependency, platform): 100 """Get a path to an executable for |dependency|, downloading as needed. 101 102 A path to a default executable may be returned if a platform specific 103 version is not specified in the config(s). 104 105 Args: 106 dependency: Name of the desired dependency, as given in the config(s) 107 used in this DependencyManager. 108 platform: Name of the platform the dependency will run on. Often of the 109 form 'os_architecture'. Must match those specified in the config(s) 110 used in this DependencyManager. 111 Returns: 112 A path to an executable of |dependency| that will run on |platform|, 113 downloading from cloud storage if needed. 114 115 Raises: 116 NoPathFoundError: If a local copy of the executable cannot be found and 117 a remote path could not be downloaded from cloud_storage. 118 CredentialsError: If cloud_storage credentials aren't configured. 119 PermissionError: If cloud_storage credentials are configured, but not 120 with an account that has permission to download the remote file. 121 NotFoundError: If the remote file does not exist where expected in 122 cloud_storage. 123 ServerError: If an internal server error is hit while downloading the 124 remote file. 125 CloudStorageError: If another error occured while downloading the remote 126 path. 127 FileNotFoundError: If an attempted download was otherwise unsuccessful. 128 129 """ 130 path, _ = self.FetchPathWithVersion(dependency, platform) 131 return path 132 133 def LocalPath(self, dependency, platform): 134 """Get a path to a locally stored executable for |dependency|. 135 136 A path to a default executable may be returned if a platform specific 137 version is not specified in the config(s). 138 Will not download the executable. 139 140 Args: 141 dependency: Name of the desired dependency, as given in the config(s) 142 used in this DependencyManager. 143 platform: Name of the platform the dependency will run on. Often of the 144 form 'os_architecture'. Must match those specified in the config(s) 145 used in this DependencyManager. 146 Returns: 147 A path to an executable for |dependency| that will run on |platform|. 148 149 Raises: 150 NoPathFoundError: If a local copy of the executable cannot be found. 151 """ 152 dependency_info = self._GetDependencyInfo(dependency, platform) 153 if not dependency_info: 154 raise exceptions.NoPathFoundError(dependency, platform) 155 local_path = dependency_info.GetLocalPath() 156 if not local_path or not os.path.exists(local_path): 157 raise exceptions.NoPathFoundError(dependency, platform) 158 return local_path 159 160 def PrefetchPaths(self, platform, dependencies=None, cloud_storage_retries=3): 161 if not dependencies: 162 dependencies = self._lookup_dict.keys() 163 164 skipped_deps = [] 165 found_deps = [] 166 missing_deps = [] 167 for dependency in dependencies: 168 dependency_info = self._GetDependencyInfo(dependency, platform) 169 if not dependency_info: 170 # The dependency is only configured for other platforms. 171 skipped_deps.append(dependency) 172 logging.warning( 173 'Dependency %s not configured for platform %s. Skipping prefetch.', 174 dependency, platform) 175 continue 176 local_path = dependency_info.GetLocalPath() 177 if local_path: 178 found_deps.append(dependency) 179 continue 180 fetched_path = None 181 for _ in range(0, cloud_storage_retries + 1): 182 try: 183 fetched_path = dependency_info.GetRemotePath() 184 except exceptions.CloudStorageError: 185 continue 186 break 187 if fetched_path: 188 found_deps.append(dependency) 189 else: 190 missing_deps.append(dependency) 191 logging.error( 192 'Dependency %s could not be found or fetched from cloud storage for' 193 ' platform %s.', dependency, platform) 194 if missing_deps: 195 raise exceptions.NoPathFoundError(', '.join(missing_deps), platform) 196 return (found_deps, skipped_deps) 197 198 def _UpdateDependencies(self, config): 199 """Add the dependency information stored in |config| to this instance. 200 201 Args: 202 config: An instances of BaseConfig or a subclasses. 203 204 Raises: 205 UnsupportedConfigFormatError: If supported_config_types was specified 206 and config is not in the supported config_types. 207 """ 208 if not isinstance(config, base_config.BaseConfig): 209 raise ValueError('Must use a BaseConfig or subclass instance with the ' 210 'DependencyManager.') 211 if (self.supported_configs and 212 config.GetConfigType() not in self.supported_configs): 213 raise exceptions.UnsupportedConfigFormatError(config.GetConfigType(), 214 config.config_path) 215 for dep_info in config.IterDependencyInfo(): 216 dependency = dep_info.dependency 217 platform = dep_info.platform 218 if dependency not in self._lookup_dict: 219 self._lookup_dict[dependency] = {} 220 if platform not in self._lookup_dict[dependency]: 221 self._lookup_dict[dependency][platform] = dep_info 222 else: 223 self._lookup_dict[dependency][platform].Update(dep_info) 224 225 226 def _GetDependencyInfo(self, dependency, platform): 227 """Get information for |dependency| on |platform|, or a default if needed. 228 229 Args: 230 dependency: Name of the desired dependency, as given in the config(s) 231 used in this DependencyManager. 232 platform: Name of the platform the dependency will run on. Often of the 233 form 'os_architecture'. Must match those specified in the config(s) 234 used in this DependencyManager. 235 236 Returns: The dependency_info for |dependency| on |platform| if it exists. 237 Or the default version of |dependency| if it exists, or None if neither 238 exist. 239 """ 240 if not self._lookup_dict or dependency not in self._lookup_dict: 241 return None 242 dependency_dict = self._lookup_dict[dependency] 243 device_type = platform 244 if not device_type in dependency_dict: 245 device_type = DEFAULT_TYPE 246 return dependency_dict.get(device_type) 247 248