Christopher Wiley | 2f48d95 | 2013-02-22 09:51:47 -0800 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | # Copyright (c) 2011 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 | # |
| 7 | # Implement a pseudo cdma modem. |
| 8 | # |
| 9 | # This modem mimics a CDMA modem and allows a user to experiment with |
| 10 | # a modem which starts in a factory reset state and is gradually moved |
| 11 | # into a fully activated state. |
| 12 | # |
| 13 | # To test you'll need to have a machine with at least 1 ethernet port |
| 14 | # available to simulate the cellular connection. Assume that it is |
| 15 | # called eth1. |
| 16 | # 1. install flimflam-test on your DUT. |
| 17 | # 2. sudo backchannel setup eth0 pseudo-modem0 |
| 18 | # 3. activation-server & |
| 19 | # 4. sudo fake-cromo |
| 20 | # 5. Use the UI to "Activate Test Network" |
| 21 | # |
| 22 | |
| 23 | |
| 24 | import dbus, glib, gobject, os, subprocess, sys, time |
| 25 | from optparse import OptionParser |
| 26 | |
| 27 | import_path = os.environ.get('SYSROOT', '/usr/local') + '/usr/lib/flimflam/test' |
| 28 | sys.path.append(import_path) |
| 29 | |
| 30 | import flimflam_test |
| 31 | |
| 32 | Modem = flimflam_test.Modem |
| 33 | ModemManager = flimflam_test.ModemManager |
| 34 | |
| 35 | class IpPoolRestrictor: |
| 36 | def __init__(self, interface): |
| 37 | self.interface = interface |
| 38 | self.restricted = False |
| 39 | |
| 40 | def enter(self): |
| 41 | # Reject all non local tcp traffic, but allow DNS lookups |
| 42 | if self.restricted: |
| 43 | return |
| 44 | subprocess.call(['iptables', |
| 45 | '-I', 'INPUT', '1', |
| 46 | '-p', 'tcp', |
| 47 | '-i', self.interface, |
| 48 | '-j', 'REJECT']) |
| 49 | self.restricted = True |
| 50 | |
| 51 | def leave(self, force=None): |
| 52 | if self.restricted or force: |
| 53 | subprocess.call(['iptables', |
| 54 | '-D', 'INPUT', |
| 55 | '-p', 'tcp', |
| 56 | '-i', self.interface, |
| 57 | '-j', 'REJECT']) |
| 58 | self.restricted = False |
| 59 | |
| 60 | restrictor = IpPoolRestrictor('pseudo-modem0') |
| 61 | |
| 62 | |
| 63 | class CarrierState(dbus.service.Object): |
| 64 | def __init__(self, bus, path): |
| 65 | self.payment_made = False |
| 66 | self.restricted = False |
| 67 | dbus.service.Object.__init__(self, bus, path) |
| 68 | |
| 69 | @dbus.service.method('org.chromium.ModemManager.Carrier', |
| 70 | in_signature = '', out_signature = '') |
| 71 | def ProcessPayment(self, *_args, **_kwargs): |
| 72 | print "CarrierState: ProcessPayment" |
| 73 | self.payment_made = True |
| 74 | self.restricted = False |
| 75 | |
| 76 | @dbus.service.method('org.chromium.ModemManager.Carrier', |
| 77 | in_signature = '', out_signature = '') |
| 78 | def ConsumePlan(self, *_args, **_kwargs): |
| 79 | print "CarrierState: ConsumePlan" |
| 80 | self.payment_made = False |
| 81 | self.restricted = True |
| 82 | |
| 83 | |
| 84 | class FactoryResetModem(Modem): |
| 85 | |
| 86 | def __init__(self, mm, name): |
| 87 | Modem.__init__(self, mm, name, |
| 88 | mdn='0000001234', |
| 89 | activation_state=Modem.NOT_ACTIVATED) |
| 90 | |
| 91 | def Activate(self, s, *_args, **_kwargs): |
| 92 | print 'FactoryResetModem: Activate "%s"' % s |
| 93 | self.StartActivation(Modem.PARTIALLY_ACTIVATED, |
| 94 | self.manager.MakePartiallyActivatedModem, |
| 95 | '0015551212') |
| 96 | |
| 97 | # Implement connect as a failure |
| 98 | def Connect(self, _props, *_args, **_kwargs): |
| 99 | print 'FactoryResetModem: Connect' |
| 100 | time.sleep(self.manager.options.connect_delay_ms / 1000.0) |
| 101 | self.state = flimflam_test.STATE_CONNECTING |
| 102 | glib.timeout_add(500, lambda: self.ConnectDone( |
| 103 | self.state, |
| 104 | flimflam_test.STATE_REGISTERED, |
| 105 | flimflam_test.REASON_USER_REQUESTED)) |
| 106 | raise flimflam_test.ConnectError() |
| 107 | |
| 108 | def ActivateImpl(self, _s, _args, _kwargs): |
| 109 | raise NotImplementedError('Unimplemented. Must implement in subclass.') |
| 110 | |
| 111 | |
| 112 | class PartiallyActivatedModem(Modem): |
| 113 | |
| 114 | def __init__(self, mm, name): |
| 115 | Modem.__init__(self, mm, name, |
| 116 | mdn='0015551212', |
| 117 | activation_state=Modem.PARTIALLY_ACTIVATED) |
| 118 | |
| 119 | def Activate(self, s, *_args, **_kwargs): |
| 120 | print 'Partially_ActivatedModem: Activate "%s"' % s |
| 121 | carrier = self.manager.carrier |
| 122 | if self.manager.options.activatable and carrier.payment_made: |
| 123 | self.StartActivation(Modem.ACTIVATED, |
| 124 | self.manager.MakeActivatedModem, |
| 125 | '6175551212') |
| 126 | else: |
| 127 | # TODO(jglasgow): define carrier error codes |
| 128 | carrier_error = 1 |
| 129 | self.StartFailedActivation(carrier_error) |
| 130 | |
| 131 | def ConnectDone(self, old, new, why): |
| 132 | # Implement ConnectDone by manipulating the IP pool restrictor |
| 133 | if new == flimflam_test.STATE_CONNECTED: |
| 134 | restrictor.enter() |
| 135 | else: |
| 136 | restrictor.leave() |
| 137 | Modem.ConnectDone(self, old, new, why) |
| 138 | |
| 139 | def ActivateImpl(self, _s, _args, _kwargs): |
| 140 | raise NotImplementedError('Unimplemented. Must implement in subclass.') |
| 141 | |
| 142 | |
| 143 | class ActivatedModem(Modem): |
| 144 | def __init__(self, mm, name): |
| 145 | Modem.__init__(self, mm, name, |
| 146 | mdn='6175551212', |
| 147 | activation_state=Modem.ACTIVATED) |
| 148 | |
| 149 | def ConnectDone(self, old, new, why): |
| 150 | carrier = self.manager.carrier |
| 151 | # Implement ConnectDone by manipulating the IP pool restrictor |
| 152 | if new == flimflam_test.STATE_CONNECTED and carrier.restricted: |
| 153 | restrictor.enter() |
| 154 | else: |
| 155 | restrictor.leave() |
| 156 | Modem.ConnectDone(self, old, new, why) |
| 157 | |
| 158 | def Connect(self, props, *args, **kwargs): |
| 159 | print 'ActivatedModem: Connect' |
| 160 | kwargs['connect_delay_ms'] = ( |
| 161 | self.manager.options.connect_delay_ms) |
| 162 | Modem.Connect(self, props, *args, **kwargs) |
| 163 | |
| 164 | def ActivateImpl(self, _s, _args, _kwargs): |
| 165 | raise NotImplementedError('Unimplemented. Must implement in subclass.') |
| 166 | |
| 167 | |
| 168 | class BrokenActivatedModem(Modem): |
| 169 | """ BrokenActivatedModem is a modem that although activated always |
| 170 | fails to connect to the network. This simulates errors in which the |
| 171 | carrier refuses to allow connections. |
| 172 | """ |
| 173 | def __init__(self, mm, name): |
| 174 | Modem.__init__(self, mm, name, |
| 175 | mdn='6175551212', |
| 176 | activation_state=Modem.ACTIVATED) |
| 177 | |
| 178 | # Implement connect by always failing |
| 179 | def Connect(self, _props, *_args, **_kwargs): |
| 180 | print 'BrokenActivatedModem: Connect' |
| 181 | time.sleep(self.manager.options.connect_delay_ms / 1000.0) |
| 182 | self.state = flimflam_test.STATE_CONNECTING |
| 183 | glib.timeout_add(500, lambda: self.ConnectDone( |
| 184 | self.state, |
| 185 | flimflam_test.STATE_REGISTERED, |
| 186 | flimflam_test.REASON_USER_REQUESTED)) |
| 187 | raise flimflam_test.ConnectError() |
| 188 | |
| 189 | def ActivateImpl(self, _s, _args, _kwargs): |
| 190 | raise NotImplementedError('Unimplemented. Must implement in subclass.') |
| 191 | |
| 192 | |
| 193 | class Manager(ModemManager): |
| 194 | def __init__(self, bus, options): |
| 195 | ModemManager.__init__(self, bus, flimflam_test.OCMM) |
| 196 | self.modem_number = 1 |
| 197 | self.options = options |
| 198 | self.carrier = CarrierState(bus, |
| 199 | '/org/chromium/ModemManager/Carrier') |
| 200 | |
| 201 | def NewModem(self, classname): |
| 202 | # modem registeres itself with mm, so does not disappear |
| 203 | _ = classname(self, '/TestModem/%d' % self.modem_number) |
| 204 | self.modem_number += 1 |
| 205 | |
| 206 | def MakeFactoryResetModem(self): |
| 207 | self.NewModem(FactoryResetModem) |
| 208 | |
| 209 | def MakePartiallyActivatedModem(self): |
| 210 | self.NewModem(PartiallyActivatedModem) |
| 211 | |
| 212 | def MakeActivatedModem(self): |
| 213 | if not self.options.activatable: |
| 214 | self.NewModem(PartiallyActivatedModem) |
| 215 | elif self.options.connectable: |
| 216 | self.NewModem(ActivatedModem) |
| 217 | else: |
| 218 | self.NewModem(BrokenActivatedModem) |
| 219 | |
| 220 | |
| 221 | def main(): |
| 222 | usage = ''' |
| 223 | Run the fake cromo program to simulate different modem and carrier |
| 224 | behaviors. By default with no arguments the program will simulate a |
| 225 | factory reset modem which needs to be activated and requires the user |
| 226 | to sign up for service. |
| 227 | |
| 228 | To test for error cases in which connections to the carrier network |
| 229 | always fail, use the --unconnectable flag. This is particularly |
| 230 | useful when the initial state is set to activated as in: |
| 231 | |
| 232 | sudo fake-cromo -u -s activated |
| 233 | |
| 234 | This can be used to simulate the conditions of crosbug.com/11355 |
| 235 | |
| 236 | Another simulation that corresponds to many field error conditions is |
| 237 | a device that fails OTASP activation. This can be simulated by using |
| 238 | the -a flag. The device should start in either the factory or partial |
| 239 | state. |
| 240 | |
| 241 | sudo fake-cromo -a |
| 242 | |
| 243 | To test the re-up process, start the modem in the restricted state with |
| 244 | |
| 245 | sudo fake-cromo -s restricted |
| 246 | |
| 247 | One can leave the restricted state by fetching |
| 248 | http://localhost:8080/payment_succeeded.html. This can be done via |
| 249 | the UI by pressing "Buy Plan", or manually. On the next reconnect the |
| 250 | user should be out of the restricted IP pool. |
| 251 | |
| 252 | If the program is interupted while a partially activated modem is in |
| 253 | the connected state it may leave the iptables set up in a way that |
| 254 | causes all tcp traffic to fail, even when using other network |
| 255 | interfaces. Fix this by restarting fake cromo and tell it to leave |
| 256 | the restricted IP pool |
| 257 | |
| 258 | sudo fake-cromo -l |
| 259 | |
| 260 | ''' |
| 261 | parser = OptionParser(usage=usage) |
| 262 | parser.add_option('-u', '--unconnectable', |
| 263 | action='store_false', dest='connectable', |
| 264 | default=True, |
| 265 | help='Do not allow modem to connect') |
| 266 | parser.add_option('-s', '--state', dest='initial_state', |
| 267 | type='choice', |
| 268 | choices=['factory', 'partial', |
| 269 | 'activated', 'restricted'], |
| 270 | default='factory', |
| 271 | help=('Set initial state to factory,' |
| 272 | 'partial, restricted or activated')) |
| 273 | parser.add_option('-a', '--activation_fails', |
| 274 | action='store_false', dest='activatable', |
| 275 | default=True, |
| 276 | help='Do not allow modem to activate') |
| 277 | parser.add_option('-l', '--leave-restricted-pool', |
| 278 | action='store_true', dest='leave_restricted_pool', |
| 279 | default=False, |
| 280 | help='Leave the restricted pool and exit') |
| 281 | parser.add_option('--connect-delay', type='int', |
| 282 | dest='connect_delay_ms', |
| 283 | default=flimflam_test.DEFAULT_CONNECT_DELAY_MS, |
| 284 | help='time in ms required to connnect') |
| 285 | |
| 286 | (options, args) = parser.parse_args() |
| 287 | if len(args) != 0: |
| 288 | parser.error("incorrect number of arguments") |
| 289 | |
| 290 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) |
| 291 | bus = dbus.SystemBus() |
| 292 | mm = Manager(bus, options) |
| 293 | mainloop = gobject.MainLoop() |
| 294 | print "Running test modemmanager." |
| 295 | _ = dbus.service.BusName(flimflam_test.CMM, bus) |
| 296 | |
| 297 | if options.leave_restricted_pool: |
| 298 | restrictor.leave(force=True) |
| 299 | return 0 |
| 300 | |
| 301 | # Choose the type of modem to instantiate... |
| 302 | if options.initial_state == 'factory': |
| 303 | mm.MakeFactoryResetModem() |
| 304 | elif options.initial_state == 'partial': |
| 305 | mm.MakePartiallyActivatedModem() |
| 306 | elif options.initial_state == 'activated': |
| 307 | mm.MakeActivatedModem() |
| 308 | elif options.initial_state == 'restricted': |
| 309 | mm.carrier.ConsumePlan() |
| 310 | mm.MakeActivatedModem() |
| 311 | else: |
| 312 | print 'Invalid initial state: %s' % options.initial_state |
| 313 | return 1 |
| 314 | |
| 315 | try: |
| 316 | mainloop.run() |
| 317 | finally: |
| 318 | restrictor.leave(force=True) |
| 319 | |
| 320 | |
| 321 | if __name__ == '__main__': |
| 322 | main() |