CLI to aid integration and testing

Change-Id: If18f194e45a8fc090a6b7869bb6d81728397ec9b
diff --git a/.gitignore b/.gitignore
index 54c7891..50df298 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,3 +60,7 @@
 
 # Files copied over during make
 ofagent/protos/third_party/google
+
+# Voltha CLI hostory files
+.voltha_cli_history
+**/.voltha_cli_history
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..9b97814
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,36 @@
+# TODO
+
+Miscellaneous next steps across the Voltha code base. These may not be entered
+in Jira yet. Items are organized per area:
+ 
+ 
+## Voltha
+
+* Ctrl-C should reliably stop Voltha
+
+
+## Chameleon
+
+* Ctrl-C should reliably stop Voltha
+
+
+## OF Agent
+
+* Ctrl-C should reliably stop Voltha
+* Make it reconnect after Voltha restarted
+
+## CLI
+
+* Add auto-completion for most common args like device and logical device ids
+* Add consistent argument checking
+* Unify code that retrieves data from gRPC
+* Unify code that prints out data/response, to allow:
+  * Selectable output mode:
+    * JSON
+    * Tabular
+* Organize history per sub context so that in each context the commands 
+  entered in that context will show
+* Metaprogramming [BIG ONE]: Make large part of the commands come from annotations embedded in
+  the protobuf files and have corresponding handler auto-generated by protoc
+* Package CLI as docker container, bake it into composition
+* Add commands in order of usage priority
diff --git a/cli/README.md b/cli/README.md
new file mode 100644
index 0000000..c810df4
--- /dev/null
+++ b/cli/README.md
@@ -0,0 +1,14 @@
+## CLI (~/cli)
+
+* Add auto-completion for most common args like device and logical device ids
+* Add consistent argument checking
+* Unify code that retrieves data from gRPC
+* Unify code that prints out data/response, to allow:
+  * Selectable output mode:
+    * JSON
+    * Tabular
+* Organize history per sub context so that in each context the commands 
+  entered in that context will show
+* Metaprogramming [BIG ONE]: Make large part of the commands come from annotations embedded in
+  the protobuf files and have corresponding handler auto-generated by protoc
+* Package CLI as docker container, bake it into composition
diff --git a/cli/device.py b/cli/device.py
new file mode 100644
index 0000000..be5450b
--- /dev/null
+++ b/cli/device.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+#
+# 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.
+#
+
+"""
+Device level CLI commands
+"""
+from cmd2 import Cmd
+from simplejson import dumps
+
+from cli.utils import print_flows, pb2dict
+from voltha.protos import third_party
+
+_ = third_party
+from voltha.protos import voltha_pb2
+
+
+class DeviceCli(Cmd):
+
+    def __init__(self, get_channel, device_id):
+        Cmd.__init__(self)
+        self.get_channel = get_channel
+        self.device_id = device_id
+        self.prompt = '(' + self.colorize(
+            self.colorize('device {}'.format(device_id), 'red'), 'bold') + ') '
+
+    def get_device(self, depth=0):
+        stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
+        res = stub.GetDevice(voltha_pb2.ID(id=self.device_id),
+                             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)
+
+    def do_flows(self, arg):
+        """Show flow table for device"""
+        device = pb2dict(self.get_device(-1))
+        print_flows(
+            'Device',
+            self.device_id,
+            type=device['type'],
+            flows=device['flows']['items'],
+            groups=device['flow_groups']['items']
+        )
+
diff --git a/cli/logical_device.py b/cli/logical_device.py
new file mode 100644
index 0000000..d60c6eb
--- /dev/null
+++ b/cli/logical_device.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+#
+# 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.
+#
+
+"""
+Logical device level CLI commands
+"""
+from cmd2 import Cmd
+from simplejson import dumps
+
+from cli.utils import pb2dict
+from cli.utils import print_flows
+from voltha.protos import third_party
+
+_ = third_party
+from voltha.protos import voltha_pb2
+
+
+class LogicalDeviceCli(Cmd):
+
+    def __init__(self, get_channel, logical_device_id):
+        Cmd.__init__(self)
+        self.get_channel = get_channel
+        self.logical_device_id = logical_device_id
+        self.prompt = '(' + self.colorize(
+            self.colorize('logical device {}'.format(logical_device_id), 'red'),
+            'bold') + ') '
+
+    def get_logical_device(self, depth=0):
+        stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
+        res = stub.GetLogicalDevice(voltha_pb2.ID(id=self.logical_device_id),
+                                    metadata=(('get-depth', str(depth)), ))
+        return res
+
+    def do_show(self, arg):
+        """Show detailed logical device information"""
+        print dumps(pb2dict(self.get_logical_device(depth=-1)),
+                    indent=4, sort_keys=True)
+
+    def do_flows(self, arg):
+        """Show flow table for logical device"""
+        logical_device = pb2dict(self.get_logical_device(-1))
+        print_flows(
+            'Logical Device',
+            self.logical_device_id,
+            type='n/a',
+            flows=logical_device['flows']['items'],
+            groups=logical_device['flow_groups']['items']
+        )
+
diff --git a/cli/main.py b/cli/main.py
index 7b90038..e9e881a 100755
--- a/cli/main.py
+++ b/cli/main.py
@@ -14,38 +14,59 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-import sys
-from cmd2 import Cmd, make_option, options
 import readline
