Cleaner CLI

Change-Id: I81916ee10450e6f2137a3ff72a693dcf521c8a85
diff --git a/cli/device.py b/cli/device.py
index be5450b..94e24e6 100644
--- a/cli/device.py
+++ b/cli/device.py
@@ -43,12 +43,14 @@
                              metadata=(('get-depth', str(depth)), ))
         return res
 
-    def do_show(self, arg):
-        """Show detailed device information"""
-        print dumps(pb2dict(self.get_device(depth=-1)),
-                    indent=4, sort_keys=True)
+    do_exit = Cmd.do_quit
 
-    def do_flows(self, arg):
+    def do_show(self, line):
+        """Show detailed device information"""
+        self.poutput(dumps(pb2dict(self.get_device(depth=-1)),
+                     indent=4, sort_keys=True))
+
+    def do_flows(self, line):
         """Show flow table for device"""
         device = pb2dict(self.get_device(-1))
         print_flows(
diff --git a/cli/logical_device.py b/cli/logical_device.py
index d60c6eb..286faf0 100644
--- a/cli/logical_device.py
+++ b/cli/logical_device.py
@@ -45,10 +45,12 @@
                                     metadata=(('get-depth', str(depth)), ))
         return res
 
+    do_exit = Cmd.do_quit
+
     def do_show(self, arg):
         """Show detailed logical device information"""
-        print dumps(pb2dict(self.get_logical_device(depth=-1)),
-                    indent=4, sort_keys=True)
+        self.poutput(dumps(pb2dict(self.get_logical_device(depth=-1)),
+                     indent=4, sort_keys=True))
 
     def do_flows(self, arg):
         """Show flow table for logical device"""
diff --git a/cli/main.py b/cli/main.py
index e9e881a..7b4b241 100755
--- a/cli/main.py
+++ b/cli/main.py
@@ -14,10 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+
 import readline
-import sys
 from optparse import make_option
-from time import sleep
+from time import sleep, time
 
 import grpc
 import requests
@@ -27,21 +27,21 @@
 
 from cli.device import DeviceCli
 from cli.logical_device import LogicalDeviceCli
+from cli.table import TablePrinter, print_pb_table
 from voltha.core.flow_decomposer import *
 from voltha.protos import third_party
 from voltha.protos import voltha_pb2
 from voltha.protos.openflow_13_pb2 import FlowTableUpdate
 
 _ = third_party
-from cli.utils import pb2dict
-
+from cli.utils import pb2dict, dict2line
 
 banner = """\
-           _ _   _              _ _
-  __ _____| | |_| |_  __ _   __| (_)
-  \ V / _ \ |  _| ' \/ _` | / _| | |
-   \_/\___/_|\__|_||_\__,_| \__|_|_|
-(to exit type q, exit or quit or hit Ctrl-D)
+         _ _   _              _ _
+__ _____| | |_| |_  __ _   __| (_)
+\ V / _ \ |  _| ' \/ _` | / _| | |
+ \_/\___/_|\__|_||_\__,_| \__|_|_|
+(to exit type quit or hit Ctrl-D)
 """
 
 class VolthaCli(Cmd):
@@ -67,11 +67,30 @@
                                   'is specified',
     ))
 
+    # cleanup of superflous commands from cmd2
+    del Cmd.do_cmdenvironment
+    # del Cmd.do_eof
+    del Cmd.do_exit
+    del Cmd.do_q
+    del Cmd.do_hi
+    del Cmd.do_l
+    del Cmd.do_li
+    del Cmd.do_r
+    del Cmd.do__load
+    del Cmd.do__relative_load
+    Cmd.do_edit = Cmd.do_ed
+
+
     def __init__(self, *args, **kw):
+
         Cmd.__init__(self, *args, **kw)
         self.prompt = '(' + self.colorize(
-            self.colorize(self.prompt, 'red'), 'bold') + ') '
+            self.colorize(self.prompt, 'blue'), 'bold') + ') '
         self.channel = None
+        self.device_ids_cache = None
+        self.device_ids_cache_ts = time()
+        self.logical_device_ids_cache = None
+        self.logical_device_ids_cache_ts = time()
 
     def load_history(self):
         """Load saved command history from local history file"""
