• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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