Home | History | Annotate | Download | only in networking
      1 # Copyright (c) 2013 The Chromium OS 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 """A collection of context managers for working with shill objects."""
      6 
      7 import errno
      8 import logging
      9 import os
     10 
     11 from contextlib import contextmanager
     12 
     13 from autotest_lib.client.common_lib import error
     14 from autotest_lib.client.common_lib import utils
     15 from autotest_lib.client.cros.networking import shill_proxy
     16 
     17 SHILL_START_LOCK_PATH = '/run/lock/shill-start.lock'
     18 
     19 class ContextError(Exception):
     20     """An error raised by a context managers dealing with shill objects."""
     21     pass
     22 
     23 
     24 class AllowedTechnologiesContext(object):
     25     """A context manager for allowing only specified technologies in shill.
     26 
     27     Usage:
     28         # Suppose both 'wifi' and 'cellular' technology are originally enabled.
     29         allowed = [shill_proxy.ShillProxy.TECHNOLOGY_CELLULAR]
     30         with AllowedTechnologiesContext(allowed):
     31             # Within this context, only the 'cellular' technology is allowed to
     32             # be enabled. The 'wifi' technology is temporarily prohibited and
     33             # disabled until after the context ends.
     34 
     35     """
     36 
     37     def __init__(self, allowed):
     38         self._allowed = set(allowed)
     39 
     40 
     41     def __enter__(self):
     42         shill = shill_proxy.ShillProxy.get_proxy()
     43 
     44         # The EnabledTechologies property is an array of strings of technology
     45         # identifiers.
     46         enabled = shill.get_dbus_property(
     47                 shill.manager,
     48                 shill_proxy.ShillProxy.MANAGER_PROPERTY_ENABLED_TECHNOLOGIES)
     49         self._originally_enabled = set(enabled)
     50 
     51         # The ProhibitedTechnologies property is a comma-separated string of
     52         # technology identifiers.
     53         prohibited_csv = shill.get_dbus_property(
     54                 shill.manager,
     55                 shill_proxy.ShillProxy.MANAGER_PROPERTY_PROHIBITED_TECHNOLOGIES)
     56         prohibited = prohibited_csv.split(',') if prohibited_csv else []
     57         self._originally_prohibited = set(prohibited)
     58 
     59         prohibited = ((self._originally_prohibited | self._originally_enabled)
     60                       - self._allowed)
     61         prohibited_csv = ','.join(prohibited)
     62 
     63         logging.debug('Allowed technologies = [%s]', ','.join(self._allowed))
     64         logging.debug('Originally enabled technologies = [%s]',
     65                       ','.join(self._originally_enabled))
     66         logging.debug('Originally prohibited technologies = [%s]',
     67                       ','.join(self._originally_prohibited))
     68         logging.debug('To be prohibited technologies = [%s]',
     69                       ','.join(prohibited))
     70 
     71         # Setting the ProhibitedTechnologies property will disable those
     72         # prohibited technologies.
     73         shill.set_dbus_property(
     74                 shill.manager,
     75                 shill_proxy.ShillProxy.MANAGER_PROPERTY_PROHIBITED_TECHNOLOGIES,
     76                 prohibited_csv)
     77 
     78         return self
     79 
     80 
     81     def __exit__(self, exc_type, exc_value, traceback):
     82         shill = shill_proxy.ShillProxy.get_proxy()
     83 
     84         prohibited_csv = ','.join(self._originally_prohibited)
     85         shill.set_dbus_property(
     86                 shill.manager,
     87                 shill_proxy.ShillProxy.MANAGER_PROPERTY_PROHIBITED_TECHNOLOGIES,
     88                 prohibited_csv)
     89 
     90         # Re-enable originally enabled technologies as they may have been
     91         # disabled.
     92         for technology in self._originally_enabled:
     93             shill.manager.EnableTechnology(technology)
     94 
     95         return False
     96 
     97 
     98 class ServiceAutoConnectContext(object):
     99     """A context manager for overriding a service's 'AutoConnect' property.
    100 
    101     As the service object of the same service may change during the lifetime
    102     of the context, this context manager does not take a service object at
    103     construction. Instead, it takes a |get_service| function at construction,
    104     which it invokes to obtain a service object when entering and exiting the
    105     context. It is assumed that |get_service| always returns a service object
    106     that refers to the same service.
    107 
    108     Usage:
    109         def get_service():
    110             # Some function that returns a service object.
    111 
    112         with ServiceAutoConnectContext(get_service, False):
    113             # Within this context, the 'AutoConnect' property of the service
    114             # returned by |get_service| is temporarily set to False if it's
    115             # initially set to True. The property is restored to its initial
    116             # value after the context ends.
    117 
    118     """
    119     def __init__(self, get_service, autoconnect):
    120         self._get_service = get_service
    121         self._autoconnect = autoconnect
    122         self._initial_autoconnect = None
    123 
    124 
    125     def __enter__(self):
    126         service = self._get_service()
    127         if service is None:
    128             raise ContextError('Could not obtain a service object.')
    129 
    130         # Always set the AutoConnect property even if the requested value
    131         # is the same so that shill will retain the AutoConnect property, else
    132         # shill may override it.
    133         service_properties = service.GetProperties()
    134         self._initial_autoconnect = shill_proxy.ShillProxy.dbus2primitive(
    135             service_properties[
    136                 shill_proxy.ShillProxy.SERVICE_PROPERTY_AUTOCONNECT])
    137         logging.info('ServiceAutoConnectContext: change autoconnect to %s',
    138                      self._autoconnect)
    139         service.SetProperty(
    140             shill_proxy.ShillProxy.SERVICE_PROPERTY_AUTOCONNECT,
    141             self._autoconnect)
    142 
    143         # Make sure the cellular service gets persisted by taking it out of
    144         # the ephemeral profile.
    145         if not service_properties[
    146                 shill_proxy.ShillProxy.SERVICE_PROPERTY_PROFILE]:
    147             shill = shill_proxy.ShillProxy.get_proxy()
    148             manager_properties = shill.manager.GetProperties(utf8_strings=True)
    149             active_profile = manager_properties[
    150                     shill_proxy.ShillProxy.MANAGER_PROPERTY_ACTIVE_PROFILE]
    151             logging.info('ServiceAutoConnectContext: change cellular service '
    152                          'profile to %s', active_profile)
    153             service.SetProperty(
    154                     shill_proxy.ShillProxy.SERVICE_PROPERTY_PROFILE,
    155                     active_profile)
    156 
    157         return self
    158 
    159 
    160     def __exit__(self, exc_type, exc_value, traceback):
    161         if self._initial_autoconnect != self._autoconnect:
    162             service = self._get_service()
    163             if service is None:
    164                 raise ContextError('Could not obtain a service object.')
    165 
    166             logging.info('ServiceAutoConnectContext: restore autoconnect to %s',
    167                          self._initial_autoconnect)
    168             service.SetProperty(
    169                 shill_proxy.ShillProxy.SERVICE_PROPERTY_AUTOCONNECT,
    170                 self._initial_autoconnect)
    171         return False
    172 
    173 
    174     @property
    175     def autoconnect(self):
    176         """AutoConnect property value within this context."""
    177         return self._autoconnect
    178 
    179 
    180     @property
    181     def initial_autoconnect(self):
    182         """Initial AutoConnect property value when entering this context."""
    183         return self._initial_autoconnect
    184 
    185 
    186 @contextmanager
    187 def stopped_shill():
    188     """A context for executing code which requires shill to be stopped.
    189 
    190     This context stops shill on entry to the context, and starts shill
    191     before exit from the context. This context further guarantees that
    192     shill will be not restarted by recover_duts, while this context is
    193     active.
    194 
    195     Note that the no-restart guarantee applies only if the user of
    196     this context completes with a 'reasonable' amount of time. In
    197     particular: if networking is unavailable for 15 minutes or more,
    198     recover_duts will reboot the DUT.
    199 
    200     """
    201     def get_lock_holder(lock_path):
    202         lock_holder = os.readlink(lock_path)
    203         try:
    204             os.stat(lock_holder)
    205             return lock_holder  # stat() success -> valid link -> locker alive
    206         except OSError as e:
    207             if e.errno == errno.ENOENT:  # dangling link -> locker is gone
    208                 return None
    209             else:
    210                 raise
    211 
    212     our_proc_dir = '/proc/%d/' % os.getpid()
    213     try:
    214         os.symlink(our_proc_dir, SHILL_START_LOCK_PATH)
    215     except OSError as e:
    216         if e.errno != errno.EEXIST:
    217             raise
    218         lock_holder = get_lock_holder(SHILL_START_LOCK_PATH)
    219         if lock_holder is not None:
    220             raise error.TestError('Shill start lock held by %s' % lock_holder)
    221         os.remove(SHILL_START_LOCK_PATH)
    222         os.symlink(our_proc_dir, SHILL_START_LOCK_PATH)
    223 
    224     utils.stop_service('shill')
    225     yield
    226     utils.start_service('shill')
    227     os.remove(SHILL_START_LOCK_PATH)
    228