1#
2# Copyright (C) 2012 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17# DEPRECATED
18# Do not use flimflam.py in future development.
19# Extend / migrate to shill_proxy suite of scripts instead.
20
21import logging, time
22
23import dbus
24
25DEFAULT_CELLULAR_TIMEOUT = 60
26
27def make_dbus_boolean(value):
28    value = value.upper()
29    if value in ["ON", "TRUE"]:
30        return dbus.Boolean(1)
31    elif value in ["OFF", "FALSE"]:
32        return dbus.Boolean(0)
33    else:
34        return dbus.Boolean(int(value))
35
36#
37# Convert a DBus value to a printable value; used
38# to print properties returned via DBus
39#
40def convert_dbus_value(value, indent=0):
41    # DEPRECATED
42    spacer = ' ' * indent
43    if value.__class__ == dbus.Byte:
44        return int(value)
45    elif value.__class__ == dbus.Boolean:
46        return bool(value)
47    elif value.__class__ == dbus.Dictionary:
48        valstr = "{"
49        for key in value:
50            valstr += "\n" + spacer + "    " + \
51                key + ": " + str(convert_dbus_value(value[key], indent + 4))
52        valstr += "\n" + spacer + "}"
53        return valstr
54    elif value.__class__ == dbus.Array:
55        valstr = "["
56        for val in value:
57            valstr += "\n" + spacer + "    " + \
58                str(convert_dbus_value(val, indent + 4))
59        valstr += "\n" + spacer + "]"
60        return valstr
61    else:
62        return str(value)
63
64class FlimFlam(object):
65    # DEPRECATED
66
67    SHILL_DBUS_INTERFACE = "org.chromium.flimflam"
68    UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
69    UNKNOWN_OBJECT = 'org.freedesktop.DBus.Error.UnknownObject'
70
71    DEVICE_WIMAX = 'wimax'
72    DEVICE_CELLULAR = 'cellular'
73
74    @staticmethod
75    def _GetContainerName(kind):
76        """Map shill element names to the names of their collections."""
77        # For example, Device - > Devices.
78        # Just pulling this out so we can use a map if we start
79        # caring about "AvailableTechnologies"
80        return kind + "s"
81
82    @staticmethod
83    def WaitForServiceState(service, expected_states, timeout,
84                            ignore_failure=False, property_name="State"):
85        """Wait until service enters a state in expected_states or times out.
86        Args:
87          service: service to watch
88          expected_states: list of exit states
89          timeout: in seconds
90          ignore_failure: should the failure state be ignored?
91          property_name: name of service property
92
93        Returns: (state, seconds waited)
94
95        If the state is "failure" and ignore_failure is False we return
96        immediately without waiting for the timeout.
97        """
98
99        state = None
100        start_time = time.time()
101        timeout = start_time + timeout
102        while time.time() < timeout:
103            properties = service.GetProperties(utf8_strings = True)
104            state = properties.get(property_name, None)
105            if ((state == "failure" and not ignore_failure) or
106                state in expected_states):
107                break
108            time.sleep(.5)
109
110        config_time = time.time() - start_time
111        # str() to remove DBus boxing
112        return (str(state), config_time)
113
114    @staticmethod
115    def DisconnectService(service, wait_timeout=15):
116        try:
117            service.Disconnect()
118        except dbus.exceptions.DBusException, error:
119            if error.get_dbus_name() not in [
120                    FlimFlam.SHILL_DBUS_INTERFACE + ".Error.InProgress",
121                    FlimFlam.SHILL_DBUS_INTERFACE + ".Error.NotConnected", ]:
122                raise error
123        return FlimFlam.WaitForServiceState(service, ['idle'], wait_timeout)
124
125    def __init__(self, bus=None):
126        if not bus:
127            bus = dbus.SystemBus()
128        self.bus = bus
129        shill = bus.get_object(FlimFlam.SHILL_DBUS_INTERFACE, "/")
130        self.manager = dbus.Interface(
131                shill,
132                FlimFlam.SHILL_DBUS_INTERFACE + ".Manager")
133
134    def _FindDevice(self, device_type, timeout):
135        """ Return the first device object that matches a given device type.
136
137            Wait until the device type is avilable or until timeout
138
139            Args:
140                device_type: string format of the type of device.
141                timeout: in seconds
142
143            Returns: Device or None
144        """
145        timeout = time.time() + timeout
146        device_obj = None
147        while time.time() < timeout:
148            device_obj = self.FindElementByPropertySubstring('Device',
149                                                             'Type',
150                                                             device_type)
151            if device_obj:
152                break
153            time.sleep(1)
154        return device_obj
155
156    def FindCellularDevice(self, timeout=DEFAULT_CELLULAR_TIMEOUT):
157        return self._FindDevice(self.DEVICE_CELLULAR, timeout)
158
159    def FindWimaxDevice(self, timeout=30):
160        return self._FindDevice(self.DEVICE_WIMAX, timeout)
161
162    def _FindService(self, device_type, timeout):
163        """Return the first service object that matches the device type.
164
165        Wait until a service is available or until the timeout.
166
167        Args:
168          device_type: string format of the type of device.
169          timeout: in seconds
170
171        Returns: service or None
172        """
173        start_time = time.time()
174        timeout = start_time + timeout
175        service = None
176        while time.time() < timeout:
177            service = self.FindElementByPropertySubstring('Service',
178                                                          'Type', device_type)
179            if service:
180                break
181            time.sleep(.5)
182        return service
183
184    def FindCellularService(self, timeout=DEFAULT_CELLULAR_TIMEOUT):
185        return self._FindService(self.DEVICE_CELLULAR, timeout)
186
187    def FindWimaxService(self, timeout=30):
188        return self._FindService(self.DEVICE_WIMAX, timeout)
189
190    def GetService(self, params):
191        path = self.manager.GetService(params)
192        return self.GetObjectInterface("Service", path)
193
194    def ConnectService(self, assoc_timeout=15, config_timeout=15,
195                       async=False, service=None, service_type='',
196                       retry=False, retries=1, retry_sleep=15,
197                       save_creds=False,
198                       **kwargs):
199        """Connect to a service and wait until connection is up
200        Args:
201          assoc_timeout, config_timeout:  Timeouts in seconds.
202          async:  return immediately.  do not wait for connection.
203          service: DBus service
204          service_type:  If supplied, invoke type-specific code to find service.
205          retry: Retry connection after Connect failure.
206          retries: Number of retries to allow.
207          retry_sleep: Number of seconds to wait before retrying.
208          kwargs:  Additional args for type-specific code
209
210        Returns:
211          (success, dictionary), where dictionary contains stats and diagnostics.
212        """
213        output = {}
214        connected_states = ["ready", "portal", "online"]
215
216        # Retry connections on failure. Need to call GetService again as some
217        # Connect failure states are unrecoverable.
218        connect_success = False
219        while not connect_success:
220            if service_type == "wifi":
221                try:
222                    # Sanity check to make sure the caller hasn't provided
223                    # both a service and a service type. At which point its
224                    # unclear what they actually want to do, so err on the
225                    # side of caution and except out.
226                    if service:
227                        raise Exception('supplied service and service type')
228                    params = {
229                        "Type": service_type,
230                        "Mode": kwargs["mode"],
231                        "SSID": kwargs["ssid"],
232                        "Security": kwargs.get("security", "none"),
233                        "SaveCredentials": save_creds }
234                    # Supply a passphrase only if it is non-empty.
235                    passphrase = kwargs.get("passphrase", "")
236                    if passphrase:
237                        params["Passphrase"] = passphrase
238                    path = self.manager.GetService(params)
239                    service = self.GetObjectInterface("Service", path)
240                except Exception, e:
241                    output["reason"] = "FAIL(GetService): exception %s" % e
242                    return (False, output)
243
244            output["service"] = service
245
246            try:
247                service.Connect()
248                connect_success = True
249            except Exception, e:
250                if not retry or retries == 0:
251                    output["reason"] = "FAIL(Connect): exception %s" % e
252                    return (False, output)
253                else:
254                    logging.info("INFO(Connect): connect failed. Retrying...")
255                    retries -= 1
256
257            if not connect_success:
258                # FlimFlam can be a little funny sometimes. At least for In
259                # Progress errors, even though the service state may be failed,
260                # it is actually still trying to connect. As such, while we're
261                # waiting for retry, keep checking the service state to see if
262                # it actually succeeded in connecting.
263                state = FlimFlam.WaitForServiceState(
264                        service=service,
265                        expected_states=connected_states,
266                        timeout=retry_sleep,
267                        ignore_failure=True)[0]
268
269                if state in connected_states:
270                    return (True, output)
271
272                # While service can be caller provided, it is also set by the
273                # GetService call above. If service was not caller provided we
274                # need to reset it to None so we don't fail the sanity check
275                # above.
276                if service_type != '':
277                    service = None
278
279        if async:
280            return (True, output)
281
282        logging.info("Associating...")
283        (state, assoc_time) = (
284            FlimFlam.WaitForServiceState(service,
285                                         ["configuration"] + connected_states,
286                                         assoc_timeout))
287        output["state"] = state
288        if state == "failure":
289            output["reason"] = "FAIL(assoc)"
290        if assoc_time > assoc_timeout:
291            output["reason"] = "TIMEOUT(assoc)"
292        output["assoc_time"] = assoc_time
293        if "reason" in output:
294            return (False, output)
295
296
297        (state, config_time) = (
298            FlimFlam.WaitForServiceState(service,
299                                         connected_states, config_timeout))
300        output["state"] = state
301        if state == "failure":
302            output["reason"] = "FAIL(config)"
303        if config_time > config_timeout:
304            output["reason"] = "TIMEOUT(config)"
305        output["config_time"] = config_time
306
307        if "reason" in output:
308            return (False, output)
309
310        return (True, output)
311
312    def GetObjectInterface(self, kind, path):
313        return dbus.Interface(
314                self.bus.get_object(FlimFlam.SHILL_DBUS_INTERFACE, path),
315                FlimFlam.SHILL_DBUS_INTERFACE + "." + kind)
316
317    def FindElementByNameSubstring(self, kind, substring):
318        properties = self.manager.GetProperties(utf8_strings = True)
319        for path in properties[FlimFlam._GetContainerName(kind)]:
320            if path.find(substring) >= 0:
321                return self.GetObjectInterface(kind, path)
322        return None
323
324    def FindElementByPropertySubstring(self, kind, prop, substring):
325        properties = self.manager.GetProperties(utf8_strings = True)
326        for path in properties[FlimFlam._GetContainerName(kind)]:
327            obj = self.GetObjectInterface(kind, path)
328            try:
329                obj_properties = obj.GetProperties(utf8_strings = True)
330            except dbus.exceptions.DBusException, error:
331                if (error.get_dbus_name() == self.UNKNOWN_METHOD or
332                    error.get_dbus_name() == self.UNKNOWN_OBJECT):
333                    # object disappeared; ignore and keep looking
334                    continue
335                else:
336                    raise error
337            if (prop in obj_properties and
338                obj_properties[prop].find(substring) >= 0):
339                return obj
340        return None
341
342    def GetObjectList(self, kind, properties=None):
343        if properties is None:
344            properties = self.manager.GetProperties(utf8_strings = True)
345        return [self.GetObjectInterface(kind, path)
346            for path in properties[FlimFlam._GetContainerName(kind)]]
347
348    def GetActiveProfile(self):
349        properties = self.manager.GetProperties(utf8_strings = True)
350        return self.GetObjectInterface("Profile", properties["ActiveProfile"])
351
352    def CreateProfile(self, ident):
353        path = self.manager.CreateProfile(ident)
354        return self.GetObjectInterface("Profile", path)
355
356    def RemoveProfile(self, ident):
357        self.manager.RemoveProfile(ident)
358
359    def PushProfile(self, ident):
360        path = self.manager.PushProfile(ident)
361        return self.GetObjectInterface("Profile", path)
362
363    def PopProfile(self, ident):
364        self.manager.PopProfile(ident)
365
366    def PopAnyProfile(self):
367        self.manager.PopAnyProfile()
368
369    def GetSystemState(self):
370        properties = self.manager.GetProperties(utf8_strings = True)
371        return properties["State"]
372
373    def GetDebugTags(self):
374        return self.manager.GetDebugTags()
375
376    def ListDebugTags(self):
377        return self.manager.ListDebugTags()
378
379    def SetDebugTags(self, taglist):
380        try:
381            self.manager.SetDebugTags(taglist)
382            self.SetDebugLevel(-4)
383        except dbus.exceptions.DBusException, error:
384            if error.get_dbus_name() not in [
385              "org.freedesktop.DBus.Error.UnknownMethod" ]:
386                raise error
387
388    def SetDebugLevel(self, level):
389        self.manager.SetDebugLevel(level)
390
391    def GetServiceOrder(self):
392        return self.manager.GetServiceOrder()
393
394    def SetServiceOrder(self, new_order):
395        old_order = self.GetServiceOrder()
396        self.manager.SetServiceOrder(new_order)
397        return (old_order, new_order)
398
399    def EnableTechnology(self, tech):
400        try:
401            self.manager.EnableTechnology(tech)
402        except dbus.exceptions.DBusException, error:
403            if error.get_dbus_name() not in [
404                    FlimFlam.SHILL_DBUS_INTERFACE + ".Error.AlreadyEnabled",
405                    FlimFlam.SHILL_DBUS_INTERFACE + ".Error.InProgress" ]:
406                raise error
407
408    def DisableTechnology(self, tech):
409        self.manager.DisableTechnology(tech, timeout=60)
410
411    def RequestScan(self, technology):
412        self.manager.RequestScan(technology)
413
414    def GetCountry(self):
415        properties = self.manager.GetProperties(utf8_strings = True)
416        return properties["Country"]
417
418    def SetCountry(self, country):
419        self.manager.SetProperty("Country", country)
420
421    def GetCheckPortalList(self):
422        properties = self.manager.GetProperties(utf8_strings = True)
423        return properties["CheckPortalList"]
424
425    def SetCheckPortalList(self, tech_list):
426        self.manager.SetProperty("CheckPortalList", tech_list)
427
428    def GetPortalURL(self):
429        properties = self.manager.GetProperties(utf8_strings = True)
430        return properties["PortalURL"]
431
432    def SetPortalURL(self, url):
433        self.manager.SetProperty("PortalURL", url)
434
435    def GetArpGateway(self):
436        properties = self.manager.GetProperties()
437        return properties["ArpGateway"]
438
439    def SetArpGateway(self, do_arp_gateway):
440        self.manager.SetProperty("ArpGateway", do_arp_gateway)
441
442
443class DeviceManager(object):
444    # DEPRECATED
445    """Use flimflam to isolate a given interface for testing.
446
447    DeviceManager can be used to turn off network devices that are not
448    under test so that they will not interfere with testing.
449
450    NB: Ethernet devices are special inside Flimflam.  You will need to
451    take care of them via other means (like, for example, the
452    backchannel ethernet code in client autotests)
453
454    Sample usage:
455
456      device_manager = flimflam.DeviceManager()
457      try:
458          device_manager.ShutdownAllExcept('cellular')
459          use routing.getRouteFor()
460             to verify that only the expected device is used
461          do stuff to test cellular connections
462      finally:
463          device_manager.RestoreDevices()
464    """
465
466    @staticmethod
467    def _EnableDevice(device, enable):
468        """Enables/Disables a device in shill."""
469        if enable:
470            device.Enable()
471        else:
472            device.Disable()
473
474    def __init__(self, flim=None):
475        self.flim_ = flim or FlimFlam()
476        self.devices_to_restore_ = []
477
478    def ShutdownAllExcept(self, device_type):
479        """Shutdown all devices except device_type ones."""
480        for device in self.flim_.GetObjectList('Device'):
481            device_properties = device.GetProperties(utf8_strings = True)
482            if (device_properties["Type"] != device_type):
483                logging.info("Powering off %s device %s",
484                             device_properties["Type"],
485                             device.object_path)
486                self.devices_to_restore_.append(device.object_path)
487                DeviceManager._EnableDevice(device, False)
488
489    def RestoreDevices(self):
490        """Restore devices powered down in ShutdownAllExcept."""
491        should_raise = False
492        to_raise = Exception("Nothing to raise")
493        for device_path in self.devices_to_restore_:
494            try:
495                logging.info("Attempting to power on device %s", device_path)
496                device = self.flim_.GetObjectInterface("Device", device_path)
497                DeviceManager._EnableDevice(device, True)
498            except Exception, e:
499                # We want to keep on trying to power things on, so save an
500                # exception and continue
501                should_raise = True
502                to_raise = e
503        if should_raise:
504            raise to_raise
505