aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilipp Maier <pmaier@sysmocom.de>2021-09-22 16:53:22 +0200
committerPhilipp Maier <pmaier@sysmocom.de>2021-10-13 16:42:52 +0200
commitc25fa06287c2bfc235e0108f4699ff93c3346f28 (patch)
treebb97bcf602878a46db4fc5fb9780f39bdec0804d
parent7db4a135abe59db4d3d4a5bca5f30a4e90568d8b (diff)
pySim-shell: add bulk provisioning supportpmaier/cardhandler
There are scenarios where multiple cards need to get the same change. Lets add a new command that takes a script as parameter and executes the secript in a loop on multiple cards while prompting the user to change the card before starting the next cycle. Change-Id: I9e4926675c5a497a22fc6a4fefdd388fe18a2b2d Related: SYS#5617
-rwxr-xr-xpySim-shell.py187
1 files changed, 183 insertions, 4 deletions
diff --git a/pySim-shell.py b/pySim-shell.py
index de532cf..2bd3519 100755
--- a/pySim-shell.py
+++ b/pySim-shell.py
@@ -30,6 +30,7 @@ import argparse
import os
import sys
from pathlib import Path
+from io import StringIO
from pySim.ts_51_011 import EF, DF, EF_SST_map
from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map
@@ -41,7 +42,7 @@ from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args
from pySim.cards import card_detect, SimCard
from pySim.utils import h2b, swap_nibbles, rpad, b2h, h2s, JsonEncoder, bertlv_parse_one
from pySim.utils import dec_st, sanitize_pin_adm, tabulate_str_list, is_hex, boxed_heading_str
-from pySim.card_handler import CardHandler
+from pySim.card_handler import CardHandler, CardHandlerAuto
from pySim.filesystem import CardMF, RuntimeState, CardDF, CardADF
from pySim.ts_51_011 import CardProfileSIM, DF_TELECOM, DF_GSM
@@ -92,7 +93,7 @@ def init_card(sl):
class PysimApp(cmd2.Cmd):
CUSTOM_CATEGORY = 'pySim Commands'
- def __init__(self, card, rs, script = None):
+ def __init__(self, card, rs, sl, ch, script = None):
super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
use_ipython=True, auto_load_commands=False, startup_script=script)
self.intro = style('Welcome to pySim-shell!', fg=fg.red)
@@ -109,6 +110,8 @@ class PysimApp(cmd2.Cmd):
self.json_pretty_print = True
self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output'))
self.apdu_trace = False
+ self.sl = sl
+ self.ch = ch
self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
onchange_cb=self._onchange_apdu_trace))
@@ -120,6 +123,8 @@ class PysimApp(cmd2.Cmd):
and commands to enable card operations.
"""
+ rc = False
+
# Unequip everything from pySim-shell that would not work in unequipped state
if self.rs:
self.rs.unregister_cmds(self)
@@ -140,10 +145,12 @@ class PysimApp(cmd2.Cmd):
self.register_command_set(PySimCommands())
self.iccid, sw = self.card.read_iccid()
rs.select('MF', self)
+ rc = True
else:
self.poutput("pySim-shell not equipped!")
self.update_prompt()
+ return rc
def poutput_json(self, data, force_no_pretty = False):
"""like cmd2.poutput() but for a JSON serializable dict."""
@@ -191,6 +198,170 @@ class PysimApp(cmd2.Cmd):
rs, card = init_card(sl);
self.equip(card, rs)
+ class InterceptStderr(list):
+ def __init__(self):
+ self._stderr_backup = sys.stderr
+ def __enter__(self):
+ self._stringio_stderr = StringIO()
+ sys.stderr = self._stringio_stderr
+ return self
+ def __exit__(self, *args):
+ self.stderr = self._stringio_stderr.getvalue().strip()
+ del self._stringio_stderr
+ sys.stderr = self._stderr_backup
+
+ def _show_failure_sign(self):
+ print(" +-------------+")
+ print(" + ## ## +")
+ print(" + ## ## +")
+ print(" + ### +")
+ print(" + ## ## +")
+ print(" + ## ## +")
+ print(" +-------------+")
+ print("")
+
+ def _show_success_sign(self):
+ print(" +-------------+")
+ print(" + ## +")
+ print(" + ## +")
+ print(" + # ## +")
+ print(" + ## # +")
+ print(" + ## +")
+ print(" +-------------+")
+ print("")
+
+ def _process_card(self, first, script_path):
+
+ # Early phase of card initialzation (this part may fail with an exception)
+ try:
+ rs, card = init_card(self.sl)
+ rc = self.equip(card, rs)
+ except:
+ self.poutput("")
+ self.poutput("Card initialization failed with an exception:")
+ self.poutput("---------------------8<---------------------")
+ traceback.print_exc()
+ self.poutput("---------------------8<---------------------")
+ self.poutput("")
+ return -1
+
+ # Actual card processing step. This part should never fail with an exception since the cmd2
+ # do_run_script method will catch any exception that might occur during script execution.
+ if rc:
+ self.poutput("")
+ self.poutput("Transcript stdout:")
+ self.poutput("---------------------8<---------------------")
+ with self.InterceptStderr() as logged:
+ self.do_run_script(script_path)
+ self.poutput("---------------------8<---------------------")
+
+ self.poutput("")
+ self.poutput("Transcript stderr:")
+ if logged.stderr:
+ self.poutput("---------------------8<---------------------")
+ self.poutput(logged.stderr)
+ self.poutput("---------------------8<---------------------")
+ else:
+ self.poutput("(none)")
+
+ # Check for exceptions
+ self.poutput("")
+ if "EXCEPTION of type" not in logged.stderr:
+ return 0
+
+ return -1
+
+ bulk_script_parser = argparse.ArgumentParser()
+ bulk_script_parser.add_argument('script_path', help="path to the script file")
+ bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
+ action='store_true')
+ bulk_script_parser.add_argument('--tries', type=int, default=2,
+ help='how many tries before trying the next card')
+ bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
+ help='commandline to execute when card handling has stopped')
+ bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
+ help='commandline to execute before actually talking to the card')
+
+ @cmd2.with_argparser(bulk_script_parser)
+ @cmd2.with_category(CUSTOM_CATEGORY)
+ def do_bulk_script(self, opts):
+ """Run script on multiple cards (bulk provisioning)"""
+
+ # Make sure that the script file exists and that it is readable.
+ if not os.access(opts.script_path, os.R_OK):
+ self.poutput("Invalid script file!")
+ return
+
+ success_count = 0
+ fail_count = 0
+
+ first = True
+ while 1:
+ # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
+ # The ratinale is: There may be a problem with the device, we do want to prevent that
+ # all remaining cards are fired to the error bin. This is only relevant for situations
+ # with large stacks, probably we do not need this feature right now.
+
+ try:
+ # In case of failure, try multiple times.
+ for i in range(opts.tries):
+ # fetch card into reader bay
+ ch.get(first)
+
+ # if necessary execute an action before we start processing the card
+ if(opts.pre_card_action):
+ os.system(opts.pre_card_action)
+
+ # process the card
+ rc = self._process_card(first, opts.script_path)
+ if rc == 0:
+ success_count = success_count + 1
+ self._show_success_sign()
+ print("Statistics: success :%i, failure: %i" % (success_count, fail_count))
+ break
+ else:
+ fail_count = fail_count + 1
+ self._show_failure_sign()
+ print("Statistics: success :%i, failure: %i" % (success_count, fail_count))
+
+
+ # Depending on success or failure, the card goes either in the "error" bin or in the
+ # "done" bin.
+ if rc < 0:
+ ch.error()
+ else:
+ ch.done()
+
+ # In most cases it is possible to proceed with the next card, but the
+ # user may decide to halt immediately when an error occurs
+ if opts.halt_on_error and rc < 0:
+ return
+
+ except (KeyboardInterrupt):
+ self.poutput("")
+ self.poutput("Terminated by user!")
+ return;
+ except (SystemExit):
+ # When all cards are processed the card handler device will throw a SystemExit
+ # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
+ # The user has the option to execute some action to make aware that the card handler
+ # needs service.
+ if(opts.on_stop_action):
+ os.system(opts.on_stop_action)
+ return
+ except:
+ self.poutput("")
+ self.poutput("Card handling failed with an exception:")
+ self.poutput("---------------------8<---------------------")
+ traceback.print_exc()
+ self.poutput("---------------------8<---------------------")
+ self.poutput("")
+ fail_count = fail_count + 1
+ self._show_failure_sign()
+ print("Statistics: success :%i, failure: %i" % (success_count, fail_count))
+
+ first = False
+
echo_parser = argparse.ArgumentParser()
echo_parser.add_argument('string', help="string to echo on the shell")
@@ -542,6 +713,8 @@ global_group = option_parser.add_argument_group('General Options')
global_group.add_argument('--script', metavar='PATH', default=None,
help='script with pySim-shell commands to be executed automatically at start-up')
global_group.add_argument('--csv', metavar='FILE', default=None, help='Read card data from CSV file')
+global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
+ help="Use automatic card handling machine")
adm_group = global_group.add_mutually_exclusive_group()
adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
@@ -577,12 +750,18 @@ if __name__ == '__main__':
# Create command layer
scc = SimCardCommands(transport=sl)
+ # Create a card handler (for bulk provisioning)
+ if opts.card_handler_config:
+ ch = CardHandlerAuto(None, opts.card_handler_config)
+ else:
+ ch = CardHandler(sl)
+
# Detect and initialize the card in the reader. This may fail when there
# is no card in the reader or the card is unresponsive. PysimApp is
# able to tolerate and recover from that.
try:
rs, card = init_card(sl)
- app = PysimApp(card, rs, opts.script)
+ app = PysimApp(card, rs, sl, ch, opts.script)
except:
print("Card initialization failed with an exception:")
print("---------------------8<---------------------")
@@ -592,7 +771,7 @@ if __name__ == '__main__':
print(" it should also be noted that some readers may behave strangely when no card")
print(" is inserted.)")
print("")
- app = PysimApp(None, None, opts.script)
+ app = PysimApp(None, None, sl, ch, opts.script)
# If the user supplies an ADM PIN at via commandline args authenticate
# immediately so that the user does not have to use the shell commands