@@ -86,81 +105,144 @@
 
     def save_history(self):
         try:
-            with file(self.history_file_name, 'w') as f:
+            with open(self.history_file_name, 'w') as f:
                 f.write('\n'.join(self.history[-self.max_history_lines:]))
-        except IOError, e:
-            print >> sys.stderr, 'Could not save history in {}: {}'.format(
-                self.history_file_name, e.msg)
+        except IOError as e:
+            self.perror('Could not save history in {}: {}'.format(
+                self.history_file_name, e))
         else:
-            print >> sys.stderr, 'History saved as {}'.format(
-                self.history_file_name)
+            self.perror('History saved as {}'.format(
+                self.history_file_name))
+
+    def perror(self, errmsg, statement=None):
+        # Touch it up to make sure error is prefixed and colored
+        Cmd.perror(self, self.colorize('***ERROR: ', 'red') + errmsg,
+                   statement)
 
     def get_channel(self):
         if self.channel is None:
             self.channel = grpc.insecure_channel(self.voltha_grpc)
         return self.channel
 
-    def preloop(self):
-        self.poutput(banner)
+    # ~~~~~~~~~~~~~~~~~ ACTUAL COMMAND IMPLEMENTATIONS ~~~~~~~~~~~~~~~~~~~~~~~~
 
-    def do_reset_history(self, arg):
+    def do_reset_history(self, line):
         """Reset CLI history"""
         while self.history:
             self.history.pop()
 
-    def do_launch(self, arg):
+    def do_launch(self, line):
         """If Voltha is not running yet, launch it"""
-        pass
+        raise NotImplementedError('not implemented yet')
 
-    def do_restart(self, arg):
+    def do_restart(self, line):
         """Launch Voltha, but if it is already running, terminate it first"""
         pass
 
-    def do_devices(self, arg):
-        """List devices registered in Voltha"""
+    def do_adapters(self, line):
+        """List loaded adapter"""
+        stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
+        res = stub.ListAdapters(Empty())
+        omit_fields = {}
+        print_pb_table('Adapters:', res.items, omit_fields, self.poutput)
+
+    def get_devices(self):
         stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
         res = stub.ListDevices(Empty())
-        for device in res.items:
-            print self.colorize('# ====== device {}'.format(device.id), 'blue')
-            print dumps(pb2dict(device), indent=4, sort_keys=True)
+        return res.items
 
-    def do_logical_devices(self, arg):
+    def get_logical_devices(self):
+        stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
+        res = stub.ListLogicalDevices(Empty())
+        return res.items
+
+    def do_devices(self, line):
+        """List devices registered in Voltha"""
+        devices = self.get_devices()
+        omit_fields = {
+            'adapter',
+            'vendor',
+            'model',
+            'hardware_version',
+            'software_version',
+            'firmware_version',
+            'serial_number'
+        }
+        print_pb_table('Devices:', devices, omit_fields, self.poutput)
+
+    def do_logical_devices(self, line):
         """List logical devices in Voltha"""
         stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
         res = stub.ListLogicalDevices(Empty())
-        for logical_device in res.items:
-            print self.colorize('# ====== logical device {}'.format(
-                logical_device.id), 'blue')
-            print dumps(pb2dict(logical_device), indent=4, sort_keys=True)
+        omit_fields = {
+            'desc.mfr_desc',
+            'desc.hw_desc',
+            'desc.sw_desc',
+            'desc.dp_desc',
+            'desc.serial_number',
+            'switch_features.capabilities'
+        }
+        print_pb_table('Logical devices:', res.items, omit_fields,
+                       self.poutput)
 
-    def do_device(self, arg):
+    def do_device(self, line):
         """Enter device level command mode"""
-        device_id = arg or self.default_device_id
+        device_id = line.strip() or self.default_device_id
         if not device_id:
             raise Exception('<device-id> parameter needed')
         sub = DeviceCli(self.get_channel, device_id)
         sub.cmdloop()
 
-    def do_logical_device(self, arg):
+    def do_logical_device(self, line):
         """Enter logical device level command mode"""