+import sys
+from optparse import make_option
+from time import sleep
+
 import grpc
+import requests
+from cmd2 import Cmd, options
+from google.protobuf.empty_pb2 import Empty
 from simplejson import dumps
 
+from cli.device import DeviceCli
+from cli.logical_device import LogicalDeviceCli
+from voltha.core.flow_decomposer import *
 from voltha.protos import third_party
 from voltha.protos import voltha_pb2
-from google.protobuf.empty_pb2 import Empty
-from google.protobuf.json_format import MessageToDict
+from voltha.protos.openflow_13_pb2 import FlowTableUpdate
+
 _ = third_party
-from cli.utils import print_flows
+from cli.utils import pb2dict
 
 
-def pb2dict(pb_msg):
-    d = MessageToDict(pb_msg, including_default_value_fields=1,
-                      preserving_proto_field_name=1)
-    return d
-
+banner = """\
+           _ _   _              _ _
+  __ _____| | |_| |_  __ _   __| (_)
+  \ V / _ \ |  _| ' \/ _` | / _| | |
+   \_/\___/_|\__|_||_\__,_| \__|_|_|
+(to exit type q, exit or quit or hit Ctrl-D)
+"""
 
 class VolthaCli(Cmd):
 
     prompt = 'voltha'
     history_file_name = '.voltha_cli_history'
+
+    # Settable CLI parameters
+    voltha_grpc = 'localhost:50055'
+    voltha_sim_rest = 'localhost:18880'
     max_history_lines = 500
+    default_device_id = None
+    default_logical_device_id = None
 
     Cmd.settable.update(dict(
-        voltha_grpc='Voltha GRPC endpoint in form of <host>:<port>'
+        voltha_grpc='Voltha GRPC endpoint in form of <host>:<port>',
+        voltha_sim_rest='Voltha simulation back door for testing in form '
+                        'of <host>:<port>',
+        max_history_lines='Maximum number of history lines stored across '
+                          'sessions',
+        default_device_id='Device id used when no device id is specified',
+        default_logical_device_id='Logical device id used when no device id '
+                                  'is specified',
     ))
 
-    voltha_grpc = 'localhost:50055'
-
     def __init__(self, *args, **kw):
         Cmd.__init__(self, *args, **kw)
         self.prompt = '(' + self.colorize(
@@ -79,6 +100,9 @@
             self.channel = grpc.insecure_channel(self.voltha_grpc)
         return self.channel
 
+    def preloop(self):
+        self.poutput(banner)
+
     def do_reset_history(self, arg):
         """Reset CLI history"""
         while self.history:
@@ -111,12 +135,18 @@
 
     def do_device(self, arg):
         """Enter device level command mode"""
-        sub = DeviceCli(self.get_channel, arg)
+        device_id = arg 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):
         """Enter logical device level command mode"""
-        sub = LogicalDeviceCli(self.get_channel, arg)
+        logical_device_id = arg 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):
@@ -130,70 +160,111 @@
         res = stub.GetHealthStatus(Empty())
         print dumps(pb2dict(res), indent=4)
 
+    def do_test(self, arg):
+        """Enter test mode, which makes a bunch on new commands available"""
+        sub = TestCli(self.history, self.get_channel)
+        sub.cmdloop()
 
-class DeviceCli(Cmd):
 
-    def __init__(self, get_channel, device_id):
-        Cmd.__init__(self)
+class TestCli(VolthaCli):
+
+    def __init__(self, history, get_channel):
+        VolthaCli.__init__(self)
+        self.history = history
         self.get_channel = get_channel
