pseudomodem.py revision 0c923a1929b50b8c90ef1c94e056489845d03b95
1#!/usr/bin/env python
2
3# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import dbus
8import dbus.mainloop.glib
9import gobject
10import logging
11import optparse
12import os
13import signal
14import subprocess
15import time
16
17import mm1
18import modem_3gpp
19import modemmanager
20import sim
21
22import common
23from autotest_lib.client.bin import utils
24from autotest_lib.client.common_lib import error
25from autotest_lib.client.cros import virtual_ethernet_pair
26
27
28IFACE_NAME = 'pseudomodem0'
29PEER_IFACE_NAME = IFACE_NAME + 'p'
30IFACE_IP_BASE = '192.168.7'
31DEFAULT_CARRIER = 'banana'
32# TODO(armansito): Remove 'cromo' once it gets deprecated.
33DEFAULT_MANAGERS = ['cromo', 'modemmanager']
34PARENT_SLEEP_TIMEOUT = 2
35
36class TestModemManagerContextError(Exception):
37    """
38    Exception subclass for exceptions that can be raised by
39    TestModemManagerContext for specific errors related to it.
40
41    """
42    pass
43
44class TestModemManagerContext(object):
45    """
46    TestModemManagerContext is an easy way for an autotest to setup a pseudo
47    modem manager environment. A typical test will look like:
48
49    with pseudomodem.TestModemManagerContext(True):
50        ...
51        # Do stuff
52        ...
53
54    Which will stop the real modem managers that are executing and launch the
55    pseudo modem manager in a subprocess.
56
57    Passing False to the TestModemManagerContext constructor will simply render
58    this class a no-op, not affecting any environment configuration.
59
60    """
61    def __init__(self, use_pseudomodem,
62                 family='3GPP',
63                 real_managers=DEFAULT_MANAGERS,
64                 sim=None,
65                 modem=None):
66        """
67        @param use_pseudomodem: Whether or not the context should create a
68                                pseudo modem manager.
69
70        @param family: If the value of |modem| is None, a default Modem of
71                       family 3GPP or CDMA is initialized based on the value of
72                       this parameter, which is a string that contains either
73                       '3GPP' or 'CDMA'. The default value is '3GPP'.
74
75        @param real_managers: Array containing the names of real modem manager
76                              daemons that need to be stopped before starting
77                              the pseudo modem manager,
78                              e.g. ['cromo', 'modemmanager']
79
80        @param sim: An instance of sim.SIM. This is required for 3GPP modems
81                    as it encapsulates information about the carrier.
82
83        @param modem: An instance of a modem.Modem subclass. If none is provided
84                      the default modem is defined by the |family| parameter.
85
86        """
87        self.use_pseudomodem = use_pseudomodem
88        self.real_managers = real_managers
89        if modem:
90            self.pseudo_modem = modem
91        elif family == '3GPP':
92            self.pseudo_modem = modem_3gpp.Modem3gpp()
93        elif family == 'CDMA':
94            # Import modem_cdma here to avoid circular imports.
95            import modem_cdma
96            self.pseudo_modem = modem_cdma.ModemCdma()
97        else:
98            raise TestModemManagerContextError(
99                "Invalid modem family value: " + str(family))
100        if not sim:
101            # Get a handle to the global 'sim' module here, as the name clashes
102            # with a local variable.
103            simmodule = globals()['sim']
104            sim = simmodule.SIM(simmodule.SIM.Carrier('test'),
105                                mm1.MM_MODEM_ACCESS_TECHNOLOGY_GSM)
106        self.sim = sim
107        self.pseudo_modem_manager = None
108
109    def __enter__(self):
110        if self.use_pseudomodem:
111            for modem_manager in self.real_managers:
112                try:
113                    utils.run('/sbin/stop %s' % modem_manager)
114                except error.CmdError:
115                    pass
116            self.pseudo_modem_manager = \
117                PseudoModemManager(modem=self.pseudo_modem, sim=self.sim)
118            self.pseudo_modem_manager.Start()
119        return self
120
121    def __exit__(self, *args):
122        if self.use_pseudomodem:
123            self.pseudo_modem_manager.Stop()
124            self.pseudo_modem_manager = None
125            for modem_manager in self.real_managers:
126                try:
127                    utils.run('/sbin/start %s' % modem_manager)
128                except error.CmdError:
129                    pass
130
131    def GetPseudoModemManager(self):
132        """
133        Returns the underlying PseudoModemManager object.
134
135        @return An instance of PseudoModemManager, or None, if this object
136                was initialized with use_pseudomodem=False.
137
138        """
139        return self.pseudo_modem_manager
140
141class VirtualEthernetInterface(object):
142    """
143    VirtualEthernetInterface sets up a virtual Ethernet pair and runs dnsmasq
144    on one end of the pair. This is used to enable the pseudo modem to expose
145    a network interface and be assigned a dynamic IP address.
146
147    """
148    def __init__(self):
149        self.vif = virtual_ethernet_pair.VirtualEthernetPair(
150                interface_name=IFACE_NAME,
151                peer_interface_name=PEER_IFACE_NAME,
152                interface_ip=None,
153                peer_interface_ip=IFACE_IP_BASE + '.1/24')
154        self.dnsmasq = None
155
156    def BringIfaceUp(self):
157        """
158        Brings up the pseudo modem network interface.
159
160        """
161        utils.run('sudo ifconfig %s up' % IFACE_NAME)
162
163    def BringIfaceDown(self):
164        """
165        Brings down the pseudo modem network interface.
166
167        """
168        utils.run('sudo ifconfig %s down' % IFACE_NAME);
169
170    def StartDHCPServer(self):
171        """
172        Runs dnsmasq on the peer end of the virtual Ethernet pair.
173
174        """
175        lease_file = '/tmp/dnsmasq.%s.leases' % IFACE_NAME
176        os.close(os.open(lease_file, os.O_CREAT | os.O_TRUNC))
177        self.dnsmasq = subprocess.Popen(
178                ['sudo',
179                 '/usr/local/sbin/dnsmasq',
180                 '--pid-file',
181                 '-k',
182                 '--dhcp-leasefile=' + lease_file,
183                 '--dhcp-range=%s.2,%s.254' % (
184                        IFACE_IP_BASE, IFACE_IP_BASE),
185                 '--port=0',
186                 '--interface=' + PEER_IFACE_NAME,
187                 '--bind-interfaces'
188                ])
189
190    def StopDHCPServer(self):
191        """
192        Stops dnsmasq if its currently running on the peer end of the virtual
193        Ethernet pair.
194
195        """
196        if self.dnsmasq:
197            self.dnsmasq.terminate()
198
199    def RestartDHCPServer(self):
200        """
201        Restarts dnsmasq on the peer end of the virtual Ethernet pair.
202
203        """
204        self.StopDHCPServer()
205        self.StartDHCPServer()
206
207    def Setup(self):
208        """
209        Sets up the virtual Ethernet pair and starts dnsmasq.
210
211        """
212        self.vif.setup()
213        self.BringIfaceDown()
214        if not self.vif.is_healthy:
215            raise Exception('Could not initialize virtual ethernet pair')
216        utils.run('sudo route add -host 255.255.255.255 dev ' +
217                   PEER_IFACE_NAME)
218
219        # Make sure 'dnsmasq' can receive DHCP requests.
220        utils.run('sudo iptables -I INPUT -p udp --dport 67 -j ACCEPT')
221        utils.run('sudo iptables -I INPUT -p udp --dport 68 -j ACCEPT')
222
223        self.StartDHCPServer()
224
225    def Teardown(self):
226        """
227        Stops dnsmasq and takes down the virtual Ethernet pair.
228
229        """
230        self.StopDHCPServer()
231        try:
232            utils.run('sudo route del -host 255.255.255.255 dev ' +
233                       PEER_IFACE_NAME)
234        except:
235            pass
236
237        # Remove iptables rules.
238        try:
239            utils.run('sudo iptables -D INPUT -p udp --dport 67 -j ACCEPT')
240            utils.run('sudo iptables -D INPUT -p udp --dport 68 -j ACCEPT')
241        except:
242            pass
243
244        self.vif.teardown()
245
246    def Restart(self):
247        """
248        Restarts the configuration.
249
250        """
251        self.Teardown()
252        self.Setup()
253
254# This is the global VirtualEthernetInterface instance. Classes inside the
255# pseudo modem manager can access the singleton via this variable.
256virtual_ethernet_interface = VirtualEthernetInterface()
257
258class PseudoModemManager(object):
259    """
260    This class is responsible for setting up the virtual ethernet interfaces,
261    initializing the DBus objects and running the main loop.
262
263    This class can be utilized either using Python's with statement, or by
264    calling Start and Stop:
265
266        with PseudoModemManager(modem, sim):
267            ... do stuff ...
268
269    or
270
271        pmm = PseudoModemManager(modem, sim)
272        pmm.Start()
273        ... do stuff ...
274        pmm.Stop()
275
276    The PseudoModemManager constructor takes a variable called "detach". If a
277    value of True is given, the PseudoModemManager will run the main loop in
278    a child process. This is particularly useful when using PseudoModemManager
279    in an autotest:
280
281        with PseudoModemManager(modem, sim, detach=True):
282            ... This will run the modem manager in the background while this
283            block executes. When the code in this block finishes running, the
284            PseudoModemManager will automatically kill the child process.
285
286    If detach=False, then the pseudo modem manager will run the main process
287    until the process exits. PseudoModemManager is created with detach=False
288    when this file is run as an executable.
289
290    """
291
292    def __init__(self,
293                 modem,
294                 sim=None,
295                 detach=True,
296                 logfile=None):
297        # TODO(armansito): The following line just doesn't work.
298        logging.basicConfig(format='%(asctime)-15s %(message)s',
299                            filename=logfile,
300                            level=logging.DEBUG)
301        self.modem = modem
302        self.sim = sim
303        self.detach = detach
304        self.child = None
305        self.started = False
306
307    def __enter__(self):
308        self.Start()
309        return self
310
311    def __exit__(self, *args):
312        self.Stop()
313
314    def Start(self):
315        """
316        Starts the pseudo modem manager based on the initialization parameters.
317        Depending on the configuration, this method may or may not fork. If a
318        subprocess is launched, a DBus mainloop will be initialized by the
319        subprocess. This method sets up the virtual Ethernet interfaces and
320        initializes tha DBus objects and servers.
321
322        """
323        logging.info('Starting pseudo modem manager.')
324        self.started = True
325
326        # TODO(armansito): See crosbug.com/36235
327        global virtual_ethernet_interface
328        virtual_ethernet_interface.Setup()
329        if self.detach:
330            self.child = os.fork()
331            if self.child == 0:
332                self._Run()
333            else:
334                time.sleep(PARENT_SLEEP_TIMEOUT)
335        else:
336            self._Run()
337
338    def Stop(self):
339        """
340        Stops the pseudo modem manager. This means killing the subprocess,
341        if any, stopping the DBus server, and tearing down the virtual Ethernet
342        pair.
343
344        """
345        logging.info('Stopping pseudo modem manager.')
346        if not self.started:
347            logging.info('Not started, cannot stop.')
348            return
349        if self.detach:
350            if self.child != 0:
351                os.kill(self.child, signal.SIGINT)
352                os.waitpid(self.child, 0)
353                self.child = 0
354                self._Cleanup()
355        else:
356            self._Cleanup()
357        self.started = False
358
359    def Restart(self):
360        """
361        Restarts the pseudo modem manager.
362
363        """
364        self.Stop()
365        self.Start()
366
367    def SetModem(self, new_modem):
368        """
369        Sets the modem object that is exposed by the pseudo modem manager and
370        restarts the pseudo modem manager.
371
372        @param new_modem: An instance of modem.Modem to assign.
373
374        """
375        self.modem = new_modem
376        self.Restart()
377        time.sleep(5)
378
379    def SetSIM(self, new_sim):
380        """
381        Sets the SIM object that is exposed by the pseudo modem manager and
382        restarts the pseudo modem manager.
383
384        @param new_sim: An instance of sim.SIM to assign.
385
386        """
387        self.sim = new_sim
388        self.Restart()
389
390    def _Cleanup(self):
391        global virtual_ethernet_interface
392        virtual_ethernet_interface.Teardown()
393
394    def _Run(self):
395        if not self.modem:
396            raise Exception('No modem object has been provided.')
397        dbus_loop = dbus.mainloop.glib.DBusGMainLoop()
398        bus = dbus.SystemBus(private=True, mainloop=dbus_loop)
399        name = dbus.service.BusName(mm1.I_MODEM_MANAGER, bus)
400        self.manager = modemmanager.ModemManager(bus)
401
402        self.modem.SetBus(bus)
403        if self.sim:
404            self.modem.SetSIM(self.sim)
405        self.manager.Add(self.modem)
406
407        self.mainloop = gobject.MainLoop()
408
409        def _SignalHandler(signum, frame):
410            logging.info('Signal handler called with signal %s', signum)
411            self.manager.Remove(self.modem)
412            self.mainloop.quit()
413            if self.detach:
414                os._exit(0)
415
416        signal.signal(signal.SIGINT, _SignalHandler)
417        signal.signal(signal.SIGTERM, _SignalHandler)
418
419        self.mainloop.run()
420
421    def SendTextMessage(self, sender_no, text):
422        """
423        Allows sending a fake text message notification.
424
425        @param sender_no: TODO
426        @param text: TODO
427
428        """
429        # TODO(armansito): Implement
430        raise NotImplementedError()
431
432
433def Start(use_cdma=False, activated=True):
434    """
435    Runs the pseudomodem in script mode. This function is called only by the
436    main function.
437
438    @param use_cdma: If True, the pseudo modem manager will be initialized with
439                     an instance of modem_cdma.ModemCdma, otherwise the default
440                     modem will be used, which is an instance of
441                     modem_3gpp.Modem3gpp.
442    @param activated: If True, the pseudo modem will be initialized as
443                      unactivated and will require service activation.
444
445    """
446    # TODO(armansito): Support "not activated" initialization option for 3GPP
447    #                  carriers.
448    if use_cdma:
449        # Import modem_cdma here to avoid circular imports.
450        import modem_cdma
451        m = modem_cdma.ModemCdma(
452            modem_cdma.ModemCdma.CdmaNetwork(activated=activated))
453        s = None
454    else:
455        m = modem_3gpp.Modem3gpp()
456        s = sim.SIM(sim.SIM.Carrier(), mm1.MM_MODEM_ACCESS_TECHNOLOGY_GSM)
457    with PseudoModemManager(modem=m, sim=s, detach=False, logfile=None):
458        pass
459
460def main():
461    """
462    The main method, executed when this file is executed as a script.
463
464    """
465    usage = """
466
467      Run pseudomodem to simulate a modem using the modemmanager-next
468      DBus interfaces.
469
470      Use --help for info.
471
472    """
473
474    # TODO(armansito): Correctly utilize the below options.
475    # See crbug.com/238430.
476
477    parser = optparse.OptionParser(usage=usage)
478    parser.add_option('-f', '--family', dest='family',
479                      metavar='<family>',
480                      help='<family> := 3GPP|CDMA')
481    parser.add_option('-n', '--not-activated', dest="not_activated",
482                      action="store_true", default=False,
483                      help='Initialize the service as not-activated.')
484
485    (opts, args) = parser.parse_args()
486    if not opts.family:
487        print "A mandatory option '--family' is missing\n"
488        parser.print_help()
489        return
490
491    family = opts.family
492    if family not in [ '3GPP', 'CDMA' ]:
493        print 'Unsupported family: ' + family
494        return
495
496    Start(family == 'CDMA', not opts.not_activated)
497
498
499if __name__ == '__main__':
500    main()
501