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         enabled = shill.get_dbus_property(
     93                 shill.manager,
     94                 shill_proxy.ShillProxy.MANAGER_PROPERTY_ENABLED_TECHNOLOGIES)
     95         to_be_reenabled = self._originally_enabled - set(enabled)
     96         for technology in to_be_reenabled:
     97             shill.manager.EnableTechnology(technology)
     98 
     99         return False
    100 
    101 
    102 class ServiceAutoConnectContext(object):
    103     """A context manager for overriding a service's 'AutoConnect' property.
    104 
    105     As the service object of the same service may change during the lifetime
    106     of the context, this context manager does not take a service object at
    107     construction. Instead, it takes a |get_service| function at construction,
    108     which it invokes to obtain a service object when entering and exiting the
    109     context. It is assumed that |get_service| always returns a service object
    110     that refers to the same service.
    111 
    112     Usage:
    113         def get_service():
    114             # Some function that returns a service object.
    115 
    116         with ServiceAutoConnectContext(get_service, False):
    117             # Within this context, the 'AutoConnect' property of the service
    118             # returned by |get_service| is temporarily set to False if it's
    119             # initially set to True. The property is restored to its initial
    120             # value after the context ends.
    121 
    122     """
    123     def __init__(self, get_service, autoconnect):
    124         self._get_service = get_service
    125         self._autoconnect = autoconnect
    126         self._initial_autoconnect = None
    127 
    128 
    129     def __enter__(self):
    130         service = self._get_service()
    131         if service is None:
    132             raise ContextError('Could not obtain a service object.')
    133 
    134         # Always set the AutoConnect property even if the requested value
    135         # is the same so that shill will retain the AutoConnect property, else
    136         # shill may override it.
    137         service_properties = service.GetProperties()
    138         self._initial_autoconnect = shill_proxy.ShillProxy.dbus2primitive(
    139             service_properties[
    140                 shill_proxy.ShillProxy.SERVICE_PROPERTY_AUTOCONNECT])
    141         logging.info('ServiceAutoConnectContext: change autoconnect to %s',
    142                      self._autoconnect)
    143         service.SetProperty(
    144             shill_proxy.ShillProxy.SERVICE_PROPERTY_AUTOCONNECT,
    145             self._autoconnect)
    146 
    147         # Make sure the cellular service gets persisted by taking it out of
    148         # the ephemeral profile.
    149         if not service_properties[
    150                 shill_proxy.ShillProxy.SERVICE_PROPERTY_PROFILE]:
    151             shill = shill_proxy.ShillProxy.get_proxy()
    152             manager_properties = shill.manager.GetProperties(utf8_strings=True)
    153             active_profile = manager_properties[
    154                     shill_proxy.ShillProxy.MANAGER_PROPERTY_ACTIVE_PROFILE]
    155             logging.info('ServiceAutoConnectContext: change cellular service '
    156                          'profile to %s', active_profile)
    157             service.SetProperty(
    158                     shill_proxy.ShillProxy.SERVICE_PROPERTY_PROFILE,
    159                     active_profile)
    160 
    161         return self
    162 
    163 
    164     def __exit__(self, exc_type, exc_value, traceback):
    165         if self._initial_autoconnect != self._autoconnect:
    166             service = self._get_service()
    167             if service is None:
    168                 raise ContextError('Could not obtain a service object.')
    169 
    170             logging.info('ServiceAutoConnectContext: restore autoconnect to %s',
    171                          self._initial_autoconnect)
    172             service.SetProperty(
    173                 shill_proxy.ShillProxy.SERVICE_PROPERTY_AUTOCONNECT,
    174                 self._initial_autoconnect)
    175         return False
    176 
    177 
    178     @property
    179     def autoconnect(self):
    180         """AutoConnect property value within this context."""
    181         return self._autoconnect
    182 
    183 
    184     @property
    185     def initial_autoconnect(self):
    186         """Initial AutoConnect property value when entering this context."""
    187         return self._initial_autoconnect
    188 
    189 
    190 @contextmanager
    191 def stopped_shill():
    192     """A context for executing code which requires shill to be stopped.
    193 
    194     This context stops shill on entry to the context, and starts shill
    195     before exit from the context. This context further guarantees that
    196     shill will be not restarted by recover_duts, while this context is
    197     active.
    198 
    199     Note that the no-restart guarantee applies only if the user of
    200     this context completes with a 'reasonable' amount of time. In
    201     particular: if networking is unavailable for 15 minutes or more,
    202     recover_duts will reboot the DUT.
    203 
    204     """
    205     def get_lock_holder(lock_path):
    206         lock_holder = os.readlink(lock_path)
    207         try:
    208             os.stat(lock_holder)
    209             return lock_holder  # stat() success -> valid link -> locker alive
    210         except OSError as e:
    211             if e.errno == errno.ENOENT:  # dangling link -> locker is gone
    212                 return None
    213             else:
    214                 raise
    215 
    216     our_proc_dir = '/proc/%d/' % os.getpid()
    217     try:
    218         os.symlink(our_proc_dir, SHILL_START_LOCK_PATH)
    219     except OSError as e:
    220         if e.errno != errno.EEXIST:
    221             raise
    222         lock_holder = get_lock_holder(SHILL_START_LOCK_PATH)
    223         if lock_holder is not None:
    224             raise error.TestError('Shill start lock held by %s' % lock_holder)
    225         os.remove(SHILL_START_LOCK_PATH)
    226         os.symlink(our_proc_dir, SHILL_START_LOCK_PATH)
    227 
    228     utils.stop_service('shill')
    229     yield
    230     utils.start_service('shill')
    231     os.remove(SHILL_START_LOCK_PATH)
    232