-        self.device_id = device_id
-        self.prompt = '(' + self.colorize(
-            self.colorize('device {}'.format(device_id), 'red'), 'bold') + ') '
-
-    def get_device(self, depth=0):
-        stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
-        res = stub.GetDevice(voltha_pb2.ID(id=self.device_id),
-                             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)
-
-    def do_flows(self, arg):
-        """Show flow table for device"""
-        device = pb2dict(self.get_device(-1))
-        print_flows(
-            'Device',
-            self.device_id,
-            type=device['type'],
-            flows=device['flows']['items'],
-            groups=device['flow_groups']['items']
-        )
-
-
-class LogicalDeviceCli(Cmd):
-
-    def __init__(self, get_channel, logical_device_id):
-        Cmd.__init__(self)
-        self.get_channel = get_channel
-        self.logical_device_id = logical_device_id
-        self.prompt = '(' + self.colorize(
-            self.colorize('device {}'.format(logical_device_id), 'red'),
+        self.prompt = '(' + self.colorize(self.colorize('test', 'cyan'),
             'bold') + ') '
 
-    def get_logical_device(self, depth=0):
+    @options([
+        make_option('-t', '--device-type', action="store", dest='device_type',
+                     help="Device type", default='simulated_olt'),
+        make_option('-m', '--mac-address', action='store', dest='mac_address',
+                    default='00:0c:e2:31:40:00'),
+        make_option('-i', '--ip-address', action='store', dest='ip_address'),
+    ])
+    def do_preprovision_olt(self, arg, opts):
+        """Preprovision a new OLT with given device type"""
         stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
-        res = stub.GetLogicalDevice(voltha_pb2.ID(id=self.logical_device_id),
-                                    metadata=(('get-depth', str(depth)), ))
-        return res
+        kw = dict(type=opts.device_type)
+        if opts.ip_address:
+            kw['ipv4_address'] = opts.ip_address
+        elif opts.mac_address:
+            kw['mac_address'] = opts.mac_address
+        else:
+            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.default_device_id = device.id
 
-    def do_show(self, arg):
-        """Show detailed logical device information"""
-        print dumps(pb2dict(self.get_logical_device(depth=-1)),
-                    indent=4, sort_keys=True)
+    def do_activate_olt(self, arg):
+        """
+        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
+        stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
+        stub.ActivateDevice(voltha_pb2.ID(id=device_id))
 
-    def do_flows(self, arg):
-        """Show flow table for logical device"""
-        logical_device = pb2dict(self.get_logical_device(-1))
-        print_flows(
-            'Logical Device',
-            self.logical_device_id,
-            type='n/a',
-            flows=logical_device['flows']['items'],
-            groups=logical_device['flow_groups']['items']
+        # try to acquire logical device id
+        while True:
+            device = stub.GetDevice(voltha_pb2.ID(id=device_id))
+            if device.oper_status == voltha_pb2.OperStatus.ACTIVE:
+                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)
+
+    def do_arrive_onus(self, arg):
+        """
+        Simulate the arrival of ONUs
+        """
+        device_id = arg 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):
+        """
+        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
+        update = FlowTableUpdate(
+            id=logical_device_id,
+            flow_mod = mk_simple_flow_mod(
+                priority=2000,
+                match_fields=[in_port(101), eth_type(0x888e)],
+                actions=[
+                    push_vlan(0x8100),
+                    set_field(vlan_vid(4096 + 4000)),
+                    output(ofp.OFPP_CONTROLLER)
+                ]
+            )
         )
+        stub = voltha_pb2.VolthaLocalServiceStub(self.get_channel())
+        res = stub.UpdateLogicalDeviceFlowTable(update)
+        print 'success', res
+
+    def do_send_simulated_upstream_eapol(self, arg):
+        """
+        Send an EAPOL upstream from a simulated OLT
+        """
+        device_id = arg 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):
+        """
+        Send out an an EAPOL start message into the given Unix interface
+        """
+        pass
 
 
 if __name__ == '__main__':
diff --git a/cli/utils.py b/cli/utils.py
index 23fab7f..5e3b7dd 100644
--- a/cli/utils.py
+++ b/cli/utils.py
@@ -1,10 +1,17 @@
 import os
 import sys
 import requests
+from google.protobuf.json_format import MessageToDict
 from termcolor import cprint, colored
 from os.path import join as pjoin
 
 
+def pb2dict(pb_msg):
+    d = MessageToDict(pb_msg, including_default_value_fields=1,
+                      preserving_proto_field_name=1)
+    return d
+
+
 def p_cookie(cookie):
     cookie = str(cookie)
     if len(cookie) > 8:
