ADTRAN: Update to containerized adapters.
Loading and running but still needs about a week or so of work to
catch up to where the OpenOLT/ONU containerized adapters are

Change-Id: I5522232e21dd76b3f95b4698af6c7e4cef96753d
diff --git a/.gitignore b/.gitignore
index 13af726..5733c21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -179,3 +179,10 @@
 # Output of the go coverage tool, specifically when used with LiteIDE
 *.out
 
+#################
+# Other
+docs/
+/venv-linux/
+/voltha-protos/
+/bin/
+/.idea/
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 6833f4b..6cec29c 100644
--- a/Makefile
+++ b/Makefile
@@ -43,18 +43,19 @@
 	 --rm --force-rm \
 	$(DOCKER_BUILD_EXTRA_ARGS)
 
-VENVDIR := venv-$(shell uname -s | tr '[:upper:]' '[:lower:]')
-VENV_BIN ?= virtualenv
-VENV_OPTS ?=
-
-PYVOLTHA_DIR ?= ../pyvoltha
-
 DOCKER_IMAGE_LIST = \
 	voltha-adtran-base \
-	voltha-adapter-adtran-onu \
 	voltha-adapter-adtran-olt
+	# voltha-adapter-adtran-onu \
 
-.PHONY: base adtran_olt adtran_onu tag push pull
+VENVDIR := venv-$(shell uname -s | tr '[:upper:]' '[:lower:]')
+VENV_BIN ?= virtualenv
+VENV_OPTS ?= -v
+
+PYVOLTHA_DIR ?= ../pyvoltha
+VOLTHA_PROTO_DIR ?= ../voltha-protos
+
+.PHONY: $(DIRS) $(DIRS_CLEAN) base adtran_olt adtran_onu tag push pull
 
 # This should to be the first and default target in this Makefile
 help:
@@ -65,9 +66,8 @@
                     If this is the first time you are building, choose \"make build\" option."
 	@echo "clean              : Remove files created by the build and tests"
 	@echo "distclean          : Remove venv directory"
-	@echo "fetch              : Pre-fetch artifacts for subsequent local builds"
 	@echo "help               : Print this help"
-	@echo "rebuild-venv       : Rebuild local Python virtualenv from scratch"
+	@echo "rebuild-venv       : Rebuild local Pythozn virtualenv from scratch"
 	@echo "venv               : Build local Python virtualenv if did not exist yet"
 	@echo "containers         : Build all the docker containers"
 	@echo "base               : Build the base docker container used by all other dockers"
@@ -80,13 +80,17 @@
 
 build: containers
 
