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