diff --git a/tmp_integration.md b/tmp_integration.md
new file mode 100644
index 0000000..d170012
--- /dev/null
+++ b/tmp_integration.md
@@ -0,0 +1,12 @@
+## ONOS bring-up
+
+```
+docker pull onosproject/onos
+docker run -itd --name=onos onosproject/onos
+ssh -p 8101 karaf@localhost
+```
+
+Loaded olt, config
+
+
+TODO: 
diff --git a/voltha/adapters/simulated_olt/simulated_olt.py b/voltha/adapters/simulated_olt/simulated_olt.py
index d407c4b..f550f97 100644
--- a/voltha/adapters/simulated_olt/simulated_olt.py
+++ b/voltha/adapters/simulated_olt/simulated_olt.py
@@ -78,7 +78,7 @@
         self.control_endpoint.listen(self.get_test_control_site())
 
         # TODO tmp: populate some devices and logical devices
-        reactor.callLater(0, self._tmp_populate_stuff)
+        # reactor.callLater(0, self._tmp_populate_stuff)
         log.info('started')
 
     def stop(self):
@@ -103,8 +103,8 @@
         return device
 
     def abandon_device(self, device):
-        raise NotImplementedError(0
-                                  )
+        raise NotImplementedError()
+
     def deactivate_device(self, device):
         raise NotImplementedError()
 
@@ -274,10 +274,8 @@
         # then shortly after we create the logical device with one port
         # that will correspond to the NNI port
         yield asleep(0.05)
-        logical_device_id = uuid4().hex[:12]
         ld = LogicalDevice(
-            id=logical_device_id,
-            datapath_id=int('0x' + logical_device_id[:8], 16), # from id
+            # not setting id and datapth_id will let the adapter agent pick id
             desc=ofp_desc(
                 mfr_desc='cord porject',
                 hw_desc='simualted pon',
@@ -297,9 +295,9 @@
             ),
             root_device_id=device.id
         )
-        self.adapter_agent.create_logical_device(ld)
+        ld_initialized = self.adapter_agent.create_logical_device(ld)
         cap = OFPPF_1GB_FD | OFPPF_FIBER
-        self.adapter_agent.add_logical_port(ld.id, LogicalPort(
+        self.adapter_agent.add_logical_port(ld_initialized.id, LogicalPort(
             id='nni',
             ofp_port=ofp_port(
                 port_no=129,
@@ -320,7 +318,7 @@
 
         # and finally update to active
         device = self.adapter_agent.get_device(device.id)
-        device.parent_id = ld.id
+        device.parent_id = ld_initialized.id
         device.oper_status = OperStatus.ACTIVE
         self.adapter_agent.update_device(device)
 
@@ -389,10 +387,10 @@
         """Simulate a packet in message posted upstream"""
         log.info('test_eapol_in', request=request, device_id=device_id)
         eapol_start = str(
-            (Ether(src='00:11:22:33:44:55') / EAPOL(type=1))
-            / Padding(load=42*'\x00')
+            Ether(src='00:11:22:33:44:55') /
+            EAPOL(type=1, len=0) /
+            Padding(load=42*'\x00')
         )
-
         device = self.adapter_agent.get_device(device_id)
         self.adapter_agent.send_packet_in(logical_device_id=device.parent_id,
                                           logical_port_no=1,
diff --git a/voltha/core/adapter_agent.py b/voltha/core/adapter_agent.py
index 2a6c61b..0c5448f 100644
--- a/voltha/core/adapter_agent.py
+++ b/voltha/core/adapter_agent.py
@@ -189,11 +189,29 @@
         self._make_up_to_date('/devices/{}/ports'.format(device_id),
                               port.port_no, port)
 
+    def _find_first_available_id(self):
+        logical_devices = self.root_proxy.get('/logical_devices')
+        existing_ids = set(ld.id for ld in logical_devices)
+        existing_datapath_ids = set(ld.datapath_id for ld in logical_devices)
+        i = 1
+        while True:
+            if i not in existing_datapath_ids and str(i) not in existing_ids:
+                return i
+            i += 1
+
     def create_logical_device(self, logical_device):
         assert isinstance(logical_device, LogicalDevice)
+
+        if not logical_device.id:
+            id = self._find_first_available_id()
+            logical_device.id = str(id)
+            logical_device.datapath_id = id
+
         self._make_up_to_date('/logical_devices',
                               logical_device.id, logical_device)
 
+        return logical_device
+
     def add_logical_port(self, logical_device_id, port):
         assert isinstance(port, LogicalPort)
         self._make_up_to_date(