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