Home | History | Annotate | Download | only in dependency_manager
      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 
      5 import logging
      6 import os
      7 
      8 from dependency_manager import base_config
      9 from dependency_manager import exceptions
     10 
     11 
     12 DEFAULT_TYPE = 'default'
     13 
     14 
     15 class 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         continue
    173       local_path = dependency_info.GetLocalPath()
    174       if local_path:
    175         found_deps.append(dependency)
    176         continue
    177       fetched_path = None
    178       cloud_storage_error = None
    179       for _ in range(0, cloud_storage_retries + 1):
    180         try:
    181           fetched_path = dependency_info.GetRemotePath()
    182         except exceptions.CloudStorageError as e:
    183           cloud_storage_error = e
    184         break
    185       if fetched_path:
    186         found_deps.append(dependency)
    187       else:
    188         missing_deps.append(dependency)
    189         logging.error(
    190             'Dependency %s could not be found or fetched from cloud storage for'
    191             ' platform %s. Error: %s', dependency, platform,
    192             cloud_storage_error)
    193     if missing_deps:
    194       raise exceptions.NoPathFoundError(', '.join(missing_deps), platform)
    195     return (found_deps, skipped_deps)
    196 
    197   def _UpdateDependencies(self, config):
    198     """Add the dependency information stored in |config| to this instance.
    199 
    200     Args:
    201         config: An instances of BaseConfig or a subclasses.
    202 
    203     Raises:
    204         UnsupportedConfigFormatError: If supported_config_types was specified
    205         and config is not in the supported config_types.
    206     """
    207     if not isinstance(config, base_config.BaseConfig):
    208       raise ValueError('Must use a BaseConfig or subclass instance with the '
    209                        'DependencyManager.')
    210     if (self.supported_configs and
    211         config.GetConfigType() not in self.supported_configs):
    212       raise exceptions.UnsupportedConfigFormatError(config.GetConfigType(),
    213                                                     config.config_path)
    214     for dep_info in config.IterDependencyInfo():
    215       dependency = dep_info.dependency
    216       platform = dep_info.platform
    217       if dependency not in self._lookup_dict:
    218         self._lookup_dict[dependency] = {}
    219       if platform not in self._lookup_dict[dependency]:
    220         self._lookup_dict[dependency][platform] = dep_info
    221       else:
    222         self._lookup_dict[dependency][platform].Update(dep_info)
    223 
    224 
    225   def _GetDependencyInfo(self, dependency, platform):
    226     """Get information for |dependency| on |platform|, or a default if needed.
    227 
    228     Args:
    229         dependency: Name of the desired dependency, as given in the config(s)
    230             used in this DependencyManager.
    231         platform: Name of the platform the dependency will run on. Often of the
    232             form 'os_architecture'. Must match those specified in the config(s)
    233             used in this DependencyManager.
    234 
    235     Returns: The dependency_info for |dependency| on |platform| if it exists.
    236         Or the default version of |dependency| if it exists, or None if neither
    237         exist.
    238     """
    239     if not self._lookup_dict or dependency not in self._lookup_dict:
    240       return None
    241     dependency_dict = self._lookup_dict[dependency]
    242     device_type = platform
    243     if not device_type in dependency_dict:
    244       device_type = DEFAULT_TYPE
    245     return dependency_dict.get(device_type)
    246 
    247