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 7import errno 8import logging 9import os 10 11from contextlib import contextmanager 12 13from autotest_lib.client.common_lib import error 14from autotest_lib.client.common_lib import utils 15from autotest_lib.client.cros.networking import shill_proxy 16 17SHILL_START_LOCK_PATH = '/run/lock/shill-start.lock' 18 19class ContextError(Exception): 20 """An error raised by a context managers dealing with shill objects.""" 21 pass 22 23 24class 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 102class 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 191def 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