-        logical_device_id = arg or self.default_logical_device_id
+        logical_device_id = line.strip() or self.default_logical_device_id
         if not logical_device_id:
             raise Exception('<logical-device-id> parameter needed')
         sub = LogicalDeviceCli(self.get_channel, logical_device_id)
         sub.cmdloop()
 
-    def do_debug(self, arg):
+    def device_ids(self, force_refresh=False):
+        if force_refresh or self.device_ids is None or \
+                (time() - self.device_ids_cache_ts) > 1:
+            self.device_ids_cache = [d.id for d in self.get_devices()]
+            self.device_ids_cache_ts = time()
+        return self.device_ids_cache
+
+    def logical_device_ids(self, force_refresh=False):
+        if force_refresh or self.logical_device_ids is None or \
+                (time() - self.logical_device_ids_cache_ts) > 1:
+            self.logical_device_ids_cache = [d.id for d
+                                             in self.get_logical_devices()]
+            self.logical_device_ids_cache_ts = time()
+        return self.logical_device_ids_cache
+
+    def complete_device(self, text, line, begidx, endidx):
+        if not text:
+            completions = self.device_ids()[:]
+        else:
+            completions = [d for d in self.device_ids() if d.startswith(text)]
+        return completions
+
+    def complete_logical_device(self, text, line, begidx, endidx):
+        if not text:
+            completions = self.logical_device_ids()[:]
+        else:
+            completions = [d for d in self.logical_device_ids()
+                           if d.startswith(text)]
+        return completions
+
+    def do_pdb(self, line):
         """Launch PDB debug prompt in CLI (for CLI development)"""
         from pdb import set_trace
         set_trace()
 
-    def do_health(self, arg):
+    def do_health(self, line):
         """Show connectivity status to Voltha status"""
         stub = voltha_pb2.HealthServiceStub(self.get_channel())
         res = stub.GetHealthStatus(Empty())
-        print dumps(pb2dict(res), indent=4)
+        self.poutput(dumps(pb2dict(res), indent=4))
 
-    def do_test(self, arg):
+    def do_test(self, line):
         """Enter test mode, which makes a bunch on new commands available"""
         sub = TestCli(self.history, self.get_channel)
         sub.cmdloop()
@@ -182,7 +264,7 @@
                     default='00:0c:e2:31:40:00'),
         make_option('-i', '--ip-address', action='store', dest='ip_address'),
     ])
-    def do_preprovision_olt(self, arg, opts):
+    def do_preprovision_olt(self, line, opts):
         """Preprovision a new OLT with given device type"""
         stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
         kw = dict(type=opts.device_type)
@@ -194,16 +276,16 @@
             raise Exception('Either IP address or Mac Address is needed')
         device = voltha_pb2.Device(**kw)
         device = stub.CreateDevice(device)
-        print 'success (device id = {})'.format(device.id)
+        self.poutput('success (device id = {})'.format(device.id))
         self.default_device_id = device.id
 
-    def do_activate_olt(self, arg):
+    def do_activate_olt(self, line):
         """
         Activate an OLT. If the <id> is not provided, it will be on the last
         pre-provisioned OLT.
         """
-        device_id = arg or self.default_device_id
-        print 'activating', device_id
+        device_id = line or self.default_device_id
+        self.poutput('activating {}'.format(device_id))
         stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
         stub.ActivateDevice(voltha_pb2.ID(id=device_id))
 
@@ -214,32 +296,36 @@
                 assert device.parent_id
                 self.default_logical_device_id = device.parent_id
                 break
-            print 'waiting for device to be activated...'
-            sleep(1)
-        print 'success (logical device id = {})'.format(
-            self.default_logical_device_id)
+            self.poutput('waiting for device to be activated...')
+            sleep(.5)
+        self.poutput('success (logical device id = {})'.format(
+            self.default_logical_device_id))
 
-    def do_arrive_onus(self, arg):
+    complete_activate_olt = VolthaCli.complete_device
+
+    def do_arrive_onus(self, line):
         """
         Simulate the arrival of ONUs
         """
-        device_id = arg or self.default_device_id
+        device_id = line or self.default_device_id
         requests.get('http://{}/devices/{}/detect_onus'.format(
             self.voltha_sim_rest, device_id
         ))
 