-containers: base adapter_adtran_olt adapter_adtran_onu
+# containers: base adapter_adtran_olt adapter_adtran_onu olt_only onu_only
+containers: base adapter_adtran_olt olt_only
 
 base:
 ifdef LOCAL_PYVOLTHA
 	@rm -f pyvoltha/dist/*
 	@mkdir -p pyvoltha/dist
 	cp $(PYVOLTHA_DIR)/dist/*.tar.gz pyvoltha/dist/
+	@rm -f voltha-protos/*
+	mkdir -p voltha-protos/dist
+	cp $(VOLTHA_PROTO_DIR)/dist/*.tar.gz voltha-protos/dist/
 	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}voltha-adtran-base-local:${TAG} -f docker/Dockerfile.base_local .
 else
 	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}voltha-adtran-base:${TAG} -f docker/Dockerfile.base .
@@ -106,6 +110,20 @@
 	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}voltha-adapter-adtran-onu:${TAG} -f docker/Dockerfile.adapter_adtran_onu .
 endif
 
+olt_only: base
+ifdef PYVOLTHA_BASE_IMAGE
+	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}voltha-adapter-adtran-olt:${TAG} -f docker/Dockerfile.adapter_adtran_olt_pyvoltha .
+else
+	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}voltha-adapter-adtran-olt:${TAG} -f docker/Dockerfile.adapter_adtran_olt .
+endif
+
+onu_only:
+ifdef PYVOLTHA_BASE_IMAGE
+	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}voltha-adapter-adtran-onu:${TAG} -f docker/Dockerfile.adapter_adtran_onu_pyvoltha .
+else
+	docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}voltha-adapter-adtran-onu:${TAG} -f docker/Dockerfile.adapter_adtran_onu .
+endif
+
 tag: $(patsubst  %,%.tag,$(DOCKER_IMAGE_LIST))
 
 push: tag $(patsubst  %,%.push,$(DOCKER_IMAGE_LIST))
@@ -122,8 +140,9 @@
 	docker pull ${REGISTRY}${REPOSITORY}voltha-$(subst .pull,,$@):${TAG}
 
 clean:
-	find . -name '*.pyc' | xargs rm -f
 	rm -rf pyvoltha
+	rm -rf voltha-protos
+	find . -name '*.pyc' | xargs rm -f
 
 distclean: clean
 	rm -rf ${VENVDIR}
@@ -151,9 +170,12 @@
 
 ifdef LOCAL_PYVOLTHA
 	mkdir -p pyvoltha/dist
-	cp ../../pyvoltha/dist/*.tar.gz pyvoltha/dist/
+	cp $(PYVOLTHA_DIR)/dist/*.tar.gz pyvoltha/dist/
+	mkdir -p voltha-protos/dist
+	cp $(VOLTHA_PROTO_DIR)/dist/*.tar.gz voltha-protos/dist/
 	@ . ${VENVDIR}/bin/activate && \
-	    pip install pyvoltha/dist/*.tar.gz
+	    pip install pyvoltha/dist/*.tar.gz && \
+	    pip install voltha-protos/dist/*.tar.gz
 endif
 
 # end file
diff --git a/adapters/adtran_common/adtran_device_handler.py b/adapters/adtran_common/adtran_device_handler.py
index 79877b7..308cc0b 100644
--- a/adapters/adtran_common/adtran_device_handler.py
+++ b/adapters/adtran_common/adtran_device_handler.py
@@ -35,6 +35,8 @@
 from pyvoltha.adapters.extensions.kpi.olt.olt_pm_metrics import OltPmMetrics
 from pyvoltha.common.utils.asleep import asleep
 from flow.flow_tables import DeviceFlows, DownstreamFlows
+from adapters.adtran_olt.net.pio_zmq import DEFAULT_PIO_TCP_PORT
+from adapters.adtran_olt.net.pon_zmq import DEFAULT_PON_AGENT_TCP_PORT
 
 _ = third_party
 
@@ -86,8 +88,6 @@
     RESTART_RPC = '<system-restart xmlns="urn:ietf:params:xml:ns:yang:ietf-system"/>'
 
     def __init__(self, **kwargs):
-        from net.pio_zmq import DEFAULT_PIO_TCP_PORT
-        from net.pon_zmq import DEFAULT_PON_AGENT_TCP_PORT
 
         super(AdtranDeviceHandler, self).__init__()
 
@@ -159,9 +159,6 @@
         self.heartbeat = None
         self.heartbeat_last_reason = ''
 
-        # Virtualized OLT Support
-        self.is_virtual_olt = False
-
         # Installed flows
         self._evcs = {}  # Flow ID/name -> FlowEntry
 
@@ -233,7 +230,7 @@
             self.host_and_port = device.host_and_port.split(":")
             self.ip_address = self.host_and_port[0]
             self.netconf_port = int(self.host_and_port[1])
-            self.adapter_agent.update_device(device)
+            self.adapter_agent.device_update(device)
 
         else:
             self.activate_failed(device, 'No IP_address field provided')
@@ -335,14 +332,10 @@
                 self.parse_provisioning_options(device)
 
                 ############################################################################
-                # Currently, only virtual OLT (pizzabox) is supported
-                # self.is_virtual_olt = Add test for MOCK Device if we want to support it
-
-                ############################################################################
                 # Start initial discovery of NETCONF support (if any)
                 try:
                     device.reason = 'establishing NETCONF connection'
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
 
                     self.startup = self.make_netconf_connection()
                     yield self.startup
@@ -355,7 +348,7 @@
                 # Update access information on network device for full protocol support
                 try:
                     device.reason = 'device networking validation'
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
                     self.startup = self.ready_network_access()
                     yield self.startup
 
@@ -367,7 +360,7 @@
                 # Restconf setup
                 try:
                     device.reason = 'establishing RESTConf connections'
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
                     self.startup = self.make_restconf_connection()
                     yield self.startup
 
@@ -379,11 +372,11 @@
                 # Get the device Information
                 if reconciling:
                     device.connect_status = ConnectStatus.REACHABLE
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
                 else:
                     try:
                         device.reason = 'retrieving device information'
-                        self.adapter_agent.update_device(device)
+                        yield self.adapter_agent.device_update(device)
                         self.startup = self.get_device_info(device)
                         results = yield self.startup
 
@@ -396,7 +389,7 @@
                         device.root = True
                         device.vendor = results.get('vendor', 'Adtran Inc.')
                         device.connect_status = ConnectStatus.REACHABLE
-                        self.adapter_agent.update_device(device)
+                        yield self.adapter_agent.device_update(device)
 
                     except Exception as e:
                         self.log.exception('device-info', e=e)
@@ -405,7 +398,7 @@
                 try:
                     # Enumerate and create Northbound NNI interfaces
                     device.reason = 'enumerating northbound interfaces'
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
                     self.startup = self.enumerate_northbound_ports(device)
                     results = yield self.startup
 
@@ -413,7 +406,7 @@
                     yield self.startup
 
                     device.reason = 'adding northbound interfaces to adapter'
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
 
                     if not reconciling:
                         for port in self.northbound_ports.itervalues():
@@ -426,7 +419,7 @@
                 try:
                     # Enumerate and create southbound interfaces
                     device.reason = 'enumerating southbound interfaces'
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
                     self.startup = self.enumerate_southbound_ports(device)
                     results = yield self.startup
 
@@ -434,7 +427,7 @@
                     yield self.startup
 
                     device.reason = 'adding southbound interfaces to adapter'
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
 
                     if not reconciling:
                         for port in self.southbound_ports.itervalues():
@@ -471,7 +464,7 @@
                 if self.pm_metrics is None:
                     try:
                         device.reason = 'setting up Performance Monitoring configuration'
-                        self.adapter_agent.update_device(device)
+                        yield self.adapter_agent.device_update(device)
 
                         kwargs = {
                             'nni-ports': self.northbound_ports.values(),
@@ -483,7 +476,7 @@
 
                         pm_config = self.pm_metrics.make_proto()
                         self.log.debug("initial-pm-config", pm_config=pm_config)
-                        self.adapter_agent.update_device_pm_config(pm_config, init=True)
+                        yield self.adapter_agent.device_pm_config_update(pm_config, init=True)
 
                     except Exception as e:
                         self.log.exception('pm-setup', e=e)
@@ -493,7 +486,7 @@
                 # Set the ports in a known good initial state
                 if not reconciling:
                     device.reason = 'setting device to a known initial state'
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
                     try:
                         for port in self.northbound_ports.itervalues():
                             self.startup = yield port.reset()
@@ -509,7 +502,7 @@
                 # Create logical ports for all southbound and northbound interfaces
                 try:
                     device.reason = 'creating logical ports'
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
                     self.startup = self.create_logical_ports(device, ld_initialized, reconciling)
                     yield self.startup
 
@@ -520,7 +513,7 @@
                 ############################################################################
                 # Setup Alarm handler
                 device.reason = 'setting up adapter alarms'
-                self.adapter_agent.update_device(device)
+                yield self.adapter_agent.device_update(device)
 
                 self.alarms = AdapterAlarms(self.adapter_agent, device.id, ld_initialized.id)
 
@@ -531,7 +524,7 @@
                 try:
                     self.log.debug('device-activation-procedures')
                     device.reason = 'performing model specific activation procedures'
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
                     self.startup = self.complete_device_specific_activation(device, reconciling)
                     yield self.startup
 
@@ -547,7 +540,7 @@
                 device.parent_id = ld_initialized.id
                 device.oper_status = OperStatus.ACTIVE
                 device.reason = ''
-                self.adapter_agent.update_device(device)
+                yield self.adapter_agent.device_update(device)
                 self.logical_device_id = ld_initialized.id
 
                 # Start collecting stats from the device after a brief pause
@@ -567,6 +560,7 @@
 
         returnValue('activated')
 
+    @inlineCallbacks
     def restart_activate(self, done_deferred, reconciling):
         """
         Startup activation failed, pause a short period of time and retry
@@ -580,18 +574,20 @@
                 d.cancel()
         except:
             pass
+
         device = self.adapter_agent.get_device(self.device_id)
         device.reason = 'Failed during {}, retrying'.format(device.reason)
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
         self.startup = reactor.callLater(_STARTUP_RETRY_TIMEOUT, self.activate,
                                          done_deferred, reconciling)
-        return 'retrying'
+        returnValue('retrying')
 
     @inlineCallbacks
     def ready_network_access(self):
         # Override in device specific class if needed
         returnValue('nop')
 
+    @inlineCallbacks
     def activate_failed(self, device, reason, reachable=True):
         """
         Activation process (adopt_device) has failed.
@@ -608,7 +604,7 @@
             device.connect_status = ConnectStatus.UNREACHABLE
 
         device.reason = reason
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
         raise Exception('Failed to activate OLT: {}'.format(device.reason))
 
     @inlineCallbacks
@@ -625,19 +621,11 @@
         client = self._netconf_client
 
         if client is None:
-            if not self.is_virtual_olt:
-                client = AdtranNetconfClient(self.ip_address,
-                                             self.netconf_port,
-                                             self.netconf_username,
-                                             self.netconf_password,
-                                             self.timeout)
-            else:
-                from python.adapters.adtran.adtran_common.net.mock_netconf_client import MockNetconfClient
-                client = MockNetconfClient(self.ip_address,
-                                           self.netconf_port,
-                                           self.netconf_username,
-                                           self.netconf_password,
-                                           self.timeout)
+            client = AdtranNetconfClient(self.ip_address,
+                                         self.netconf_port,
+                                         self.netconf_username,
+                                         self.netconf_password,
+                                         self.timeout)
         if client.connected:
             self._netconf_client = client
             returnValue(True)
@@ -889,7 +877,7 @@
         # Get the latest device reference
         device = self.adapter_agent.get_device(self.device_id)
         device.reason = 'Disabling'
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
 
         # Drop registration for ONU detection
         # self.adapter_agent.unregister_for_onu_detect_state(self.device.id)
@@ -905,7 +893,7 @@
 
         device.oper_status = OperStatus.UNKNOWN
         device.connect_status = ConnectStatus.UNREACHABLE
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
 
         # Disable all child devices first
         self.adapter_agent.update_child_devices_state(self.device_id,
@@ -941,7 +929,7 @@
         self._rest_client = None
 
         device.reason = ''
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
         self.log.info('disabled', device_id=device.id)
         returnValue(None)
 
@@ -972,7 +960,7 @@
         # Update the connect status to REACHABLE
         device.connect_status = ConnectStatus.REACHABLE
         device.oper_status = OperStatus.ACTIVATING
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
 
         # Reenable any previously configured southbound ports
         for port in self.southbound_ports.itervalues():
@@ -1015,7 +1003,7 @@
         self.logical_device_id = ld_initialized.id
 
         # update device active status now
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
 
         # Reenable all child devices
         self.adapter_agent.update_child_devices_state(device.id,
@@ -1051,14 +1039,12 @@
         except:
             pass
         # Issue reboot command
+        try:
+            yield self.netconf_client.rpc(AdtranDeviceHandler.RESTART_RPC)
 
-        if not self.is_virtual_olt:
-            try:
-                yield self.netconf_client.rpc(AdtranDeviceHandler.RESTART_RPC)
-
-            except Exception as e:
-                self.log.exception('NETCONF-shutdown', e=e)
-                returnValue(defer.fail(Failure()))
+        except Exception as e:
+            self.log.exception('NETCONF-shutdown', e=e)
+            returnValue(defer.fail(Failure()))
 
         # self.adapter_agent.unregister_for_onu_detect_state(self.device.id)
 
@@ -1070,7 +1056,7 @@
         previous_conn_status = device.connect_status
         device.oper_status = OperStatus.ACTIVATING
         device.connect_status = ConnectStatus.UNREACHABLE
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
 
         # Update the child devices connect state to UNREACHABLE
         self.adapter_agent.update_child_devices_state(self.device_id,
@@ -1130,7 +1116,7 @@
                 finally:
                     self._netconf_client = None
 
-        if (self.netconf_client is None and not self.is_virtual_olt) or self.rest_client is None:
+        if self.netconf_client is None or self.rest_client is None:
             current_time = time.time()
             if current_time < timeout:
                 self.startup = reactor.callLater(5, self._finish_reboot, timeout,
@@ -1138,7 +1124,7 @@
                                                  previous_conn_status)
                 returnValue(self.startup)
 
-            if self.netconf_client is None and not self.is_virtual_olt:
+            if self.netconf_client is None:
                 self.log.error('NETCONF-restore-failure')
                 pass        # TODO: What is best course of action if cannot get clients back?
 
@@ -1154,7 +1140,7 @@
         device = self.adapter_agent.get_device(self.device_id)
         device.oper_status = previous_oper_status
         device.connect_status = previous_conn_status
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
 
         # Update the child devices connect state to REACHABLE
         self.adapter_agent.update_child_devices_state(self.device_id,
@@ -1216,7 +1202,7 @@
         # Get the latest device reference
         device = self.adapter_agent.get_device(self.device_id)
         device.reason = 'Deleting'
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
 
         # self.adapter_agent.unregister_for_onu_detect_state(self.device.id)
 
@@ -1327,6 +1313,7 @@
         if active and self.netconf_client is None or not self.netconf_client.connected:
             self.make_netconf_connection(close_existing_client=True)
 
+    @inlineCallbacks
     def heartbeat_check_status(self, _):
         """
         Check the number of heartbeat failures against the limit and emit an alarm if needed
@@ -1342,7 +1329,7 @@
                     device.connect_status = ConnectStatus.UNREACHABLE
                     device.oper_status = OperStatus.FAILED
                     device.reason = self.heartbeat_last_reason
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
                     HeartbeatAlarm(self.alarms, 'olt', self.heartbeat_miss).raise_alarm()
                     self.on_heatbeat_alarm(True)
             else:
@@ -1351,7 +1338,7 @@
                     device.connect_status = ConnectStatus.REACHABLE
                     device.oper_status = OperStatus.ACTIVE
                     device.reason = ''
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
                     HeartbeatAlarm(self.alarms, 'olt').clear_alarm()
                     self.on_heatbeat_alarm(False)
 
diff --git a/adapters/adtran_olt/adtran_olt.yml b/adapters/adtran_olt/adapters-adtran-olt.yml
similarity index 100%
rename from adapters/adtran_olt/adtran_olt.yml
rename to adapters/adtran_olt/adapters-adtran-olt.yml
diff --git a/adapters/adtran_olt/adtran_olt.py b/adapters/adtran_olt/adtran_olt.py
index c052b78..e25b42a 100644
--- a/adapters/adtran_olt/adtran_olt.py
+++ b/adapters/adtran_olt/adtran_olt.py
@@ -22,7 +22,7 @@
 from pyvoltha.adapters.iadapter import OltAdapter
 from pyvoltha.protos import third_party
 from pyvoltha.protos.common_pb2 import AdminState
-
+from pyvoltha.protos.health_pb2 import HealthStatus
 from adtran_olt_handler import AdtranOltHandler
 
 
@@ -43,7 +43,7 @@
                                                version='2.0.0',
                                                device_type=AdtranOltAdapter.name,
                                                accepts_bulk_flow_update=True,
-                                               accepts_add_remove_flow_updates=False)  # TODO: Implement me
+                                               accepts_add_remove_flow_updates=True)
 
         log.debug('adtran_olt.__init__')
 
@@ -55,7 +55,6 @@
         """
         # TODO: Currently this is always healthy for every adapter.
         #       If we decide not to modify this, delete this method and use base class method
-        from pyvoltha.protos.health_pb2 import HealthStatus
         return HealthStatus(state=HealthStatus.HEALTHY)
 
     def abandon_device(self, device):
@@ -75,8 +74,9 @@
         is provisioned top-down and needs to be activated by the adapter.
 
         :param device: A voltha.Device object, with possible device-type
-                specific extensions. Such extensions shall be described as part of
-                the device type specification returned by device_types().
+                       specific extensions. Such extensions shall be described as part of
+                       the device type specification returned by device_types().
+
         :return: (Deferred) Shall be fired to acknowledge device ownership.
         """
         log.info('adopt-device', device=device)
@@ -84,23 +84,36 @@
             'adapter': self,
             'device-id': device.id
         }
-        self.devices_handlers[device.id] = self.device_handler_class(**kwargs)
-        d = defer.Deferred()
-        reactor.callLater(0, self.devices_handlers[device.id].activate, d, False)
-        return d
+        try:
+            self.devices_handlers[device.id] = self.device_handler_class(**kwargs)
+            d = defer.Deferred()
+            reactor.callLater(0, self.devices_handlers[device.id].activate, d, False)
+            return d
+
+        except Exception as _e:
+            raise
 
     def reconcile_device(self, device):
+        """
+        Make sure the adapter looks after given device. Called when this
+        device has changed ownership from another Voltha instance to
+        this one (typically, this occurs when the previous voltha
+        instance went down).
+
+        :param device: A voltha.Device object, with possible device-type
+                       specific extensions. Such extensions shall be described as part of
+                       the device type specification returned by device_types().
+
+        :return: (Deferred) Shall be fired to acknowledge device ownership.
+        """
         try:
-            self.devices_handlers[device.id] = self.device_handler_class(self,
-                                                                         device.id)
+            kwargs = {
+                'adapter': self,
+                'device-id': device.id
+            }
+            self.devices_handlers[device.id] = self.device_handler_class(**kwargs)
             # Work only required for devices that are in ENABLED state
             if device.admin_state == AdminState.ENABLED:
-
-                kwargs = {
-                    'adapter': self,
-                    'device-id': device.id
-                }
-                self.devices_handlers[device.id] =self.device_handler_class(**kwargs)
                 d = defer.Deferred()
                 reactor.callLater(0, self.devices_handlers[device.id].activate, d, True)
 
diff --git a/adapters/adtran_olt/adtran_olt_handler.py b/adapters/adtran_olt/adtran_olt_handler.py
index ad32b84..92c3398 100644
--- a/adapters/adtran_olt/adtran_olt_handler.py
+++ b/adapters/adtran_olt/adtran_olt_handler.py
@@ -160,9 +160,6 @@
             'startup-revision': 'unknown',
             'software-images': []
         }
-        if self.is_virtual_olt:
-            returnValue(device)
-
         try:
             pe_state = PhysicalEntitiesState(self.netconf_client)
             self.startup = pe_state.get_state()
@@ -322,12 +319,8 @@
             from nni_port import MockNniPort
 
             ietf_interfaces = IetfInterfacesState(self.netconf_client)
-
-            if self.is_virtual_olt:
-                results = MockNniPort.get_nni_port_state_results()
-            else:
-                self.startup = ietf_interfaces.get_state()
-                results = yield self.startup
+            self.startup = ietf_interfaces.get_state()
+            results = yield self.startup
 
             ports = ietf_interfaces.get_port_entries(results, 'ethernet')
             returnValue(ports)
@@ -355,10 +348,9 @@
             # May already exist if device was not fully reachable when first enabled
             if port_no not in self.northbound_ports:
                 self.log.info('processing-nni', port_no=port_no, name=port['port_no'])
-                self.northbound_ports[port_no] = NniPort(self, **port) if not self.is_virtual_olt \
-                    else MockNniPort(self, **port)
+                self.northbound_ports[port_no] = NniPort(self, **port)
 
-            if len(self.northbound_ports) >= self.max_nni_ports: # TODO: For now, limit number of NNI ports to make debugging easier
+            if len(self.northbound_ports) >= self.max_nni_ports:  # TODO: For now, limit number of NNI ports to make debugging easier
                 break
 
         self.num_northbound_ports = len(self.northbound_ports)
@@ -407,12 +399,8 @@
             results = yield self.startup
 
             ietf_interfaces = IetfInterfacesState(self.netconf_client)
-
-            if self.is_virtual_olt:
-                nc_results = MockNniPort.get_pon_port_state_results()
-            else:
-                self.startup = ietf_interfaces.get_state()
-                nc_results = yield self.startup
+            self.startup = ietf_interfaces.get_state()
+            nc_results = yield self.startup
 
             ports = ietf_interfaces.get_port_entries(nc_results, 'xpon')
             if len(ports) == 0:
@@ -692,7 +680,7 @@
         # Upstream direction?
         if self.is_pon_port(port_no):
             #TODO: Validate the evc-map name
-            from python.adapters.adtran.adtran_common.flow.evc_map import EVCMap
+            from pyvoltha.adapters.adtran_common.flow.evc_map import EVCMap
             map_info = EVCMap.decode_evc_map_name(evc_map)
             logical_port_no = int(map_info.get('ingress-port'))
 
@@ -1162,6 +1150,7 @@
 
         self.adapter_agent.update_image_download(request)
 
+    @inlineCallbacks
     def start_download(self, device, request, done):
         """
         This is called to request downloading a specified image into
@@ -1193,7 +1182,7 @@
                 self._downloads[download.name] = download
                 self._update_download_status(request, download)
                 done.callback('started')
-                return done
+                returnValue(done)
 
             except Exception:
                 request.additional_info = 'Download request startup failed due to exception'
@@ -1214,9 +1203,10 @@
 
             # restore admin state to enabled
             device.admin_state = AdminState.ENABLED
-            self.adapter_agent.update_device(device)
+            yield self.adapter_agent.device_update(device)
             raise
 
+    @inlineCallbacks
     def download_status(self, device, request, done):
         """
         This is called to inquire about a requested image download status based
@@ -1241,11 +1231,12 @@
                                  ImageDownload.DOWNLOAD_FAILED]:
             # restore admin state to enabled
             device.admin_state = AdminState.ENABLED
-            self.adapter_agent.update_device(device)
+            yield self.adapter_agent.device_update(device)
 
         done.callback(request.state)
-        return done
+        returnValue(done)
 
+    @inlineCallbacks
     def cancel_download(self, device, request, done):
         """
         This is called to cancel a requested image download based on a NBI
@@ -1273,10 +1264,11 @@
 
         if device.admin_state == AdminState.DOWNLOADING_IMAGE:
             device.admin_state = AdminState.ENABLED
-            self.adapter_agent.update_device(device)
+            yield self.adapter_agent.device_update(device)
 
-        return done
+        returnValue(done)
 
+    @inlineCallbacks
     def activate_image(self, device, request, done):
         """
         This is called to activate a downloaded image from a standby partition
@@ -1306,9 +1298,10 @@
 
         # restore admin state to enabled
         device.admin_state = AdminState.ENABLED
-        self.adapter_agent.update_device(device)
-        return done
+        yield self.adapter_agent.device_update(device)
+        returnValue(done)
 
+    @inlineCallbacks
     def revert_image(self, device, request, done):
         """
         This is called to deactivate the specified image at active partition,
@@ -1338,8 +1331,8 @@
 
         # restore admin state to enabled
         device.admin_state = AdminState.ENABLED
-        self.adapter_agent.update_device(device)
-        return done
+        yield self.adapter_agent.device_update(device)
+        returnValue(done)
 
     def add_onu_device(self, pon_id, onu_id, serial_number):
         onu_device = self.adapter_agent.get_child_device(self.device_id,
diff --git a/adapters/adtran_olt/main.py b/adapters/adtran_olt/main.py
index 07bcc07..bea91bf 100755
--- a/adapters/adtran_olt/main.py
+++ b/adapters/adtran_olt/main.py
@@ -49,9 +49,9 @@
 _ = third_party
 
 
-defs = dict(
-    version_file='./VERSION',
-    config=os.environ.get('CONFIG', './adapters-adtran_olt.yml'),
+defs=dict(
+    version_file='/voltha/VERSION',
+    config=os.environ.get('CONFIG', './adapters-adtran-olt.yml'),
     container_name_regex=os.environ.get('CONTAINER_NUMBER_EXTRACTOR', '^.*\.(['
                                                                       '0-9]+)\..*$'),
     consul=os.environ.get('CONSUL', 'localhost:8500'),
@@ -64,8 +64,8 @@
     core_topic=os.environ.get('CORE_TOPIC', 'rwcore'),
     interface=os.environ.get('INTERFACE', get_my_primary_interface()),
     instance_id=os.environ.get('INSTANCE_ID', os.environ.get('HOSTNAME', '1')),
-    kafka_adapter=os.environ.get('KAFKA_ADAPTER', '172.20.10.3:9092'),
-    kafka_cluster=os.environ.get('KAFKA_CLUSTER', '172.20.10.3:9092'),
+    kafka_adapter=os.environ.get('KAFKA_ADAPTER', '192.168.0.20:9092'),
+    kafka_cluster=os.environ.get('KAFKA_CLUSTER', '10.100.198.220:9092'),
     backend=os.environ.get('BACKEND', 'none'),
     retry_interval=os.environ.get('RETRY_INTERVAL', 2),
     heartbeat_topic=os.environ.get('HEARTBEAT_TOPIC', "adapters.heartbeat"),
@@ -74,7 +74,7 @@
     debug_enabled=True,
     debug_host='work.bcsw.net',
     # debug_host='10.0.2.15',
-    debug_port=8765,
+    debug_port=5678,
 )
 
 
@@ -286,7 +286,7 @@
     except:
         import sys
         logger.error("pydevd startup exception: %s" % sys.exc_info()[0])
-        print('REMOTE DEBUGGING will not be supported in this run...')
+        logger.error('REMOTE DEBUGGING will not be supported in this run...')
 
 
 def load_config(args):
@@ -295,6 +295,7 @@
         dir = os.path.dirname(os.path.abspath(__file__))
         path = os.path.join(dir, path)
     path = os.path.abspath(path)
+
     with open(path) as fd:
         config = yaml.load(fd)
     return config
@@ -308,6 +309,7 @@
     log.info("  / ____ \| |__| | | |  | | \ \  / ____ \| |\  |   | |__| | |____| |   ")
     log.info(" /_/    \_\_____/  |_|  |_| _\_\/_/    \_\_| \_|    \____/|______|_|   ")
     log.info("     /\      | |           | |                                         ")
+    log.info('   _       _             _                            ')
     log.info("    /  \   __| | __ _ _ __ | |_ ___ _ __                               ")
     log.info("   / /\ \ / _` |/ _` | '_ \| __/ _ \ '__|                              ")
     log.info("  / ____ \ (_| | (_| | |_) | ||  __/ |                                 ")
@@ -322,42 +324,45 @@
 class Main(object):
 
     def __init__(self):
+        try:
+            self.args = args = parse_args()
+            self.config = load_config(args)
 
-        self.args = args = parse_args()
-        self.config = load_config(args)
+            verbosity_adjust = (args.verbose or 0) - (args.quiet or 0)
+            self.log = setup_logging(self.config.get('logging', {}),
+                                     args.instance_id,
+                                     verbosity_adjust=verbosity_adjust)
+            self.log.info('container-number-extractor',
+                          regex=args.container_name_regex)
 
-        verbosity_adjust = (args.verbose or 0) - (args.quiet or 0)
-        self.log = setup_logging(self.config.get('logging', {}),
-                                 args.instance_id,
-                                 verbosity_adjust=verbosity_adjust)
-        self.log.info('container-number-extractor',
-                      regex=args.container_name_regex)
+            if args.debug_enabled:
+                setup_remote_debug(args.debug_host, args.debug_port, self.log)
 
-        if args.debug_enabled:
-            setup_remote_debug(args.debug_host, args.debug_port, self.log)
+            self.adtran_olt_adapter_version = self.get_version()
+            self.log.info('ADTRAN-OLT-Adapter-Version',
+                          version=self.adtran_olt_adapter_version)
 
-        self.adtran_olt_adapter_version = self.get_version()
-        self.log.info('ADTRAN-OLT-Adapter-Version', version=self.adtran_olt_adapter_version)
+            if not args.no_banner:
+                print_banner(self.log)
 
-        if not args.no_banner:
-            print_banner(self.log)
+            self.adapter = None
 
-        self.adapter = None
-        self.core_proxy = None
-        self.adapter_proxy = None
+            # Create a unique instance id using the passed-in instance id and
+            # UTC timestamp
+            current_time = arrow.utcnow().timestamp
+            self.instance_id = self.args.instance_id + '_' + str(current_time)
 
-        # Create a unique instance id using the passed-in instance id and
-        # UTC timestamp
-        current_time = arrow.utcnow().timestamp
-        self.instance_id = self.args.instance_id + '_' + str(current_time)
+            self.core_topic = args.core_topic
+            self.listening_topic = args.name
+            self.startup_components()
 
-        self.core_topic = args.core_topic
-        self.listening_topic = args.name
-        self.startup_components()
+            if not args.no_heartbeat:
+                self.start_heartbeat()
+                self.start_kafka_cluster_heartbeat(self.instance_id)
 
-        if not args.no_heartbeat:
-            self.start_heartbeat()
-            self.start_kafka_cluster_heartbeat(self.instance_id)
+        except Exception as e:
+            self.log.exception('unhandled-exception', e=e)
+            raise
 
     def get_version(self):
         path = defs['version_file']
@@ -442,6 +447,7 @@
                     kv_store=self.args.backend,
                     default_topic=self.args.name,
                     group_id_prefix=self.args.instance_id,
+                    # Needs to assign a real class
                     target_cls=adtran_request_handler
                 )
             ).start()
@@ -543,8 +549,8 @@
                     kafka_cluster_proxy.send_message(topic, dumps(message))
                 else:
                     self.log.error('kafka-proxy-unavailable')
-            except Exception, err:
-                self.log.exception('failed-sending-message-heartbeat', e=err)
+            except Exception as e:
+                self.log.exception('failed-sending-message-heartbeat', e=e)
 
         try:
             t0 = time.time()
diff --git a/adapters/adtran_olt/resources/adtran_olt_resource_manager.py b/adapters/adtran_olt/resources/adtran_olt_resource_manager.py
index caf5a46..8cce46c 100644
--- a/adapters/adtran_olt/resources/adtran_olt_resource_manager.py
+++ b/adapters/adtran_olt/resources/adtran_olt_resource_manager.py
@@ -245,7 +245,7 @@
             # we need to derive the ONU Id for which the packet arrived based
             # on the pon_intf and gemport available in the packet_indication
             # self.kv_store[str(pon_intf_gemport)] = ' '.join(map(str, (onu_id, uni_id)))
-            self.kv_store.put(self._make_path(str(pon_intf_gemport)), ' '.join(map(str, (onu_id, uni_id)))
+            self.kv_store.put(self._make_path(str(pon_intf_gemport)), ' '.join(map(str, (onu_id, uni_id))))
 
     def get_onu_uni_from_ponport_gemport(self, pon_port, gemport):
         pon_intf_gemport = (pon_port, gemport)
diff --git a/adapters/adtran_onu/adtran_onu.yml b/adapters/adtran_onu/adapters-adtran-onu.yml
similarity index 100%
rename from adapters/adtran_onu/adtran_onu.yml
rename to adapters/adtran_onu/adapters-adtran-onu.yml
diff --git a/adapters/adtran_onu/adtran_onu.py b/adapters/adtran_onu/adtran_onu.py
index c0801d1..7a88ed0 100755
--- a/adapters/adtran_onu/adtran_onu.py
+++ b/adapters/adtran_onu/adtran_onu.py
@@ -61,7 +61,7 @@
         self.adtran_omci['omci-capabilities']['tasks']['get-capabilities'] = AdtnCapabilitiesTask
         # TODO: Continue to customize adtran_omci here as needed
 
-        self._omci_agent = OpenOMCIAgent(self.adapter_agent.core,
+        self._omci_agent = OpenOMCIAgent(self.adapter_agent,
                                          support_classes=self.adtran_omci)
 
     @property
diff --git a/adapters/adtran_onu/adtran_onu_handler.py b/adapters/adtran_onu/adtran_onu_handler.py
index 61fdc2c..beed980 100644
--- a/adapters/adtran_onu/adtran_onu_handler.py
+++ b/adapters/adtran_onu/adtran_onu_handler.py
@@ -212,9 +212,8 @@
             assert device.parent_id, 'Invalid Parent ID'
             assert device.proxy_address.device_id, 'Invalid Device ID'
 
-            # register for proxied messages right away
+            # Cache our proxy address
             self.proxy_address = device.proxy_address
-            self.adapter_agent.register_for_proxied_messages(device.proxy_address)
 
             # initialize device info
             device.root = False
@@ -244,7 +243,7 @@
             parent_device = self.adapter_agent.get_device(device.parent_id)
 
             self.logical_device_id = parent_device.parent_id
-            self.adapter_agent.update_device(device)
+            yield self.adapter_agent.device_update(device)
 
             ############################################################################
             # Setup PM configuration for this device
@@ -260,7 +259,7 @@
             pm_config = self.pm_metrics.make_proto()
             self.openomci.set_pm_config(self.pm_metrics.omci_pm.openomci_interval_pm)
             self.log.info("initial-pm-config", pm_config=pm_config)
-            self.adapter_agent.update_device_pm_config(pm_config, init=True)
+            yield self.adapter_agent.device_pm_config_update(pm_config, init=True)
 
             ############################################################################
             # Setup Alarm handler
@@ -276,8 +275,9 @@
             device.reason = 'Failed to activate: {}'.format(e.message)
             device.connect_status = ConnectStatus.UNREACHABLE
             device.oper_status = OperStatus.FAILED
-            self.adapter_agent.update_device(device)
+            self.adapter_agent.device_update(device)
 
+    @inlineCallbacks
     def reconcile(self, device):
         self.log.info('reconciling-ONU-device-starts')
 
@@ -294,19 +294,15 @@
         # Register for adapter messages
         self.adapter_agent.register_for_inter_adapter_messages()
 
-        # Set the connection status to REACHABLE
-        device.connect_status = ConnectStatus.REACHABLE
-        self.adapter_agent.update_device(device)
-        self.enabled = True
-
         # TODO: Verify that the uni, pon and logical ports exists
+        self.enabled = True
 
         # Mark the device as REACHABLE and ACTIVE
         device = self.adapter_agent.get_device(device.id)
         device.connect_status = ConnectStatus.REACHABLE
         device.oper_status = OperStatus.ACTIVE
         device.reason = ''
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
 
         self.log.info('reconciling-ONU-device-ends')
 
@@ -425,6 +421,7 @@
         downstream = tp['downstream_gem_port_attribute_list']
         self._create_gemports(upstream, downstream, tcont, uni_id, tech_profile_id)
 
+    @inlineCallbacks
     def load_and_configure_tech_profile(self, uni_id, tp_path):
         self.log.debug("loading-tech-profile-configuration", uni_id=uni_id, tp_path=tp_path)
 
@@ -442,7 +439,7 @@
                 if tp_path in self._tp_service_specific_task[uni_id]:
                     self.log.info("tech-profile-config-already-in-progress",
                                   tp_path=tp_path)
-                    return
+                    returnValue('already-in-progress')
 
                 tp = self.kv_client[tp_path]
                 tp = ast.literal_eval(tp)
@@ -455,7 +452,7 @@
                     self.log.info("tech-profile-config-done-successfully")
                     device = self.adapter_agent.get_device(self.device_id)
                     device.reason = ''
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
 
                     if tp_path in self._tp_service_specific_task[uni_id]:
                         del self._tp_service_specific_task[uni_id][tp_path]
@@ -466,7 +463,7 @@
                     self.log.warn('tech-profile-config-failure-retrying', reason=_reason)
                     device = self.adapter_agent.get_device(self.device_id)
                     device.reason = 'Tech Profile config failed-retrying'
-                    self.adapter_agent.update_device(device)
+                    yield self.adapter_agent.device_update(device)
 
                     if tp_path in self._tp_service_specific_task[uni_id]:
                         del self._tp_service_specific_task[uni_id][tp_path]
@@ -601,7 +598,7 @@
         device.oper_status = OperStatus.ACTIVATING
         device.connect_status = ConnectStatus.UNREACHABLE
         device.reason = 'Attempting reboot'
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
 
         # TODO: send alert and clear alert after the reboot
         try:
@@ -617,10 +614,10 @@
         # Go ahead and pause less than that and start to look
         # for it being alive
         device.reason = 'reboot in progress'
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
 
         # Disable OpenOMCI
-        self.omci.enabled = False
+        self.openomci.enabled = False
         self._deferred = reactor.callLater(_ONU_REBOOT_MIN,
                                            self._finish_reboot,
                                            previous_oper_status,
@@ -631,13 +628,13 @@
     def _finish_reboot(self, previous_oper_status, previous_conn_status,
                        reregister):
         # Restart OpenOMCI
-        self.omci.enabled = True
+        self.openomci.enabled = True
 
         device = self.adapter_agent.get_device(self.device_id)
         device.oper_status = previous_oper_status
         device.connect_status = previous_conn_status
         device.reason = ''
-        self.adapter_agent.update_device(device)
+        yield self.adapter_agent.device_update(device)
 
         if reregister:
             self.adapter_agent.register_for_inter_adapter_messages()
@@ -655,6 +652,7 @@
         # TODO: Support self test?
         return SelfTestResponse(result=SelfTestResponse.NOT_SUPPORTED)
 
+    @inlineCallbacks
     def disable(self):
         self.log.info('disabling', device_id=self.device_id)
         try:
@@ -668,7 +666,7 @@
             device.oper_status = OperStatus.UNKNOWN
             device.connect_status = ConnectStatus.UNREACHABLE
             device.reason = 'Disabled'
-            self.adapter_agent.update_device(device)
+            yield self.adapter_agent.device_update(device)
 
             # Remove the uni logical port from the OLT, if still present
             parent_device = self.adapter_agent.get_device(device.parent_id)
@@ -702,6 +700,7 @@
         self.enabled = False
         self.log.info('disabled')
 
+    @inlineCallbacks
     def reenable(self):
         self.log.info('re-enabling', device_id=self.device_id)
         try:
@@ -729,7 +728,7 @@
                                                                 self._pon.get_port())
             # Update the connect status to REACHABLE
             device.connect_status = ConnectStatus.REACHABLE
-            self.adapter_agent.update_device(device)
+            yield self.adapter_agent.device_update(device)
 
             # re-add uni port to logical device
             parent_device = self.adapter_agent.get_device(device.parent_id)
@@ -748,7 +747,7 @@
             device.reason = ''
 
             self.enabled = True
-            self.adapter_agent.update_device(device)
+            yield self.adapter_agent.device_update(device)
 
             self.log.info('re-enabled')
 
@@ -773,6 +772,7 @@
         omci, self._openomci = self._openomci, None
         omci.delete()
 
+    @inlineCallbacks
     def add_uni_ports(self):
         """ Called after in-sync achieved and not in xPON mode"""
         # TODO: We have to methods adding UNI ports.  Go to one
@@ -823,7 +823,7 @@
                 pon_port.peers[d[0]].port_no = uni_port.port_number
                 self.adapter_agent.add_port_reference_to_parent(self.device_id,
                                                                 pon_port)
-            self.adapter_agent.update_device(device)
+            yield self.adapter_agent.device_update(device)
             uni_port.enabled = True
             uni_id += 1
 
diff --git a/adapters/adtran_onu/main.py b/adapters/adtran_onu/main.py
index e19f320..8bff56d 100755
--- a/adapters/adtran_onu/main.py
+++ b/adapters/adtran_onu/main.py
@@ -45,13 +45,14 @@
 from adtran_onu import AdtranOnuAdapter
 from pyvoltha.protos import third_party
 from pyvoltha.protos.adapter_pb2 import AdapterConfig
+import sys
 
 _ = third_party
 
 
-defs = dict(
-    version_file='./VERSION',
-    config=os.environ.get('CONFIG', './adapters-adtran_onu.yml'),
+defs=dict(
+    version_file='/voltha/VERSION',
+    config=os.environ.get('CONFIG', './adapters-adtran-onu.yml'),
     container_name_regex=os.environ.get('CONTAINER_NUMBER_EXTRACTOR', '^.*\.(['
                                                                       '0-9]+)\..*$'),
     consul=os.environ.get('CONSUL', 'localhost:8500'),
@@ -74,7 +75,7 @@
     debug_enabled=True,
     debug_host='work.bcsw.net',
     # debug_host='10.0.2.15',
-    debug_port=5678,
+    debug_port=8765,
 )
 
 
@@ -322,42 +323,48 @@
 class Main(object):
 
     def __init__(self):
+        try:
+            self.args = args = parse_args()
+            self.config = load_config(args)
 
-        self.args = args = parse_args()
-        self.config = load_config(args)
+            verbosity_adjust = (args.verbose or 0) - (args.quiet or 0)
+            self.log = setup_logging(self.config.get('logging', {}),
+                                     args.instance_id,
+                                     verbosity_adjust=verbosity_adjust)
+            self.log.info('container-number-extractor',
+                          regex=args.container_name_regex)
 
-        verbosity_adjust = (args.verbose or 0) - (args.quiet or 0)
-        self.log = setup_logging(self.config.get('logging', {}),
-                                 args.instance_id,
-                                 verbosity_adjust=verbosity_adjust)
-        self.log.info('container-number-extractor',
-                      regex=args.container_name_regex)
+            if args.debug_enabled:
+                setup_remote_debug(args.debug_host, args.debug_port, self.log)
 
-        if args.debug_enabled:
-            setup_remote_debug(args.debug_host, args.debug_port, self.log)
+            self.adtran_onu_adapter_version = self.get_version()
+            self.log.info('ADTRAN-ONU-Adapter-Version', version=self.adtran_onu_adapter_version)
 
-        self.adtran_onu_adapter_version = self.get_version()
-        self.log.info('ADTRAN-ONU-Adapter-Version', version=self.adtran_onu_adapter_version)
+            if not args.no_banner:
+                print_banner(self.log)
 
-        if not args.no_banner:
-            print_banner(self.log)
+            self.adapter = None
+            self.core_proxy = None
+            self.adapter_proxy = None
 
-        self.adapter = None
-        self.core_proxy = None
-        self.adapter_proxy = None
+            # Create a unique instance id using the passed-in instance id and
+            # UTC timestamp
+            current_time = arrow.utcnow().timestamp
+            self.instance_id = self.args.instance_id + '_' + str(current_time)
 
-        # Create a unique instance id using the passed-in instance id and
-        # UTC timestamp
-        current_time = arrow.utcnow().timestamp
-        self.instance_id = self.args.instance_id + '_' + str(current_time)
+            self.core_topic = args.core_topic
+            self.listening_topic = args.name
+            self.startup_components()
 
-        self.core_topic = args.core_topic
-        self.listening_topic = args.name
-        self.startup_components()
+            if not args.no_heartbeat:
+                self.start_heartbeat()
+                self.start_kafka_cluster_heartbeat(self.instance_id)
 
-        if not args.no_heartbeat:
-            self.start_heartbeat()
-            self.start_kafka_cluster_heartbeat(self.instance_id)
+        except Exception as e:
+            self.log.exception('unhandled-exception', e=e)
+            for tick in xrange(0, 30):
+                import time
+                time.sleep(2)
 
     def get_version(self):
         path = defs['version_file']
diff --git a/adapters/adtran_onu/pon_port.py b/adapters/adtran_onu/pon_port.py
index fd0d0c1..c89815f 100644
--- a/adapters/adtran_onu/pon_port.py
+++ b/adapters/adtran_onu/pon_port.py
@@ -150,6 +150,7 @@
                                                    port_no=device.parent_port_no)])
         return self._port
 
+    @inlineCallbacks
     def _update_adapter_agent(self):
         """
         Update the port status and state in the core
@@ -161,11 +162,13 @@
             self._port.admin_state = self._admin_state
             self._port.oper_status = self._oper_status
 
-        # adapter_agent add_port also does an update of port status
-        try:
-            self._handler.adapter_agent.add_port(self._handler.device_id, self.get_port())
-        except Exception as e:
-            self.log.exception('update-port', e=e)
+            try:
+                yield self._handler.adapter_agent.port_state_update(self._handler.device_id,
+                                                                    self._port.type,
+                                                                    self._port.port_no,
+                                                                    self._port.oper_status)
+            except Exception as e:
+                self.log.exception('update-port', e=e)
 
     def add_tcont(self, tcont, reflow=False):
         """
diff --git a/adapters/adtran_onu/uni_port.py b/adapters/adtran_onu/uni_port.py
index 96cd353..b829499 100644
--- a/adapters/adtran_onu/uni_port.py
+++ b/adapters/adtran_onu/uni_port.py
@@ -19,7 +19,8 @@
 from pyvoltha.protos.logical_device_pb2 import LogicalPort
 from pyvoltha.protos.openflow_13_pb2 import OFPPS_LIVE, OFPPF_FIBER
 from pyvoltha.protos.openflow_13_pb2 import ofp_port
-import adtran_olt.resources.adtranolt_platform as platform
+# import adtran_olt.resources.adtranolt_platform as platform
+from twisted.internet.defer import inlineCallbacks
 
 
 class UniPort(object):
@@ -136,6 +137,7 @@
         """
         return self._logical_port_number
 
+    @inlineCallbacks
     def _update_adapter_agent(self):
         """
         Update the port status and state in the core
@@ -147,14 +149,16 @@
             self._port.admin_state = self._admin_state
             self._port.oper_status = self._oper_status
 
-        try:
-            # adapter_agent add_port also does an update of existing port
-            self._handler.adapter_agent.add_port(self._handler.device_id,
-                                                 self.get_port())
-        except KeyError:  # Expected exception during ONU disabling
-            pass
-        except Exception as e:  # Expected exception during ONU disabling
-            self.log.exception('update-port', e=e)
+            try:
+                yield self._handler.adapter_agent.port_state_update(self._handler.device_id,
+                                                                    self._port.type,
+                                                                    self._port.port_no,
+                                                                    self._port.oper_status)
+
+            except KeyError:  # Expected exception during ONU disabling
+                pass
+            except Exception as e:  # Expected exception during ONU disabling
+                self.log.exception('update-port', e=e)
 
     def get_port(self):
         """
diff --git a/compose/adapters-adtran_olt.yml b/compose/adapters-adtran-olt.yml
similarity index 77%
rename from compose/adapters-adtran_olt.yml
rename to compose/adapters-adtran-olt.yml
index 6489c20..3dedce2 100644
--- a/compose/adapters-adtran_olt.yml
+++ b/compose/adapters-adtran-olt.yml
@@ -15,24 +15,21 @@
 
 version: '2'
 services:
-  ponsim_olt:
+  adtran_olt:
     image: "${REGISTRY}${REPOSITORY}voltha-adapter-adtran-olt${TAG}"
     logging:
       driver: "json-file"
       options:
         max-size: "10m"
         max-file: "3"
-    entrypoint:
-      - /app/adtran_olt
-      - -device_type
-      - "OLT"
-      - -internal_if
-      - "eth0"
-      - -external_if
-      - "eth0"
-      - -verbose
-    ports:
-      - "50060:50060"
+    command: [
+      "/voltha/adapters/adtran_olt/main.py",
+      "-v",
+      "--name=adtran_olt",
+      "--kafka_adapter=${DOCKER_HOST_IP}:9092",
+      "--kafka_cluster=${DOCKER_HOST_IP}:9092",
+      "--core_topic=rwcore"
+    ]
     networks:
       - default
 
diff --git a/compose/adapters-adtran_onu.yml b/compose/adapters-adtran-onu.yml
similarity index 77%
rename from compose/adapters-adtran_onu.yml
rename to compose/adapters-adtran-onu.yml
index 839d206..ca58676 100644
--- a/compose/adapters-adtran_onu.yml
+++ b/compose/adapters-adtran-onu.yml
@@ -15,24 +15,21 @@
 
 version: '2'
 services:
-  ponsim_onu:
+  adtran_onu:
     image: "${REGISTRY}${REPOSITORY}voltha-adapter-adtran-onu${TAG}"
     logging:
       driver: "json-file"
       options:
         max-size: "10m"
         max-file: "3"
-    entrypoint:
-      - /app/adtran_onu
-      - -device_type
-      - "OLT"
-      - -internal_if
-      - "eth0"
-      - -external_if
-      - "eth0"
-      - -verbose
-    ports:
-      - "50061:50061"
+    command: [
+      "/voltha/adapters/adtran_onu/main.py",
+      "-v",
+      "--name=adtran_onu",
+      "--kafka_adapter=${DOCKER_HOST_IP}:9092",
+      "--kafka_cluster=${DOCKER_HOST_IP}:9092",
+      "--core_topic=rwcore"
+    ]
     networks:
       - default
 
diff --git a/compose/system-test.yml b/compose/system-test.yml
new file mode 100644
index 0000000..80646c9
--- /dev/null
+++ b/compose/system-test.yml
@@ -0,0 +1,180 @@
+---
+# Copyright 2018 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.
+
+version: '2'
+
+networks:
+  default:
+    driver: bridge
+
+services:
+
+  zookeeper:
+    image: "wurstmeister/zookeeper:latest"
+    environment:
+      SERVICE_2181_NAME: "zookeeper"
+    ports:
+    - 2181:2181
+    networks:
+    - default
+
+
+  kafka:
+    image: "wurstmeister/kafka:2.11-2.0.1"
+    environment:
+      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://${DOCKER_HOST_IP}:9092
+      KAFKA_LISTENERS: PLAINTEXT://:9092
+      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+      KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
+      SERVICE_9092_NAME: "kafka"
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock
+    ports:
+     - 9092:9092
+    networks:
+    - default
+
+
+  etcd:
+    image: "quay.io/coreos/etcd:v3.2.9"
+    command: [
+      "etcd",
+      "--name=etcd0",
+      "--advertise-client-urls=http://${DOCKER_HOST_IP}:2379,http://${DOCKER_HOST_IP}:4001",
+      "--listen-client-urls=http://0.0.0.0:2379,http://0.0.0.0:4001",
+      "--initial-advertise-peer-urls=http://${DOCKER_HOST_IP}:2380",
+      "--listen-peer-urls=http://0.0.0.0:2380",
+      "--initial-cluster-token=etcd-cluster-1",
+      "--initial-cluster=etcd0=http://${DOCKER_HOST_IP}:2380",
+      "--initial-cluster-state=new"
+    ]
+    ports:
+    - "2379:2379"
+    - 2380
+    - 4001
+    networks:
+    - default
+
+
+  rw_core:
+    image: voltha-rw-core
+    entrypoint:
+        - /app/rw_core
+        - -kv_store_type=etcd
+        - -kv_store_host=${DOCKER_HOST_IP}
+        - -kv_store_port=2379
+        - -grpc_port=50057
+        - -banner=true
+        - -kafka_adapter_host=${DOCKER_HOST_IP}
+        - -kafka_adapter_port=9092
+        - -kafka_cluster_host=${DOCKER_HOST_IP}
+        - -kafka_cluster_port=9092
+        - -rw_core_topic=rwcore
+        - -kv_store_data_prefix=service/voltha
+        - -in_competing_mode=false
+        - -log_level=0
+    volumes:
+    - "/var/run/docker.sock:/tmp/docker.sock"
+    ports:
+      - 50057:50057
+    networks:
+    - default
+    restart: unless-stopped
+
+
+  cli:
+    image: "${REGISTRY}${REPOSITORY}voltha-cli:latest"
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "10m"
+        max-file: "3"
+    environment:
+      DOCKER_HOST_IP: "${DOCKER_HOST_IP}"
+    entrypoint:
+    - /voltha/python/cli/setup.sh
+    - -g ${DOCKER_HOST_IP}:50057
+    networks:
+    - default
+    ports:
+    - "5022:22"
+
+
+  ofagent:
+    image: "${REGISTRY}${REPOSITORY}voltha-ofagent:latest"
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "10m"
+        max-file: "3"
+    command: [
+      "/ofagent/ofagent/main.py",
+      "--consul=${DOCKER_HOST_IP}:8500",
+      "--controller=${DOCKER_HOST_IP}:6653",
+      "--grpc-endpoint=${DOCKER_HOST_IP}:50057",
+      "--instance-id-is-container-name",
+      "-v"
+    ]
+    volumes:
+    - "/var/run/docker.sock:/tmp/docker.sock"
+    networks:
+    - default
+    restart: unless-stopped
+
+
+  adapter_adtran_olt:
+    image: "${REGISTRY}${REPOSITORY}voltha-adapter-adtran-olt${TAG}"
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "10m"
+        max-file: "3"
+    command: [
+      "/voltha/adapters/adtran_olt/main.py",
+      "-v",
+      "--name=adtran_olt",
+      "--kafka_adapter=${DOCKER_HOST_IP}:9092",
+      "--kafka_cluster=${DOCKER_HOST_IP}:9092",
+      "--backend=etcd",
+      "--etcd=${DOCKER_HOST_IP}:2379",
+      "--core_topic=rwcore"
+    ]
+    networks:
+    - default
+    restart: unless-stopped
+    depends_on:
+      - rw_core
+
+#  adapter_adtran_onu:
+#    image: "${REGISTRY}${REPOSITORY}voltha-adapter-adtran-onu${TAG}"
+#    logging:
+#      driver: "json-file"
+#      options:
+#        max-size: "10m"
+#        max-file: "3"
+#    command: [
+#      "/voltha/adapters/adtran_onu/main.py",
+#      "-v",
+#      "--name=adtran_onu",
+#      "--kafka_adapter=${DOCKER_HOST_IP}:9092",
+#      "--kafka_cluster=${DOCKER_HOST_IP}:9092",
+#      "--core_topic=rwcore"
+#    ]
+#    networks:
+#      - default
+#    restart: unless-stopped
+#    depends_on:
+#      - rw_core
+
diff --git a/docker/Dockerfile.adapter_adtran_olt b/docker/Dockerfile.adapter_adtran_olt
index 638b5b7..bc86c8a 100644
--- a/docker/Dockerfile.adapter_adtran_olt
+++ b/docker/Dockerfile.adapter_adtran_olt
@@ -21,6 +21,7 @@
 
 # Adtran specific
 COPY adapters/adtran_olt /voltha/adapters/adtran_olt
+COPY VERSION /voltha
 
 # Exposing process and default entry point
 CMD ["python", "/voltha/adapters/adtran_olt/main.py"]
diff --git a/docker/Dockerfile.adapter_adtran_olt_pyvoltha b/docker/Dockerfile.adapter_adtran_olt_pyvoltha
index 369ca8c..ab75613 100644
--- a/docker/Dockerfile.adapter_adtran_olt_pyvoltha
+++ b/docker/Dockerfile.adapter_adtran_olt_pyvoltha
@@ -19,8 +19,11 @@
 
 MAINTAINER Voltha Community <info@opennetworking.org>
 
+ENV PYTHONPATH=/voltha
+
 # Adtran specific
 COPY adapters/adtran_olt /voltha/adapters/adtran_olt
+COPY VERSION /voltha
 
 # Exposing process and default entry point
 CMD ["python", "/voltha/adapters/adtran_olt/main.py"]
diff --git a/docker/Dockerfile.adapter_adtran_onu b/docker/Dockerfile.adapter_adtran_onu
index 3484dfb..be729a1 100644
--- a/docker/Dockerfile.adapter_adtran_onu
+++ b/docker/Dockerfile.adapter_adtran_onu
@@ -21,6 +21,7 @@
 
 # Adtran specific
 COPY adapters/adtran_olt /voltha/adapters/adtran_onu
+COPY VERSION /voltha
 
 # Exposing process and default entry point
 CMD ["python", "/voltha/adapters/adtran_onu/main.py"]
diff --git a/docker/Dockerfile.adapter_adtranonu_pyvoltha b/docker/Dockerfile.adapter_adtranonu_pyvoltha
index 6fcea60..0050ac7 100644
--- a/docker/Dockerfile.adapter_adtranonu_pyvoltha
+++ b/docker/Dockerfile.adapter_adtranonu_pyvoltha
@@ -21,6 +21,7 @@
 
 # Adtran specific
 COPY adapters/adtran_olt /voltha/adapters/adtran_onu
+COPY VERSION /voltha
 
 # Exposing process and default entry point
 CMD ["python", "/voltha/adapters/adtran_onu/main.py"]
\ No newline at end of file
diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base
index b65e77b..8e9d461 100644
--- a/docker/Dockerfile.base
+++ b/docker/Dockerfile.base
@@ -35,7 +35,7 @@
 
 COPY pki /voltha/pki/
 
-# Adtran specific
+# Adtran specific (common library and debug support)
 COPY adapters/adtran_common /voltha/adapters/adtran_common
 COPY pydevd /voltha/pydevd
 
diff --git a/docker/Dockerfile.base_local b/docker/Dockerfile.base_local
index 802e93f..792c2eb 100644
--- a/docker/Dockerfile.base_local
+++ b/docker/Dockerfile.base_local
@@ -35,7 +35,7 @@
 
 COPY pki /voltha/pki/
 
-# Adtran specific
+# Adtran specific (common library and debug support)
 COPY adapters/adtran_common /voltha/adapters/adtran_common
 COPY pydevd /voltha/pydevd
 
@@ -43,3 +43,7 @@
 COPY pyvoltha/dist /pyvoltha/dist
 RUN pip install /pyvoltha/dist/*.tar.gz
 
+# Install local voltha-protos
+COPY voltha-protos/dist /voltha-protos/dist
+RUN pip install /voltha-protos/dist/*.tar.gz
+
diff --git a/docker/config/Makefile.protos b/docker/config/Makefile.protos
new file mode 100644
index 0000000..12ff9e3
--- /dev/null
+++ b/docker/config/Makefile.protos
@@ -0,0 +1,59 @@
+#
+# Copyright 2017 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.
+#
+
+# Makefile to build all protobuf and gRPC related artifacts
+
+default: build
+
+PROTO_FILES := $(wildcard *.proto)
+PROTO_ALL_FILES := $(PROTO_FILES) $(PROTO_GOOGLE_API)
+PROTO_PB2_FILES := $(foreach f,$(PROTO_FILES),$(subst .proto,_pb2.py,$(f)))
+PROTO_PB2_GOOGLE_API := $(foreach f,$(PROTO_GOOGLE_API),$(subst .proto,_pb2.py,$(f)))
+PROTO_All_PB2_C_FILES := $(foreach f,$(PROTO_ALL_FILES),$(subst .proto,_pb2.pyc,$(f)))
+PROTO_ALL_PB2_GPRC_FILES := $(foreach f,$(PROTO_ALL_FILES),$(subst .proto,_pb2_grpc.py,$(f)))
+PROTO_ALL_DESC_FILES := $(foreach f,$(PROTO_ALL_FILES),$(subst .proto,.desc,$(f)))
+
+# Google API needs to be built from within the third party directory
+#
+google_api:
+	python -m grpc.tools.protoc \
+	    -I. \
+            --python_out=. \
+            --grpc_python_out=. \
+            --descriptor_set_out=google/api/annotations.desc \
+            --include_imports \
+            --include_source_info \
+        google/api/annotations.proto google/api/http.proto
+
+build: $(PROTO_PB2_FILES)
+
+%_pb2.py: %.proto
+	python -m grpc.tools.protoc \
+                -I. \
+                -I/protos \
+                --python_out=. \
+                --grpc_python_out=. \
+                --descriptor_set_out=$(basename $<).desc \
+                --include_imports \
+                --include_source_info \
+                $<
+
+clean:
+	rm -f $(PROTO_PB2_FILES) \
+		$(PROTO_ALL_DESC_FILES) \
+		$(PROTO_ALL_PB2_GPRC_FILES) \
+		$(PROTO_All_PB2_C_FILES) \
+		$(PROTO_PB2_GOOGLE_API)
diff --git a/env.sh b/env.sh
index ec3b52f..8f6fffb 100644
--- a/env.sh
+++ b/env.sh
@@ -27,3 +27,4 @@
 
 # add top-level voltha dir to pythonpath
 export PYTHONPATH=$VOLTHA_BASE/$VENVDIR/lib/python2.7/site-packages:$PYTHONPATH:$VOLTHA_BASE:$VOLTHA_BASE/cli:$VOLTHA_BASE/protos/third_party
+export PATH=$VOLTHA_BASE/bin:$VOLTHA_BASE/bin/k8s:$PATH
diff --git a/k8s/README.md b/k8s/README.md
new file mode 100644
index 0000000..9d9a36f
--- /dev/null
+++ b/k8s/README.md
@@ -0,0 +1,38 @@
+## How to deploy read/write core pairs on Kubernetes
+
+The current technique installs a separate rw-core deployment to each Kubernetes node, where each deployment consists of a pair (replicas = 2) of co-located rw-cores. Co-location is enforced by making use of the Kubernetes nodeSelector constraint applied at the pod spec level.
+
+In order for node selection to work, a label must be applied to each node. There is a set of built-in node labels that comes with Kubernetes out of the box, one of which is kubernetes.io/hostname. This label can be used to constrain the deployment of a core pair to a node with a specific hostname. Another approach is to take greater control and create new node labels.
+
+The following discussion assumes installation of the voltha-k8s-playground (https://github.com/ciena/voltha-k8s-playground) which configures three Kubernetes nodes named k8s1, k8s2, and k8s3.
+
+Create a "nodename" label for each Kubernetes node:
+```
+kubectl label nodes k8s1 nodename=k8s1
+kubectl label nodes k8s2 nodename=k8s2
+kubectl label nodes k8s3 nodename=k8s3
+```
+
+Verify that the labels have been applied:
+```
+kubectl get nodes --show-labels
+NAME      STATUS    ROLES         AGE       VERSION   LABELS
+k8s1      Ready     master,node   4h        v1.9.5    ...,kubernetes.io/hostname=k8s1,nodename=k8s1
+k8s2      Ready     node          4h        v1.9.5    ...,kubernetes.io/hostname=k8s2,nodename=k8s2
+k8s3      Ready     node          4h        v1.9.5    ...,kubernetes.io/hostname=k8s3,nodename=k8s3
+```
+
+
+Ensure that a nodeSelector section appears in the deployment's pod spec (such a section should already exist in each manifest):
+```
+      ...
+      nodeSelector:
+        nodename: k8s1
+```
+
+Once the labels have been applied, deploy the 3 core pairs:
+```
+kubectl apply -f k8s/rw-core-pair1.yml
+kubectl apply -f k8s/rw-core-pair2.yml
+kubectl apply -f k8s/rw-core-pair3.yml
+```
diff --git a/k8s/adapters-adtran-olt.yml b/k8s/adapters-adtran-olt.yml
new file mode 100644
index 0000000..1c61f83
--- /dev/null
+++ b/k8s/adapters-adtran-olt.yml
@@ -0,0 +1,59 @@
+# Copyright 2018 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.
+
+# ADTRAN OLT adapter
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: adapter-adtran-olt
+  namespace: voltha
+spec:
+  clusterIP: None
+  selector:
+    app: adapter-adtran-olt
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: adapter-adtran-olt
+  namespace: voltha
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: adapter-adtran-olt
+  template:
+    metadata:
+      labels:
+        app: adapter-adtran-olt
+      annotations:
+        cni: "calico"
+    spec:
+      containers:
+        - name: adapter-adtran-olt
+          image: voltha-adapter-adtran-olt
+          env:
+            - name: NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
+          args:
+            - "/voltha/python/adapters/adtran/adtran_olt/main.py"
+            - "-v"
+            - "--name=adtran_olt"
+            - "--kafka_adapter=kafka.$(NAMESPACE).svc.cluster.local:9092"
+            - "--kafka_cluster=kafka.$(NAMESPACE).svc.cluster.local:9092"
+            - "--core_topic=rwcore"
+          imagePullPolicy: IfNotPresent
diff --git a/k8s/adapters-adtran-onu.yml b/k8s/adapters-adtran-onu.yml
new file mode 100644
index 0000000..a2f23c3
--- /dev/null
+++ b/k8s/adapters-adtran-onu.yml
@@ -0,0 +1,59 @@
+# Copyright 2018 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.
+
+# ADTRAN OLT adapter
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: adapter-adtran-onu
+  namespace: voltha
+spec:
+  clusterIP: None
+  selector:
+    app: adapter-adtran-onu
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: adapter-adtran-onu
+  namespace: voltha
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: adapter-adtran-onu
+  template:
+    metadata:
+      labels:
+        app: adapter-adtran-onu
+      annotations:
+        cni: "calico"
+    spec:
+      containers:
+        - name: adapter-adtran-onu
+          image: voltha-adapter-adtran-onu
+          env:
+            - name: NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
+          args:
+            - "/voltha/python/adapters/adtran_onu/main.py"
+            - "-v"
+            - "--name=adtran_olt"
+            - "--kafka_adapter=kafka.$(NAMESPACE).svc.cluster.local:9092"
+            - "--kafka_cluster=kafka.$(NAMESPACE).svc.cluster.local:9092"
+            - "--core_topic=rwcore"
+          imagePullPolicy: IfNotPresent
diff --git a/k8s/single-node/cli.yml b/k8s/single-node/cli.yml
new file mode 100644
index 0000000..680d172
--- /dev/null
+++ b/k8s/single-node/cli.yml
@@ -0,0 +1,63 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# 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.
+apiVersion: v1
+kind: Service
+metadata:
+  name: vcli
+  namespace: voltha
+  labels:
+    name: vcli
+spec:
+  ports:
+    - name: ssh
+      port: 5022
+      targetPort: 22
+  selector:
+    app: vcli
+---
+apiVersion: apps/v1beta1
+kind: Deployment
+metadata:
+  name: vcli
+  namespace: voltha
+spec:
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        app: vcli
+      annotations:
+        cni: "calico"
+    spec:
+      containers:
+        - name: vcli
+          image: voltha-cli
+          env:
+            - name: POD_IP
+              valueFrom:
+                fieldRef:
+                  fieldPath: status.podIP
+            - name: NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
+          args:
+            - "/voltha/python/cli/setup.sh"
+            - "-g rw-core:50057"
+          ports:
+            - containerPort: 22
+              name: ssh-port
+          imagePullPolicy: Never
+
+
diff --git a/k8s/single-node/etcd-cluster.yml b/k8s/single-node/etcd-cluster.yml
new file mode 100644
index 0000000..61a35fa
--- /dev/null
+++ b/k8s/single-node/etcd-cluster.yml
@@ -0,0 +1,25 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# 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.
+apiVersion: "etcd.database.coreos.com/v1beta2"
+kind: "EtcdCluster"
+metadata:
+  name: etcd
+  namespace: voltha
+spec:
+  size: 1
+  version: "3.2.18"
+  pod:
+    annotations:
+      cni: "calico"
+
diff --git a/k8s/single-node/kafka.yml b/k8s/single-node/kafka.yml
new file mode 100644
index 0000000..4604676
--- /dev/null
+++ b/k8s/single-node/kafka.yml
@@ -0,0 +1,58 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# 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.
+apiVersion: v1
+kind: Service
+metadata:
+  name: kafka
+  namespace: voltha
+spec:
+  clusterIP: None
+  selector:
+    app: kafka
+  ports:
+  - protocol: TCP
+    port: 9092
+    targetPort: 9092
+---
+apiVersion: apps/v1beta1
+kind: StatefulSet
+metadata:
+  name: kafka
+  namespace: voltha
+spec:
+  serviceName: kafka
+  replicas: 1
+  selector:
+    matchLabels:
+      app: kafka
+  template:
+    metadata:
+      labels:
+        app: kafka
+      annotations:
+        cni: "calico"
+    spec:
+      terminationGracePeriodSeconds: 10
+      containers:
+      - name: kafka
+        image: wurstmeister/kafka:1.0.0
+        ports:
+        - containerPort: 9092
+        env:
+        - name: KAFKA_ADVERTISED_PORT
+          value: "9092"
+        - name: KAFKA_ZOOKEEPER_CONNECT
+          value: zoo:2181
+        - name: KAFKA_HEAP_OPTS
+          value: "-Xmx256M -Xms128M"
diff --git a/k8s/single-node/olt.yml b/k8s/single-node/olt.yml
new file mode 100644
index 0000000..148e1ad
--- /dev/null
+++ b/k8s/single-node/olt.yml
@@ -0,0 +1,74 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# 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.
+apiVersion: v1
+kind: Service
+metadata:
+   name: olt
+   namespace: voltha
+   labels:
+     name: olt
+spec:
+  ports:
+    - name: grpc
+      port: 50060
+      targetPort: 50060
+  selector:
+    app: olt
+---
+apiVersion: apps/v1beta1
+kind: Deployment
+metadata:
+  name: olt
+  namespace: voltha
+spec:
+   replicas: 1
+   template:
+     metadata:
+       labels:
+         app: olt
+       annotations:
+         cni: "calico"
+     spec:
+       containers:
+         - name: olt
+           image: "voltha-ponsim:latest"
+           env:
+             - name: POD_IP
+               valueFrom:
+                 fieldRef:
+                   fieldPath: status.podIP
+             - name: NAMESPACE
+               valueFrom:
+                 fieldRef:
+                   fieldPath: metadata.namespace
+
+           args:
+             - "/app/ponsim"
+             - "-device_type"
+             - "OLT"
+             - "-onus"
+             - "1"
+             - "-internal_if"
+             - "eth0"
+             - "-external_if"
+             - "eth0"
+             - "-vcore_endpoint"
+             - "rw-core"
+             - "-promiscuous"
+             - "-verbose"
+           ports:
+            - containerPort: 50060
+              name: grpc-port
+
+           imagePullPolicy: Never
diff --git a/k8s/single-node/onu.yml b/k8s/single-node/onu.yml
new file mode 100644
index 0000000..38e6cc6
--- /dev/null
+++ b/k8s/single-node/onu.yml
@@ -0,0 +1,74 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# 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.
+apiVersion: v1
+kind: Service
+metadata:
+   name: onu
+   namespace: voltha
+   labels:
+     name: onu
+spec:
+  ports:
+    - name: grpc
+      port: 50061
+      targetPort: 50061
+  selector:
+    app: onu
+---
+apiVersion: apps/v1beta1
+kind: Deployment
+metadata:
+  name: onu
+  namespace: voltha
+spec:
+   replicas: 1
+   template:
+     metadata:
+       labels:
+         app: onu
+       annotations:
+         cni: "calico,pon0"
+     spec:
+       containers:
+         - name: onu
+           image: "voltha-ponsim:latest"
+           env:
+             - name: POD_IP
+               valueFrom:
+                 fieldRef:
+                   fieldPath: status.podIP
+             - name: NAMESPACE
+               valueFrom:
+                 fieldRef:
+                   fieldPath: metadata.namespace
+
+           args:
+             - "/app/ponsim"
+             - "-device_type"
+             - "ONU"
+             - "-parent_addr"
+             - "olt"
+             - "-grpc_port"
+             - "50061"
+             - "-internal_if"
+             - "eth0"
+             - "-external_if"
+             - "eth1"
+             - "-promiscuous"
+             - "-verbose"
+           ports:
+            - containerPort: 50061
+              name: grpc-port
+
+           imagePullPolicy: Never
diff --git a/k8s/single-node/ro-core.yml b/k8s/single-node/ro-core.yml
new file mode 100644
index 0000000..b2f278f
--- /dev/null
+++ b/k8s/single-node/ro-core.yml
@@ -0,0 +1,73 @@
+# Copyright 2018 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.
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: ro-core
+  namespace: voltha
+spec:
+  clusterIP: None
+  ports:
+    - name: grpc
+      port: 50057
+      targetPort: 50057
+  selector:
+    app: ro-core
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: ro-core
+  namespace: voltha
+spec:
+  replicas: 1 
+  selector:
+    matchLabels:
+      app: ro-core
+  template:
+    metadata:
+      labels:
+        app: ro-core
+      annotations:
+        cni: "calico"
+    spec:
+      containers:
+        - name: voltha
+          image: voltha-ro-core
+          env:
+            - name: NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
+            - name: POD_IP
+              valueFrom:
+                fieldRef:
+                  fieldPath: status.podIP
+          args:
+            - "/app/ro_core"
+            - "-kv_store_type=etcd"
+            - "-kv_store_host=etcd.$(NAMESPACE).svc.cluster.local"
+            - "-kv_store_port=2379"
+            - "-grpc_port=50057"
+            - "-banner=true"
+            - "-ro_core_topic=rocore"
+            - "-log_level=0"
+          ports:
+            - containerPort: 50057
+              name: grpc-port
+          imagePullPolicy: IfNotPresent
+
+
+
diff --git a/k8s/single-node/rw-core.yml b/k8s/single-node/rw-core.yml
new file mode 100644
index 0000000..7fd3848
--- /dev/null
+++ b/k8s/single-node/rw-core.yml
@@ -0,0 +1,77 @@
+# Copyright 2018 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.
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: rw-core
+  namespace: voltha
+spec:
+  clusterIP: None
+  ports:
+    - name: grpc
+      port: 50057
+      targetPort: 50057
+  selector:
+    app: rw-core
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: rw-core
+  namespace: voltha
+spec:
+  replicas: 1 
+  selector:
+    matchLabels:
+      app: rw-core
+  template:
+    metadata:
+      labels:
+        app: rw-core
+      annotations:
+        cni: "calico"
+    spec:
+      containers:
+        - name: voltha
+          image: voltha-rw-core
+          env:
+            - name: NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
+            - name: POD_IP
+              valueFrom:
+                fieldRef:
+                  fieldPath: status.podIP
+          args:
+            - "/app/rw_core"
+            - "-kv_store_type=etcd"
+            - "-kv_store_host=etcd.$(NAMESPACE).svc.cluster.local"
+            - "-kv_store_port=2379"
+            - "-grpc_port=50057"
+            - "-banner=true"
+            - "-kafka_adapter_host=kafka.$(NAMESPACE).svc.cluster.local"
+            - "-kafka_adapter_port=9092"
+            - "-kafka_cluster_host=kafka.$(NAMESPACE).svc.cluster.local"
+            - "-kafka_cluster_port=9092"
+            - "-rw_core_topic=rwcore"
+            - "-log_level=0"
+          ports:
+            - containerPort: 50057
+              name: grpc-port
+          imagePullPolicy: IfNotPresent
+
+
+
diff --git a/k8s/single-node/zookeeper.yml b/k8s/single-node/zookeeper.yml
new file mode 100644
index 0000000..ffd3e12
--- /dev/null
+++ b/k8s/single-node/zookeeper.yml
@@ -0,0 +1,69 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# 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.
+apiVersion: v1
+kind: Service
+metadata:
+  name: zoo
+  namespace: voltha
+spec:
+  clusterIP: None
+  selector:
+    app: zookeeper
+  ports:
+  - name: client
+    port: 2181
+    targetPort: 2181
+  - name: follower
+    port: 2888
+    targetPort: 2888
+  - name: leader
+    port: 3888
+    targetPort: 3888
+---
+apiVersion: apps/v1beta1
+kind: StatefulSet
+metadata:
+  name: zookeeper
+  namespace: voltha
+spec:
+  serviceName: zoo
+  replicas: 1
+  selector:
+    matchLabels:
+      app: zookeeper
+  template:
+    metadata:
+      labels:
+        app: zookeeper
+      annotations:
+        cni: "calico"
+    spec:
+      containers:
+      - name: zoo
+        image: zookeeper:3.4.11
+        ports:
+        - containerPort: 2181
+        - containerPort: 2888
+        - containerPort: 3888
+        env:
+        - name: NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        - name: ZOO_MY_ID
+          value: "1"
+#        - name: ZOO_SERVERS
+#          value: >
+#            server.1=zookeeper1-0.zoo1.$(NAMESPACE).svc.cluster.local:2888:3888
+
diff --git a/requirements.txt b/requirements.txt
index 0ccfe39..aa62e19 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,9 @@
+lxml==3.6.4
 ncclient==0.5.3
 packaging==17.1
 pyvoltha==0.1.7
-lxml==3.6.4
-zmq==0.0.0
 pyzmq==16.0.3
 txZMQ==0.8.0
+voltha-protos==0.1.0
 xmltodict==0.11.0
+zmq==0.0.0