-    def do_install_eapol_flow(self, arg):
+    complete_arrive_onus = VolthaCli.complete_device
+
+    def do_install_eapol_flow(self, line):
         """
         Install an EAPOL flow on the given logical device. If device is not
         given, it will be applied to logical device of the last pre-provisioned
         OLT device.
         """
-        logical_device_id = arg or self.default_logical_device_id
+        logical_device_id = line or self.default_logical_device_id
         update = FlowTableUpdate(
             id=logical_device_id,
             flow_mod = mk_simple_flow_mod(
                 priority=2000,
-                match_fields=[in_port(101), eth_type(0x888e)],
+                match_fields=[in_port(241), eth_type(0x888e)],
                 actions=[
                     push_vlan(0x8100),
                     set_field(vlan_vid(4096 + 4000)),
@@ -249,18 +335,22 @@
         )
         stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
         res = stub.UpdateLogicalDeviceFlowTable(update)
-        print 'success', res
+        self.poutput('success ({})'.format(res))
 
-    def do_send_simulated_upstream_eapol(self, arg):
+    complete_install_eapol_flow = VolthaCli.complete_logical_device
+
+    def do_send_simulated_upstream_eapol(self, line):
         """
         Send an EAPOL upstream from a simulated OLT
         """
-        device_id = arg or self.default_device_id
+        device_id = line or self.default_device_id
         requests.get('http://{}/devices/{}/test_eapol_in'.format(
             self.voltha_sim_rest, device_id
         ))
 
-    def do_inject_eapol_start(self, arg):
+    complete_send_simulated_upstream_eapol = VolthaCli.complete_device
+
+    def do_inject_eapol_start(self, line):
         """
         Send out an an EAPOL start message into the given Unix interface
         """
@@ -269,6 +359,7 @@
 
 if __name__ == '__main__':
     c = VolthaCli()
+    c.poutput(banner)
     c.load_history()
     c.cmdloop()
     c.save_history()
diff --git a/cli/table.py b/cli/table.py
new file mode 100644
index 0000000..3eb828a
--- /dev/null
+++ b/cli/table.py
@@ -0,0 +1,121 @@
+#
+# Copyright 2016 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import sys
+
+from google.protobuf.message import Message
+from termcolor import colored
+
+_printfn = lambda l: sys.stdout.write(l + '\n')
+
+
+class TablePrinter(object):
+    """Simple tabular data printer utility. For usage, see bottom of file"""
+
+    def __init__(self):
+        self.max_field_lengths = {}
+        self.field_names = {}
+        self.cell_values = {}
+
+    def add_cell(self, row_number, field_key, field_name, value):
+        if not isinstance(value, str):
+            value = str(value)
+        self._add_field_type(field_key, field_name)
+        row = self.cell_values.setdefault(row_number, {})
+        row[field_key] = value
+        self._update_max_length(field_key, value)
+
+    def print_table(self, header=None, printfn=_printfn, dividers=10):
+
+        if header is not None:
+            printfn(header)
+
+        field_keys = sorted(self.field_names.keys())
+
+        if not field_keys:
+            printfn('table empty')
+            return
+
+        def p_sep():
+            printfn('+' + '+'.join(
+                [(self.max_field_lengths[k] + 2) * '-'
+                 for k in field_keys]) + '+')
+
+        p_sep()
+
+        printfn('| ' + ' | '.join(
+            '%%%ds' % self.max_field_lengths[k] % self.field_names[k]
+            for k in field_keys) + ' |')
+        p_sep()
+
+        for i in range(len(self.cell_values)):
+            row = self.cell_values[i]
+            printfn(colored('| ' + ' | '.join(
+                '%%%ds' % self.max_field_lengths[k] % row.get(k, '')
+                for k in field_keys
+            ) + ' |'))
+            if not ((i + 1) % dividers):
+                p_sep()
+
+        if (i + 1) % dividers:
+            p_sep()
+
+    def _update_max_length(self, field_key, string):
+        length = len(string)
+        if length > self.max_field_lengths.get(field_key, 0):
+            self.max_field_lengths[field_key] = length
+
+    def _add_field_type(self, field_key, field_name):
+        if field_key not in self.field_names:
+            self.field_names[field_key] = field_name
+            self._update_max_length(field_key, field_name)
+        else:
+            assert self.field_names[field_key] == field_name
+
+
+def print_pb_table(header, items, fields_to_omit=None, printfn=_printfn):
+    from cli.utils import pb2dict
+
+    t = TablePrinter()
+    for row, obj in enumerate(items):
+        assert isinstance(obj, Message)
+
+        def add(_row, pb, prefix='', number=0):
+            d = pb2dict(pb)
+            for field in pb._fields:
+                fname = prefix + field.name
+                if fname in fields_to_omit:
+                    continue
+                value = getattr(pb, field.name)
+                if isinstance(value, Message):
+                    add(_row, value, fname + '.',
+                        100 * (number + field.number))
+                else:
+                    t.add_cell(_row, number + field.number, fname,
+                               d.get(field.name))
+
+        add(row, obj)
+
+    t.print_table(header, printfn)
+
+
+if __name__ == '__main__':
+    import random
+    t = TablePrinter()
+    for row in range(10):
+        t.add_cell(row, 0, 'id', row + 100)
+        t.add_cell(row, 1, 'name', 'Joe Somebody')
+        t.add_cell(row, 2, 'ows', '${}'.format(random.randint(10, 100000)))
+    t.print_table()
diff --git a/cli/utils.py b/cli/utils.py
index 5e3b7dd..941ad9c 100644
--- a/cli/utils.py
+++ b/cli/utils.py
@@ -1,9 +1,28 @@
-import os
+#
+# Copyright 2016 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
 import sys
-import requests
+
 from google.protobuf.json_format import MessageToDict
 from termcolor import cprint, colored
-from os.path import join as pjoin
+
+from cli.table import TablePrinter
+
+
+_printfn = lambda l: sys.stdout.write(l + '\n')
 
 
 def pb2dict(pb_msg):
@@ -87,49 +106,22 @@
 }
 
 
-def print_flows(what, id, type, flows, groups):
+def print_flows(what, id, type, flows, groups, printfn=_printfn):
 
-    print
-    print ''.join([
+    header = ''.join([
         '{} '.format(what),
         colored(id, color='green', attrs=['bold']),
         ' (type: ',
         colored(type, color='blue'),
         ')'
-    ])
-    print 'Flows ({}):'.format(len(flows))
+    ]) + '\nFlows ({}):'.format(len(flows))
 
-    max_field_lengths = {}
-    field_names = {}
-
-    def update_max_length(field_key, string):
-        length = len(string)
-        if length > max_field_lengths.get(field_key, 0):
-            max_field_lengths[field_key] = length
-
-    def add_field_type(field_key, field_name):
-        if field_key not in field_names:
-            field_names[field_key] = field_name
-            update_max_length(field_key, field_name)
-        else:
-            assert field_names[field_key] == field_name
-
-    cell_values = {}
-
-    # preprocess data
-    if not flows:
-        return
+    table = TablePrinter()
     for i, flow in enumerate(flows):
 
-        def add_field(field_key, field_name, value):
-            add_field_type(field_key, field_name)
-            row = cell_values.setdefault(i, {})
-            row[field_key] = value
-            update_max_length(field_key, value)
-
-        add_field(0, 'table_id', value=str(flow['table_id']))
-        add_field(1, 'priority', value=str(flow['priority']))
-        add_field(2, 'cookie', p_cookie(flow['cookie']))
+        table.add_cell(i, 0, 'table_id', value=str(flow['table_id']))
+        table.add_cell(i, 1, 'priority', value=str(flow['priority']))
+        table.add_cell(i, 2, 'cookie', p_cookie(flow['cookie']))
 
         assert flow['match']['type'] == 'OFPMT_OXM'
         for field in flow['match']['oxm_fields']:
@@ -137,40 +129,20 @@
             ofb = field['ofb_field']
             assert not ofb['has_mask'], 'masked match not handled yet'  # TODO
             type = ofb['type'][len('OFPXMT_OFB_'):]
-            add_field(*field_printers[type](ofb))
+            table.add_cell(i, *field_printers[type](ofb))
 
         for instruction in flow['instructions']:
             if instruction['type'] == 4:
                 for action in instruction['actions']['actions']:
                     type = action['type'][len('OFPAT_'):]
-                    add_field(*action_printers[type](action))
+                    table.add_cell(i, *action_printers[type](action))
 
-    # print header
-    field_keys = sorted(field_names.keys())
-    def p_sep():
-        print '+' + '+'.join(
-            [(max_field_lengths[k] + 2) * '-' for k in field_keys]) + '+'
-
-    p_sep()
-    print '| ' + ' | '.join(
-        '%%%ds' % max_field_lengths[k] % field_names[k]
-        for k in field_keys) + ' |'
-    p_sep()
-
-    # print values
-    for i in xrange(len(flows)):
-        row = cell_values[i]
-        cprint('| ' + ' | '.join(
-            '%%%ds' % max_field_lengths[k] % row.get(k, '')
-            for k in field_keys
-        ) + ' |')
-        if not ((i + 1) % 3):
-            p_sep()
-
-    if ((i + 1) % 3):
-        p_sep()
+    table.print_table(header, printfn)
 
     # TODO groups TBF
     assert len(groups) == 0
 
 
+def dict2line(d):
+    assert isinstance(d, dict)
+    return ', '.join('{}: {}'.format(k, v) for k, v in sorted(d.items()))
diff --git a/common/frameio/frameio.py b/common/frameio/frameio.py
index 27aed12..b215da3 100644
--- a/common/frameio/frameio.py
+++ b/common/frameio/frameio.py
@@ -139,7 +139,12 @@
 
     def _dispatch(self, frame):
         log.debug('calling-publisher', frame=hexify(frame))
-        self.callback(self, frame)
+        try:
+            self.callback(self, frame)
+        except Exception, e:
+            log.exception('callback-error',
+                          explanation='Callback failed while processing frame',
+                          e=e)
 
     def recv(self):
         """Called on the select thread when a packet arrives"""
diff --git a/voltha/adapters/tibit_olt/tibit_olt.py b/voltha/adapters/tibit_olt/tibit_olt.py
index 6a0ff9e..45d610f 100644
--- a/voltha/adapters/tibit_olt/tibit_olt.py
+++ b/voltha/adapters/tibit_olt/tibit_olt.py
@@ -17,39 +17,38 @@
 """
 Tibit OLT device adapter
 """
-import scapy
-import structlog
 import json
-
 from uuid import uuid4
 
-from scapy.layers.inet import ICMP, IP
+import structlog
+from scapy.fields import StrField
 from scapy.layers.l2 import Ether, Dot1Q
-from twisted.internet.defer import DeferredQueue, inlineCallbacks
+from scapy.packet import Packet, bind_layers
 from twisted.internet import reactor
-
+from twisted.internet.defer import DeferredQueue, inlineCallbacks
 from zope.interface import implementer
 
-from common.utils.asleep import asleep
-
 from common.frameio.frameio import BpfProgramFilter
-from voltha.registry import registry
 from voltha.adapters.interface import IAdapterInterface
+from voltha.adapters.tibit_olt.EOAM import EOAMPayload, DPoEOpcode_SetRequest
+from voltha.adapters.tibit_olt.EOAM_TLV import DOLTObject, \
+    PortIngressRuleClauseMatchLength02, PortIngressRuleResultForward, \
+    PortIngressRuleResultSet, PortIngressRuleResultInsert, \
+    PortIngressRuleTerminator, AddPortIngressRule, CablelabsOUI
+from voltha.adapters.tibit_olt.EOAM_TLV import PortIngressRuleHeader
+from voltha.core.flow_decomposer import *
 from voltha.core.logical_device_agent import mac_str_to_tuple
 from voltha.protos.adapter_pb2 import Adapter, AdapterConfig
+from voltha.protos.common_pb2 import LogLevel, ConnectStatus
+from voltha.protos.common_pb2 import OperStatus, AdminState
 from voltha.protos.device_pb2 import Device, Port
 from voltha.protos.device_pb2 import DeviceType, DeviceTypes
 from voltha.protos.health_pb2 import HealthStatus
-from voltha.protos.common_pb2 import LogLevel, ConnectStatus
-from voltha.protos.common_pb2 import OperStatus, AdminState
-
 from voltha.protos.logical_device_pb2 import LogicalDevice, LogicalPort
 from voltha.protos.openflow_13_pb2 import ofp_desc, ofp_port, OFPPF_10GB_FD, \
     OFPPF_FIBER, OFPPS_LIVE, ofp_switch_features, OFPC_PORT_STATS, \
     OFPC_GROUP_STATS, OFPC_TABLE_STATS, OFPC_FLOW_STATS
-
-from scapy.packet import Packet, bind_layers
-from scapy.fields import StrField
+from voltha.registry import registry
 
 log = structlog.get_logger()
 
@@ -156,7 +155,8 @@
 
         # if we got response, we can fill out the device info, mark the device
         # reachable
-        jdev = json.loads(response.data[5:])
+        # jdev = json.loads(response.data[5:])
+        jdev = json.loads(response.payload.payload.body.load)
         device.root = True
         device.vendor = 'Tibit Communications, Inc.'
         device.model = jdev.get('results', {}).get('device', 'DEVICE_UNKNOWN')
@@ -255,7 +255,7 @@
             if 1: # TODO check if it is really what we expect, and wait if not
                 break
 
-        jdev = json.loads(response.data[5:])
+        jdev = json.loads(response.payload.payload.body.load)
         tibit_mac = ''
         for macid in jdev['results']:
             if macid['macid'] is None:
@@ -342,8 +342,78 @@
         raise NotImplementedError()
 
     def update_flows_bulk(self, device, flows, groups):
-        log.debug('bulk-flow-update', device_id=device.id,
-                  flows=flows, groups=groups)
+        log.info('bulk-flow-update', device_id=device.id,
+                 flows=flows, groups=groups)
+
+        assert len(groups.items) == 0, "Cannot yet deal with groups"
+
+        for flow in flows.items:
+            in_port = get_in_port(flow)
+            assert in_port is not None
+
+            precedence = 255 - min(flow.priority / 256, 255)
+
+            if in_port == 2:
+                # Downstream rule
+                pass  # TODO still ignores
+
+            elif in_port == 1:
+                # Upstream rule
+                req = DOLTObject()
+                req /= PortIngressRuleHeader(precedence=precedence)
+
+                for field in get_ofb_fields(flow):
+                    if field.type == ETH_TYPE:
+                        _type = field.eth_type
+                        req /= PortIngressRuleClauseMatchLength02(
+                            fieldcode=3,
+                            operator=1,
+                            match0=(_type >> 8) & 0xff,
+                            match1=_type & 0xff)
+                    elif field.type == IP_PROTO:
+                        pass
+                    # TODO etc
+
+                for action in get_actions(flow):
+
+                    if action.type == OUTPUT:
+                        req /= PortIngressRuleResultForward()
+
+                    elif action.type == PUSH_VLAN:
+                        if action.push.ethertype != 0x8100:
+                            log.error('unhandled-ether-type',
+                                      ethertype=action.push.ethertype)
+                        req /= PortIngressRuleResultInsert(fieldcode=7)
+
+                    elif action.type == SET_FIELD:
+                        assert (action.set_field.field.oxm_class ==
+                                ofp.OFPXMC_OPENFLOW_BASIC)
+                        field = action.set_field.field.ofb_field
+                        if field.type == VLAN_VID:
+                            req /= PortIngressRuleResultSet(
+                                fieldcode=7, value=field.vlan_vid & 0xfff)
+                        else:
+                            log.error('unsupported-action-set-field-type',
+                                      field_type=field.type)
+
+                    else:
+                        log.error('unsupported-action-type',
+                                  action_type=action.type)
+
+                req /= PortIngressRuleTerminator()
+                req /= AddPortIngressRule()
+
+                msg = (
+                    Ether(dst=device.mac_address) /
+                    Dot1Q(vlan=TIBIT_MGMT_VLAN, prio=TIBIT_MGMT_PRIORITY) /
+                    EOAMPayload(
+                        body=CablelabsOUI() / DPoEOpcode_SetRequest() / req)
+                )
+
+                self.io_port.send(str(msg))
+
+            else:
+                raise Exception('Port should be 1 or 2 by our convention')
 
     def update_flows_incrementally(self, device, flow_changes, group_changes):
         raise NotImplementedError()