EVC/EVC-MAP update and initial xPON support

Change-Id: I5bc807318ebcd0901315ffc08bb0a14e66a35688
diff --git a/voltha/adapters/adtran_olt/README.md b/voltha/adapters/adtran_olt/README.md
index 889157f..f9257e9 100644
--- a/voltha/adapters/adtran_olt/README.md
+++ b/voltha/adapters/adtran_olt/README.md
@@ -1,22 +1,23 @@
 # Adtran OLT Device Adapter
-
 To preprovision an Adtran OLT, you will need to provide the IP Address and 
 the NETCONF/REST credentials for the device.  The NETCONF/REST credentials are an
 extension of the existing **preprovision_olt** command and these are placed after
 entering two dashes '_--_'.  The full syntax to use is.
 
-| Short | Long          | Default | Notes
-| :---: + :-----------: + :-----: + -----
-|  -u   | --nc_username | ''      | NETCONF username
-|  -p   | --nc_password | ''      | NETCONF Password
-|  -t   | --nc_port     | 830     | NETCONF TCP Port
-|  -U   | --rc_username | ''      | REST USERNAME
-|  -P   | --rc_password | ''      | REST PASSWORD
-|  -T   | --rc_port     | 8081    | REST PORT
+| Short | Long           | Default | Notes |
+| :---: | :------------: | :-----: | ----- |
+|  -u   | --nc_username  | ''      | NETCONF Username |
+|  -p   | --nc_password  | ''      | NETCONF Password |
+|  -t   | --nc_port      | 830     | NETCONF TCP Port |
+|  -U   | --rc_username  | ''      | REST Username |
+|  -P   | --rc_password  | ''      | REST Password |
+|  -T   | --rc_port      | 8081    | REST TCP Port |
+|  -z   | --zmq_port     | 5656    | ZeroMQ OMCI Proxy Port |
+|  -a   | --autoactivate | False   | Autoactivate ONUs, xPON othewise |
 
 For example, if your Adtran OLT is address 10.17.174.193 with the default TCP ports and
 NETCONF credentials of admin/admin and REST credentials of ADMIN/ADMIN, the command line
-would be
+would be:
 
 ```bash
     preprovision_olt -t adtran_olt -i 10.17.174.193 -- -u admin -p admin -U ADMIN -P ADMIN
@@ -27,4 +28,68 @@
 ```
 
 Currently the Adtran Device Adapter will enable all PON ports on startup and attempt to activate any discovered ONUs.
-This behaviour will change once PON Management is fully supported.
\ No newline at end of file
+This behaviour will change once PON Management is fully supported.
+
+## REST Based Pre-Provisioning
+In addition to CLI provisioning, the Adtran OLT Device Adapter can also be provisioned though the
+VOLTHA Northbound REST API. 
+
+```bash
+VOLTHA_IP=localhost
+OLT_IP=10.17.174.228
+REST_PORT=`docker inspect compose_chameleon_1 | jq -r '.[0].NetworkSettings.Ports["8881/tcp"][0].HostPort'`
+    
+curl -k -s -X POST https://${VOLTHA_IP}:${REST_PORT}/api/v1/local/devices \
+ --header 'Content-Type: application/json' --header 'Accept: application/json' \
+ -d "{\"type\": \"adtran_olt\",\"ipv4_address\": \"${OLT_IP}\",\"extra_args\": \"-u admin -p admin -U ADMIN -P ADMIN\"}" \
+| jq '.' | tee /tmp/adtn-olt.json
+```
+This will not only pre-provision the OLT, but it will also return the created VOLTHA Device ID for use other commands.
+The output is also shown on the console as well:
+
+```bash
+REST_PORT=`docker inspect compose_chameleon_1 | jq -r '.[0].NetworkSettings.Ports["8881/tcp"][0].HostPort'`
+    
+curl -k -s -X POST https://${VOLTHA_IP}:${REST_PORT}/api/v1/local/devices \
+  --header 'Content-Type: application/json' --header 'Accept: application/json' \
+  -d "{\"type\": \"adtran_olt\",\"ipv4_address\": \"${OLT_IP}\",\"extra_args\": \"-u admin -p admin -U ADMIN -P ADMIN\"}" \
+| jq '.' | tee /tmp/adtn-olt.json
+{
+  "extra_args": "-u admin -p admin -U ADMIN -P ADMIN",
+  "vendor": "",
+  "channel_terminations": [],
+  "parent_port_no": 0,
+  "connect_status": "UNKNOWN",
+  "root": false,
+  "adapter": "adtran_olt",
+  "vlan": 0,
+  "hardware_version": "",
+  "ports": [],
+  "ipv4_address": "10.17.174.228",
+  "parent_id": "",
+  "oper_status": "UNKNOWN",
+  "admin_state": "PREPROVISIONED",
+  "reason": "",
+  "serial_number": "",
+  "model": "",
+  "type": "adtran_olt",
+  "id": "00017cbb382b9260",
+  "firmware_version": ""
+}
+```
+## Enabling the Pre-Provisioned OLT
+To enable the OLT, you need the retrieve the OLT Device ID and issue a POST request to the proper URL as in:
+```bash
+DEVICE_ID=$(jq .id /tmp/adtn-olt.json | sed 's/"//g')
+
+curl -k -s -X POST https://${VOLTHA_IP}:${REST_PORT}/api/v1/local/devices/${DEVICE_ID}/enable
+```
+### Other REST APIs
+A full list of URLs supported by VOLTHA can be obtained from the swagger API pointing
+your favorite Internet Browser at: **https://${VOLTHA_IP}:${REST_PORT}/#**
+
+To list out any devices, you can use the following command:
+
+```bash
+curl -k -s  https://${VOLTHA_IP}:${REST_PORT}/api/v1/local/devices | json_pp
+```
diff --git a/voltha/adapters/adtran_olt/adtran_device_handler.py b/voltha/adapters/adtran_olt/adtran_device_handler.py
index 37ee403..e8a64bd 100644
--- a/voltha/adapters/adtran_olt/adtran_device_handler.py
+++ b/voltha/adapters/adtran_olt/adtran_device_handler.py
@@ -1,18 +1,16 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# 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 generic VOLTHA device handler
 """
@@ -49,8 +47,18 @@
 _ = third_party
 
 _PACKET_IN_VLAN = 4000
+_MULTICAST_VLAN = 4092
+_MANAGEMENT_VLAN = 4093
 _is_inband_frame = BpfProgramFilter('(ether[14:2] & 0xfff) = 0x{:03x}'.format(_PACKET_IN_VLAN))
 
+_DEFAULT_RESTCONF_USERNAME = ""
+_DEFAULT_RESTCONF_PASSWORD = ""
+_DEFAULT_RESTCONF_PORT = 8081
+
+_DEFAULT_NETCONF_USERNAME = ""
+_DEFAULT_NETCONF_PASSWORD = ""
+_DEFAULT_NETCONF_PORT = 830
+
 
 class AdtranDeviceHandler(object):
     """
@@ -84,7 +92,9 @@
     # RPC XML shortcuts
     RESTART_RPC = '<system-restart xmlns="urn:ietf:params:xml:ns:yang:ietf-system"/>'
 
-    def __init__(self, adapter, device_id, username='', password='', timeout=20):
+    def __init__(self, adapter, device_id, timeout=20):
+        from net.adtran_zmq import DEFAULT_ZEROMQ_OMCI_TCP_PORT
+
         self.adapter = adapter
         self.adapter_agent = adapter.adapter_agent
         self.device_id = device_id
@@ -111,17 +121,35 @@
         self.restart_failure_timeout = 5 * 60   # 5 Minute timeout
 
         # REST Client
-        self.rest_port = None
-        self.rest_username = username
-        self.rest_password = password
+        self.rest_port = _DEFAULT_RESTCONF_PORT
+        self.rest_username = _DEFAULT_RESTCONF_USERNAME
+        self.rest_password = _DEFAULT_RESTCONF_PASSWORD
         self._rest_client = None
 
         # NETCONF Client
-        self.netconf_port = None
-        self.netconf_username = username
-        self.netconf_password = password
+        self.netconf_port = _DEFAULT_NETCONF_PORT
+        self.netconf_username = _DEFAULT_NETCONF_USERNAME
+        self.netconf_password = _DEFAULT_NETCONF_PASSWORD
         self._netconf_client = None
 
+        # If Auto-activate is true, all PON ports (up to a limit below) will be auto-enabled
+        # and any ONU's discovered will be auto-activated.
+        #
+        # If it is set to False, then the xPON API/CLI should be used to enable any PON
+        # ports. Before enabling a PON, set it's polling interval. If the polling interval
+        # is 0, then manual ONU discovery is in effect. If >0, then every 'polling' seconds
+        # autodiscover is requested. Any discovered ONUs will need to have their serial-numbers
+        # registered (via xPON API/CLI) before they are activated.
+
+        self._autoactivate = False
+
+        # TODO Remove items below after one PON fully supported and working as expected
+        self.max_nni_ports = 1
+        self.max_pon_ports = 1
+
+        # OMCI ZMQ Channel
+        self.zmq_port = DEFAULT_ZEROMQ_OMCI_TCP_PORT
+
         # Heartbeat support
         self.heartbeat_count = 0
         self.heartbeat_miss = 0
@@ -137,10 +165,6 @@
         # Installed flows
         self.evcs = {}  # Flow ID/name -> FlowEntry
 
-        # TODO Remove items below after one PON fully supported and working as expected
-        self.max_nni_ports = 1
-        self.max_pon_ports = 1
-
     def __del__(self):
         # Kill any startup or heartbeat defers
 
@@ -177,6 +201,8 @@
         return self._rest_client
 
     def parse_provisioning_options(self, device):
+        from net.adtran_zmq import DEFAULT_ZEROMQ_OMCI_TCP_PORT
+
         if not device.ipv4_address:
             self.activate_failed(device, 'No ip_address field provided')
 
@@ -192,14 +218,22 @@
             return ivalue
 
         parser = argparse.ArgumentParser(description='Adtran Device Adapter')
-        parser.add_argument('--nc_username', '-u', action='store', default='hsvroot', help='NETCONF username')
-        parser.add_argument('--nc_password', '-p', action='store', default='BOSCO', help='NETCONF Password')
-        parser.add_argument('--nc_port', '-t', action='store', default=830, type=check_tcp_port,
+        parser.add_argument('--nc_username', '-u', action='store', default=_DEFAULT_NETCONF_USERNAME,
+                            help='NETCONF username')
+        parser.add_argument('--nc_password', '-p', action='store', default=_DEFAULT_NETCONF_PASSWORD,
+                            help='NETCONF Password')
+        parser.add_argument('--nc_port', '-t', action='store', default=_DEFAULT_NETCONF_PORT, type=check_tcp_port,
                             help='NETCONF TCP Port')
-        parser.add_argument('--rc_username', '-U', action='store', default='ADMIN', help='REST username')
-        parser.add_argument('--rc_password', '-P', action='store', default='PASSWORD', help='REST Password')
-        parser.add_argument('--rc_port', '-T', action='store', default=8081, type=check_tcp_port,
-                            help='REST TCP Port')
+        parser.add_argument('--rc_username', '-U', action='store', default=_DEFAULT_RESTCONF_USERNAME,
+                            help='REST username')
+        parser.add_argument('--rc_password', '-P', action='store', default=_DEFAULT_RESTCONF_PASSWORD,
+                            help='REST Password')
+        parser.add_argument('--rc_port', '-T', action='store', default=_DEFAULT_RESTCONF_PORT, type=check_tcp_port,
+                            help='RESTCONF TCP Port')
+        parser.add_argument('--zmq_port', '-z', action='store', default=DEFAULT_ZEROMQ_OMCI_TCP_PORT,
+                            type=check_tcp_port, help='ZeroMQ Port')
+        parser.add_argument('--autoactivate', '-a', action='store_true', default=False,
+                            help='Autoactivate / Demo mode')
 
         try:
             args = parser.parse_args(shlex.split(device.extra_args))
@@ -212,12 +246,27 @@
             self.rest_password = args.rc_password
             self.rest_port = args.rc_port
 
+            self.zmq_port = args.zmq_port
+
+            self._autoactivate = args.autoactivate
+
         except argparse.ArgumentError as e:
             self.activate_failed(device,
                                  'Invalid arguments: {}'.format(e.message),
                                  reachable=False)
         except Exception as e:
-            self.log.exception('parsing error: {}'.format(e.message))
+            self.log.exception('option_parsing_error: {}'.format(e.message))
+
+    @property
+    def autoactivate(self):
+        """
+        Flag indicating if auto-discover/enable of PON ports is enabled as
+        well as ONU auto activation. useful for demos
+
+        If autoactivate is enabled, the default startup state (first time) for a PON port is disabled
+        If autoactivate is disabled, the efault startup state for a PON port is enabled
+        """
+        return self._autoactivate
 
     @inlineCallbacks
     def activate(self, device, reconciling=False):
@@ -241,18 +290,16 @@
             try:
                 self.startup = self.make_restconf_connection()
                 results = yield self.startup
-                self.log.debug('HELLO Contents: {}'.format(pprint.PrettyPrinter().pformat(results)))
+                self.log.debug('HELLO_Contents: {}'.format(pprint.PrettyPrinter().pformat(results)))
 
                 # See if this is a virtualized OLT. If so, no NETCONF support available
 
                 self.is_virtual_olt = 'module-info' in results and\
                                       any(mod.get('module-name', None) == 'adtran-ont-mock'
                                           for mod in results['module-info'])
-                if self.is_virtual_olt:
-                    self.log.info('*** VIRTUAL OLT detected ***')
 
             except Exception as e:
-                self.log.exception('Initial RESTCONF adtran-hello failed', e=e)
+                self.log.exception('Initial_RESTCONF_hello_failed', e=e)
                 self.activate_failed(device, e.message, reachable=False)
 
             ############################################################################
@@ -263,7 +310,7 @@
                 yield self.startup
 
             except Exception as e:
-                self.log.exception('Initial NETCONF connection failed', e=e)
+                self.log.exception('NETCONF_connection_failed', e=e)
                 self.activate_failed(device, e.message, reachable=False)
 
             ############################################################################
@@ -302,7 +349,7 @@
                     self.adapter_agent.update_device(device)
 
                 except Exception as e:
-                    self.log.exception('Device Information request(s) failed', e=e)
+                    self.log.exception('Device_info_failed', e=e)
                     self.activate_failed(device, e.message, reachable=False)
 
             try:
@@ -319,7 +366,7 @@
                         self.adapter_agent.add_port(device.id, port.get_port())
 
             except Exception as e:
-                self.log.exception('Northbound port enumeration and creation failed', e=e)
+                self.log.exception('NNI_enumeration', e=e)
                 self.activate_failed(device, e.message)
 
             try:
@@ -336,7 +383,7 @@
                         self.adapter_agent.add_port(device.id, port.get_port())
 
             except Exception as e:
-                self.log.exception('Southbound port enumeration and creation failed', e=e)
+                self.log.exception('PON_enumeration', e=e)
                 self.activate_failed(device, e.message)
 
             if reconciling:
@@ -378,22 +425,22 @@
                 yield self.startup
 
             except Exception as e:
-                self.log.exception('Logical port creation failed', e=e)
+                self.log.exception('logical-port', e=e)
                 self.activate_failed(device, e.message)
 
             # Complete device specific steps
             try:
-                self.log.debug('Performing final device specific activation procedures')
+                self.log.debug('device-activation-procedures')
                 self.startup = self.complete_device_specific_activation(device, reconciling)
                 yield self.startup
 
             except Exception as e:
-                self.log.exception('Device specific activation failed', e=e)
+                self.log.exception('device-activation-procedures', e=e)
                 self.activate_failed(device, e.message)
 
             # Schedule the heartbeat for the device
 
-            self.log.debug('Starting heartbeat')
+            self.log.debug('Starting-heartbeat')
             self.start_heartbeat(delay=5)
 
             device = self.adapter_agent.get_device(device.id)
@@ -401,6 +448,7 @@
             device.oper_status = OperStatus.ACTIVE
             device.reason = ''
             self.adapter_agent.update_device(device)
+            self.logical_device_id = ld_initialized.id
 
             # finally, open the frameio port to receive in-band packet_in messages
             self._activate_io_port()
@@ -546,6 +594,21 @@
                     self.log.exception('Failed to reset ports to known good initial state', e=e)
                     self.activate_failed(device, e.message)
 
+            # Clean up all EVC and EVC maps (exceptions ok/not-fatal)
+            try:
+                from flow.evc import EVC
+                self.startup = yield EVC.remove_all(self.netconf_client)
+
+            except Exception as e:
+                self.log.exception('Failed attempting to clean up existing EVCs', e=e)
+
+            try:
+                from flow.evc_map import EVCMap
+                self.startup = yield EVCMap.remove_all(self.netconf_client)
+
+            except Exception as e:
+                self.log.exception('Failed attempting to clean up existing EVC-Maps', e=e)
+
         # Start/stop the interfaces as needed
 
         for port in self.northbound_ports.itervalues():
@@ -555,7 +618,7 @@
         if reconciling:
             start_downlinks = device.admin_state == AdminState.ENABLED
         else:
-            start_downlinks = self.initial_port_state == AdminState.ENABLED
+            start_downlinks = self.autoactivate
 
         for port in self.southbound_ports.itervalues():
             self.startup = port.start() if start_downlinks else port.stop()
@@ -737,7 +800,7 @@
             try:
                 yield self.netconf_client.close()
             except Exception as e:
-                self.log.exception('NETCONF client shutdown failed', e=e)
+                self.log.exception('NETCONF-shutdown', e=e)
 
         def _null_clients():
             self._netconf_client = None
@@ -745,6 +808,10 @@
 
         reactor.callLater(0, _null_clients)
 
+        #  Update the logice device mapping
+        if ldi in self.adapter.logical_device_id_to_root_device_id:
+            del self.adapter.logical_device_id_to_root_device_id[ldi]
+
         self.log.info('disabled', device_id=device.id)
         returnValue(results)
 
@@ -769,14 +836,14 @@
             yield self.make_restconf_connection()
 
         except Exception as e:
-            self.log.exception('RESTCONF adtran-hello reconnect failed', e=e)
+            self.log.exception('adtran-hello-reconnect', e=e)
             # TODO: What is best way to handle reenable failure?
 
         try:
             yield self.make_netconf_connection()
 
         except Exception as e:
-            self.log.exception('NETCONF re-connection failed', e=e)
+            self.log.exception('NETCONF-re-connection', e=e)
             # TODO: What is best way to handle reenable failure?
 
         # Recreate the logical device
@@ -850,7 +917,7 @@
                 yield self.netconf_client.rpc(AdtranDeviceHandler.RESTART_RPC)
 
             except Exception as e:
-                self.log.exception('NETCONF client shutdown', e=e)
+                self.log.exception('NETCONF-shutdown', e=e)
                 # TODO: On failure, what is the best thing to do?
 
             # Shutdown communications with OLT. Typically it takes about 2 seconds
@@ -861,7 +928,7 @@
                 self.log.debug('Restart response XML was: {}'.format('ok' if response.ok else 'bad'))
 
             except Exception as e:
-                self.log.exception('NETCONF client shutdown', e=e)
+                self.log.exception('NETCONF-client-shutdown', e=e)
 
         #  Clear off clients
 
@@ -878,7 +945,7 @@
             yield reactor.callLater(10, self._finish_reboot, timeout,
                                     previous_oper_status, previous_conn_status)
         except Exception as e:
-            self.log.exception('finish reboot scheduling', e=e)
+            self.log.exception('finish-reboot', e=e)
 
         returnValue('Waiting for reboot')
 
@@ -886,13 +953,13 @@
     def _finish_reboot(self, timeout, previous_oper_status, previous_conn_status):
         # Now wait until REST & NETCONF are re-established or we timeout
 
-        self.log.info('Resuming OLT activity after reboot requested',
+        self.log.info('Resuming-activity',
                       remaining=timeout - time.time(), timeout=timeout, current=time.time())
 
         if self.rest_client is None:
             try:
                 response = yield self.make_restconf_connection(get_timeout=10)
-                self.log.debug('Restart RESTCONF connection JSON was: {}'.format(response))
+                # self.log.debug('Restart RESTCONF connection JSON was: {}'.format(response))
 
             except Exception:
                 self.log.debug('No RESTCONF connection yet')
@@ -901,10 +968,9 @@
         if self.netconf_client is None:
             try:
                 yield self.make_netconf_connection(connect_timeout=10)
-                self.log.debug('Restart NETCONF connection succeeded')
+                # self.log.debug('Restart NETCONF connection succeeded')
 
             except Exception as e:
-                self.log.debug('No NETCONF connection yet: {}'.format(e.message))
                 try:
                     if self.netconf_client is not None:
                         yield self.netconf_client.close()
@@ -920,16 +986,16 @@
                     yield reactor.callLater(5, self._finish_reboot, timeout,
                                             previous_oper_status, previous_conn_status)
                 except Exception:
-                    self.log.debug('Rebooted check rescheduling')
+                    self.log.debug('Rebooted-check', e=e)
 
                 returnValue('Waiting some more...')
 
             if self.netconf_client is None and not self.is_virtual_olt:
-                self.log.error('Could not restore NETCONF communications after device RESET')
+                self.log.error('NETCONF-restore-failure')
                 pass        # TODO: What is best course of action if cannot get clients back?
 
             if self.rest_client is None:
-                self.log.error('Could not restore RESTCONF communications after device RESET')
+                self.log.error('RESTCONF-restore-failure')
                 pass        # TODO: What is best course of action if cannot get clients back?
 
         # Pause additional 5 seconds to let allow OLT microservices to complete some more initialization
@@ -1003,7 +1069,7 @@
             try:
                 yield self.netconf_client.close()
             except Exception as e:
-                self.log.exception('NETCONF client shutdown', e=e)
+                self.log.exception('NETCONF-shutdown', e=e)
 
             self._netconf_client = None
 
@@ -1055,7 +1121,7 @@
             pkt = Ether(msg)
             out_pkt = (
                 Ether(src=pkt.src, dst=pkt.dst) /
-                Dot1Q(vlan=4000) /
+                Dot1Q(vlan=_PACKET_IN_VLAN) /
                 Dot1Q(vlan=egress_port, type=pkt.type) /
                 pkt.payload
             )
@@ -1114,7 +1180,7 @@
 
     def start_heartbeat(self, delay=10):
         assert delay > 1
-        self.log.info('*** Starting Device Heartbeat ***')
+        self.log.info('Starting-Device-Heartbeat ***')
         self.heartbeat = reactor.callLater(delay, self.check_pulse)
         return self.heartbeat
 
@@ -1141,7 +1207,7 @@
             assert results
             # Update device states
 
-            self.log.info('heartbeat success')
+            self.log.info('heartbeat-success')
 
             if device.connect_status != ConnectStatus.REACHABLE:
                 device.connect_status = ConnectStatus.REACHABLE
diff --git a/voltha/adapters/adtran_olt/adtran_olt.py b/voltha/adapters/adtran_olt/adtran_olt.py
index e44a15b..36dfaa5 100644
--- a/voltha/adapters/adtran_olt/adtran_olt.py
+++ b/voltha/adapters/adtran_olt/adtran_olt.py
@@ -1,18 +1,16 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# 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 1-U OLT adapter.
@@ -53,13 +51,13 @@
         self.descriptor = Adapter(
             id=self.name,
             vendor='Adtran Inc.',
-            version='0.1',
+            version='0.2',
             config=AdapterConfig(log_level=LogLevel.INFO)
         )
         log.debug('adtran_olt.__init__', adapter_agent=adapter_agent)
         self.devices_handlers = dict()  # device_id -> AdtranOltHandler()
         self.interface = registry('main').get_args().interface
-        # self.logical_device_id_to_root_device_id = dict()
+        self.logical_device_id_to_root_device_id = dict()
 
     def start(self):
         """
@@ -164,12 +162,6 @@
         """
         log.info('abandon-device', device=device)
         raise NotImplementedError()
-        # handler = self.devices_handlers.pop(device.id)
-        #
-        # if handler is not None:
-        #     reactor.callLater(0, handler.deactivate, device)
-        #
-        # return device
 
     def disable_device(self, device):
         """
@@ -364,7 +356,7 @@
 
     def receive_inter_adapter_message(self, msg):
         """
-        Called when the adapter recieves a message that was sent to it directly
+        Called when the adapter receives a message that was sent to it directly
         from another adapter. An adapter may register for these messages by calling
         the register_for_inter_adapter_messages() method in the adapter agent.
         Note that it is the responsibility of the sending and receiving
@@ -399,7 +391,9 @@
         API to create various interfaces (only some PON interfaces as of now)
         in the devices
         """
-        raise NotImplementedError()
+        log.info('create_interface', data=data)
+        handler = self.devices_handlers[device.id]
+        handler.create_interface(device, data)
 
     def update_interface(self, device, data):
         """
diff --git a/voltha/adapters/adtran_olt/adtran_olt_handler.py b/voltha/adapters/adtran_olt/adtran_olt_handler.py
index b0e6874..be83dc4 100644
--- a/voltha/adapters/adtran_olt/adtran_olt_handler.py
+++ b/voltha/adapters/adtran_olt/adtran_olt_handler.py
@@ -1,20 +1,18 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
+
 import datetime
-import pprint
 import random
 
 from twisted.internet import reactor
@@ -27,6 +25,9 @@
 from voltha.extensions.omci.omci import *
 from voltha.protos.common_pb2 import AdminState, OperStatus
 from voltha.protos.device_pb2 import Device
+from voltha.protos.bbf_fiber_base_pb2 import \
+    ChannelgroupConfig, ChannelpartitionConfig, ChannelpairConfig, ChannelterminationConfig, \
+    OntaniConfig, VOntaniConfig, VEnetConfig
 
 
 class AdtranOltHandler(AdtranDeviceHandler):
@@ -38,30 +39,46 @@
     # Full table output
 
     GPON_OLT_HW_URI = '/restconf/data/gpon-olt-hw'
-    GPON_OLT_HW_STATE_URI = '/restconf/data/gpon-olt-hw:olt-state'
-    GPON_PON_CONFIG_LIST_URI = '/restconf/data/gpon-olt-hw:olt/pon'
+    GPON_OLT_HW_STATE_URI = GPON_OLT_HW_URI + ':olt-state'
+    GPON_PON_CONFIG_LIST_URI = GPON_OLT_HW_URI + ':olt/pon'
 
     # Per-PON info
 
-    GPON_PON_PON_STATE_URI = '/restconf/data/gpon-olt-hw:olt-state/pon={}'  # .format(pon)
-    GPON_PON_CONFIG_URI = '/restconf/data/gpon-olt-hw:olt/pon={}'  # .format(pon)
-    GPON_PON_ONU_CONFIG_URI = '/restconf/data/gpon-olt-hw:olt/pon={}/onus/onu'  # .format(pon)
+    GPON_PON_STATE_URI = GPON_OLT_HW_STATE_URI + '/pon={}'        # .format(pon-id)
+    GPON_PON_CONFIG_URI = GPON_PON_CONFIG_LIST_URI + '={}'        # .format(pon-id)
+
+    GPON_ONU_CONFIG_LIST_URI = GPON_PON_CONFIG_URI + '/onus/onu'  # .format(pon-id)
+    GPON_ONU_CONFIG_URI = GPON_ONU_CONFIG_LIST_URI + '={}'        # .format(pon-id,onu-id)
+
+    GPON_TCONT_CONFIG_LIST_URI = GPON_ONU_CONFIG_URI + '/t-conts/t-cont'  # .format(pon-id,onu-id)
+    GPON_TCONT_CONFIG_URI = GPON_TCONT_CONFIG_LIST_URI + '={}'            # .format(pon-id,onu-id,alloc-id)
+
+    GPON_GEM_CONFIG_LIST_URI = GPON_ONU_CONFIG_URI + '/gem-ports/gem-port'  # .format(pon-id,onu-id)
+    GPON_GEM_CONFIG_URI = GPON_GEM_CONFIG_LIST_URI + '={}'                  # .format(pon-id,onu-id,gem-id)
 
     GPON_PON_DISCOVER_ONU = '/restconf/operations/gpon-olt-hw:discover-onu'
 
-    def __init__(self, adapter, device_id, username="", password="",
-                 timeout=20, initial_port_state=True):
-        super(AdtranOltHandler, self).__init__(adapter, device_id, username=username,
-                                               password=password, timeout=timeout)
+    BASE_ONU_OFFSET = 64
+
+    def __init__(self, adapter, device_id, timeout=20):
+        super(AdtranOltHandler, self).__init__(adapter, device_id, timeout=timeout)
         self.gpon_olt_hw_revision = None
         self.status_poll = None
         self.status_poll_interval = 5.0
         self.status_poll_skew = self.status_poll_interval / 10
-        self.initial_port_state = AdminState.ENABLED if initial_port_state else AdminState.DISABLED
-        self.initial_onu_state = AdminState.DISABLED
 
         self.zmq_client = None
 
+        # xPON config dictionaries
+
+        self._channel_groups = {}         #  Name -> dict
+        self._channel_partitions = {}     #  Name -> dict
+        self._channel_pairs = {}          #  Name -> dict
+        self._channel_terminations = {}   #  Name -> dict
+        self._v_ont_anis = {}             #  Name -> dict
+        self._ont_anis = {}               #  Name -> dict
+        self._v_enets = {}                #  Name -> dict
+
     def __del__(self):
         # OLT Specific things here.
         #
@@ -169,7 +186,7 @@
 
         for port in results:
             port_no = port['port_no']
-            self.log.info('Processing northbound port {}/{}'.format(port_no, port['port_no']))
+            self.log.info('processing-nni', port_no=port_no, name=port['port_no'])
             assert port_no
             assert port_no not in self.northbound_ports
             self.northbound_ports[port_no] = NniPort(self, **port) if not self.is_virtual_olt \
@@ -213,7 +230,7 @@
         for pon in results:
             # Number PON Ports after the NNI ports
             pon_id = pon['pon-id']
-            log.info('Processing pon port {}'.format(pon_id))
+            log.info('Processing-pon-port', pon_id=pon_id)
             assert pon_id not in self.southbound_ports
 
             admin_state = AdminState.ENABLED if pon.get('enabled',
@@ -225,7 +242,7 @@
                                                     admin_state=admin_state)
 
             # TODO: For now, limit number of PON ports to make debugging easier
-            if len(self.southbound_ports) >= self.max_pon_ports:
+            if self.autoactivate and len(self.southbound_ports) >= self.max_pon_ports:
                 break
 
         self.num_southbound_ports = len(self.southbound_ports)
@@ -252,7 +269,7 @@
         #
         # o TODO Update some PON level statistics
 
-        self.zmq_client = AdtranZmqClient(self.ip_address, self.rx_packet)
+        self.zmq_client = AdtranZmqClient(self.ip_address, rx_callback=self.rx_packet, port=self.zmq_port)
         self.status_poll = reactor.callLater(5, self.poll_for_status)
         return succeed('Done')
 
@@ -270,7 +287,7 @@
     def reenable(self):
         super(AdtranOltHandler, self).reenable()
 
-        self.zmq_client = AdtranZmqClient(self.ip_address, self.rx_packet)
+        self.zmq_client = AdtranZmqClient(self.ip_address, rx_callback=self.rx_packet, port=self.zmq_port)
         self.status_poll = reactor.callLater(1, self.poll_for_status)
 
     def reboot(self):
@@ -287,7 +304,7 @@
     def _finish_reboot(self, timeout, previous_oper_status, previous_conn_status):
         super(AdtranOltHandler, self)._finish_reboot(timeout, previous_oper_status, previous_conn_status)
 
-        self.zmq_client = AdtranZmqClient(self.ip_address, self.rx_packet)
+        self.zmq_client = AdtranZmqClient(self.ip_address, rx_callback=self.rx_packet, port=self.zmq_port)
         self.status_poll = reactor.callLater(1, self.poll_for_status)
 
     def delete(self):
@@ -303,7 +320,7 @@
 
     def rx_packet(self, message):
         try:
-            self.log.info('rx_Packet: Message from ONU')
+            self.log.debug('rx_packet')
 
             pon_id, onu_id, msg, is_omci = AdtranZmqClient.decode_packet(message)
 
@@ -319,10 +336,10 @@
                 #                                   logical_port_no=cvid,  # C-VID encodes port no
                 #                                   packet=str(msg))
         except Exception as e:
-            self.log.exception('Exception during RX Packet processing', e=e)
+            self.log.exception('rx_packet', e=e)
 
     def poll_for_status(self):
-        self.log.debug('Initiating status poll')
+        self.log.debug('Initiating-status-poll')
 
         device = self.adapter_agent.get_device(self.device_id)
 
@@ -341,17 +358,19 @@
         Results of the status poll
         :param results:
         """
+        from pon_port import PonPort
+
         if isinstance(results, dict) and 'pon' in results:
             try:
-                self.log.debug('Status poll success')
+                self.log.debug('status-success')
                 for pon_id, pon in OltState(results).pons.iteritems():
-                    if pon_id in self.southbound_ports:
-                        self.southbound_ports[pon_id].process_status_poll(pon)
+                    pon_port = self.southbound_ports.get(pon_id, None)
+
+                    if pon_port is not None and pon_port.state == PonPort.State.RUNNING:
+                        pon_port.process_status_poll(pon)
 
             except Exception as e:
-                self.log.exception('Exception during PON status poll processing', e=e)
-        else:
-            self.log.warning('Had some kind of polling error')
+                self.log.exception('PON-status-poll', e=e)
 
         # Reschedule
 
@@ -385,8 +404,8 @@
         :param device: A voltha.Device object, with possible device-type
                        specific extensions.
         """
-        self.log.info('bulk-flow-update: {} flows'.format(len(flows)),
-                      device_id=device.id, flows=flows)
+        self.log.debug('bulk-flow-update', num_flows=len(flows),
+                       device_id=device.id, flows=flows)
 
         valid_flows = []
 
@@ -411,7 +430,7 @@
 
                 if evc is not None:
                     try:
-                        results = yield evc.install()
+                        evc.schedule_install()
 
                         if evc.name not in self.evcs:
                             self.evcs[evc.name] = evc
@@ -419,34 +438,23 @@
                             # TODO: Do we get duplicates here (ie all flows re-pushed on each individual flow add?)
                             pass
 
-                        # Also make sure all EVC MAPs are installed
-
-                        for evc_map in evc.evc_maps:
-                            try:
-                                results = yield evc_map.install()
-                                pass                                # TODO: What to do on error?
-
-                            except Exception as e:
-                                evc_map.status = 'Exception during EVC-MAP Install: {}'.format(e.message)
-                                self.log.exception(evc_map.status, e=e)
-
                     except Exception as e:
-                        evc.status = 'Exception during EVC Install: {}'.format(e.message)
-                        self.log.exception(evc.status, e=e)
+                        evc.status = 'EVC Install Exception: {}'.format(e.message)
+                        self.log.exception('EVC-install', e=e)
 
             except Exception as e:
-                self.log.exception('Failure during bulk flow update - add', e=e)
+                self.log.exception('bulk-flow-update-add', e=e)
 
         # Now drop all flows from this device that were not in this bulk update
         try:
             FlowEntry.drop_missing_flows(device.id, valid_flows)
 
         except Exception as e:
-            self.log.exception('Failure during bulk flow update - remove', e=e)
+            self.log.exception('bulk-flow-update-remove', e=e)
 
-    @inlineCallbacks
+    # @inlineCallbacks
     def send_proxied_message(self, proxy_address, msg):
-        self.log.info('sending-proxied-message: message type: {}'.format(type(msg)))
+        self.log.debug('sending-proxied-message', msg=msg)
 
         if isinstance(msg, Packet):
             msg = str(msg)
@@ -461,7 +469,7 @@
                 self.zmq_client.send(data)
 
             except Exception as e:
-                self.log.info('zmqClient.send exception', exc=str(e))
+                self.log.exception('zmqClient.send', e=e)
                 raise
 
     @staticmethod
@@ -484,11 +492,11 @@
     def _onu_offset(self, onu_id):
         # Start ONU's just past the southbound PON port numbers. Since ONU ID's start
         # at zero, add one
-        return self.num_northbound_ports + self.num_southbound_ports + onu_id + 1
+        assert AdtranOltHandler.BASE_ONU_OFFSET > (self.num_northbound_ports + self.num_southbound_ports + 1)
+        return AdtranOltHandler.BASE_ONU_OFFSET + onu_id
 
     def _channel_id_to_pon_id(self, channel_id, onu_id):
         from pon_port import PonPort
-
         return (channel_id - self._onu_offset(onu_id)) / PonPort.MAX_ONUS_SUPPORTED
 
     def _pon_id_to_port_number(self, pon_id):
@@ -503,15 +511,193 @@
     def is_uni_port(self, port):
         return port >= self._onu_offset(0)  # TODO: Really need to rework this one...
 
+    def get_southbound_port(self, port):
+        pon_id = self._port_number_to_pon_id(port)
+        return self.southbound_ports.get(pon_id, None)
+
     def get_port_name(self, port):
         if self.is_nni_port(port):
             return self.northbound_ports[port].name
 
         if self.is_pon_port(port):
-            return self.southbound_ports[self._port_number_to_pon_id(port)].name
+            return self.get_southbound_port(port).name
 
         if self.is_uni_port(port):
             return self.northbound_ports[port].name
 
         if self.is_logical_port(port):
-            raise NotImplemented('TODO: Logical ports not yet supported')
\ No newline at end of file
+            raise NotImplemented('TODO: Logical ports not yet supported')
+
+    def get_xpon_info(self, pon_id, pon_id_type='xgs-ponid'):
+        """
+        Lookup all xPON configuraiton data for a specific pon-id / channel-termination
+        :param pon_id: (int) PON Identifier
+        :return: (dict) reduced xPON information for the specific PON port
+        """
+        terminations = {key: val for key, val in self._channel_terminations.iteritems()
+                        if val[pon_id_type] == pon_id}
+
+        pair_names = set([term['channel-pair'] for term in terminations.itervalues()])
+
+        pairs = {key: val for key, val in self._channel_pairs.iteritems()
+                 if key in pair_names}
+
+        partition_names = set([pair['channel-partition'] for pair in pairs.itervalues()])
+
+        partitions = {key: val for key, val in self._channel_partitions.iteritems()
+                      if key in partition_names}
+
+        v_ont_anis = {key: val for key, val in self._v_ont_anis.iteritems()
+                      if val['preferred-channel-pair'] in pair_names}
+
+        return {
+            'channel-terminations': terminations,
+            'channel-pairs': pairs,
+            'channel-partitions': partitions,
+            'v_ont_anis': v_ont_anis
+        }
+
+    def create_interface(self, device, data):
+        """
+        Create XPON interfaces
+        :param device: (Device)
+        :param data: (ChannelgroupConfig) Channel Group configuration
+        """
+        name = data.name
+        interface = data.interface
+        inst_data = data.data
+
+        if isinstance(data, ChannelgroupConfig):
+            self.log.debug('create_interface-channel-group', interface=interface, data=inst_data)
+            self._channel_groups[name] = {
+                'name': name,
+                'enabled': interface.enabled,
+                'system-id': inst_data.system_id,
+                'polling-period': inst_data.polling_period
+            }
+
+        elif isinstance(data, ChannelpartitionConfig):
+            self.log.debug('create_interface-channel-partition', interface=interface, data=inst_data)
+
+            def _auth_method_enum_to_string(value):
+                from voltha.protos.bbf_fiber_types_pb2 import SERIAL_NUMBER, LOID, \
+                    REGISTRATION_ID, OMCI, DOT1X
+                return {
+                    SERIAL_NUMBER: 'serial-number',
+                    LOID: 'loid',
+                    REGISTRATION_ID: 'registation-id',
+                    OMCI: 'omci',
+                    DOT1X: 'don1x'
+                }.get(value, 'unknown')
+
+            self._channel_partitions[name] = {
+                'name': name,
+                'enabled': interface.enabled,
+                'authentication-method': _auth_method_enum_to_string(inst_data.authentication_method),
+                'channel-group': inst_data.channelgroup_ref,
+                'fec-downstream': inst_data.fec_downstream,
+                'mcast-aes': inst_data.multicast_aes_indicator,
+                'differential-fiber-distance': inst_data.differential_fiber_distance
+            }
+
+        elif isinstance(data, ChannelpairConfig):
+            self.log.debug('create_interface-channel-pair', interface=interface, data=inst_data)
+            self._channel_pairs[name] = {
+                'name': name,
+                'enabled': interface.enabled,
+                'channel-group': inst_data.channelgroup_ref,
+                'channel-partition': inst_data.channelpartition_ref,
+                'line-rate': inst_data.channelpair_linerate
+            }
+
+        elif isinstance(data, ChannelterminationConfig):
+            self.log.debug('create_interface-channel-termination', interface=interface, data=inst_data)
+            self._channel_terminations[name] = {
+                'name': name,
+                'enabled': interface.enabled,
+                'xgs-ponid': inst_data.xgs_ponid,
+                'xgpon-ponid': inst_data.xgpon_ponid,
+                'channel-pair': inst_data.channelpair_ref,
+                'ber-calc-period': inst_data.ber_calc_period
+            }
+            self.on_channel_termination_config(name, 'create')
+
+        elif isinstance(data, OntaniConfig):
+            self.log.debug('create_interface-ont-ani', interface=interface, data=inst_data)
+            self._ont_anis[name] = {
+                'name': name,
+                'enabled': interface.enabled,
+                'upstream-fec': inst_data.upstream_fec_indicator,
+                'mgnt-gemport-aes': inst_data.mgnt_gemport_aes_indicator
+            }
+
+        elif isinstance(data, VOntaniConfig):
+            self.log.debug('create_interface-v-ont-ani', interface=interface, data=inst_data)
+            self._v_ont_anis[name] = {
+                'name': name,
+                'enabled': interface.enabled,
+                'onu-id': inst_data.onu_id,
+                'expected-serial-number': inst_data.expected_serial_number,
+                'preferred-channel-pair': inst_data.preferred_chanpair,
+                'channel-partition': inst_data.parent_ref,
+                'upstream-channel-speed': inst_data.upstream_channel_speed
+            }
+
+        elif isinstance(data, VEnetConfig):
+            self.log.debug('create_interface-v-enet', interface=interface, data=inst_data)
+            self._v_enets[name] = {
+                'name': name,
+                'enabled': interface.enabled,
+                'v-ont-ani': inst_data.v_ontani_ref
+            }
+
+        else:
+            raise NotImplementedError('Unknown data type')
+
+    def on_channel_termination_config(self, name, operation, pon_type='xgs-ponid'):
+        supported_operations = ['create']
+
+        assert operation in supported_operations
+        assert name in self._channel_terminations
+        ct = self._channel_terminations[name]
+
+        pon_id = ct[pon_type]
+        # Look up the southbound PON port
+
+        pon_port = self.southbound_ports.get(pon_id, None)
+        if pon_port is None:
+            raise ValueError('Unknown PON port. PON-ID: {}'.format(pon_id))
+
+        assert ct['channel-pair'] in self._channel_pairs
+        cpair = self._channel_pairs[ct['channel-pair']]
+
+        assert cpair['channel-group'] in self._channel_groups
+        assert cpair['channel-partition'] in self._channel_partitions
+        cg = self._channel_groups[cpair['channel-group']]
+        cpart = self._channel_partitions[cpair['channel-partition']]
+
+        enabled = ct['enabled']
+        
+        polling_period = cg['polling-period']
+        authentication_method = cpart['authentication-method']
+        # line_rate = cpair['line-rate']
+        # downstream_fec = cpart['fec-downstream']
+        # deployment_range = cpart['differential-fiber-distance']
+        # mcast_aes = cpart['mcast-aes']
+
+        # TODO: Support BER calculation period
+        # TODO support FEC, and MCAST AES settings
+        # TODO Support setting of line rate
+
+        if operation == 'create':
+            pon_port.xpon_name = name
+            pon_port.discovery_tick = polling_period
+            pon_port.authentication_method = authentication_method
+            # pon_port.deployment_range = deployment_range
+            # pon_port.fec_enable = downstream_fec
+            # pon_port.mcast_aes = mcast_aes
+
+            if enabled:
+                pon_port.start()
+            else:
+                pon_port.stop()
diff --git a/voltha/adapters/adtran_olt/codec/ietf_interfaces.py b/voltha/adapters/adtran_olt/codec/ietf_interfaces.py
index 332a49b..5cf9c79 100644
--- a/voltha/adapters/adtran_olt/codec/ietf_interfaces.py
+++ b/voltha/adapters/adtran_olt/codec/ietf_interfaces.py
@@ -1,18 +1,16 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# 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.
-#
 
 from twisted.internet.defer import inlineCallbacks, returnValue
 import xmltodict
@@ -90,9 +88,9 @@
         if interface_type is None:
             return entries
 
-        for entry in entries:
-            import pprint
-            log.info(pprint.PrettyPrinter(indent=2).pformat(entry))
+        # for entry in entries:
+        #     import pprint
+        #     log.info(pprint.PrettyPrinter(indent=2).pformat(entry))
 
         def _matches(entry, value):
             if 'type' in entry and '#text' in entry['type']:
diff --git a/voltha/adapters/adtran_olt/codec/olt_config.py b/voltha/adapters/adtran_olt/codec/olt_config.py
index f5bfb7b..89a4afe 100644
--- a/voltha/adapters/adtran_olt/codec/olt_config.py
+++ b/voltha/adapters/adtran_olt/codec/olt_config.py
@@ -1,18 +1,17 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
+
 import pprint
 
 import os
@@ -26,7 +25,6 @@
     Class to wrap decode of olt container (config) from the ADTRAN
     gpon-olt-hw.yang YANG model
     """
-
     def __init__(self, packet):
         self._packet = packet
         self._pons = None
@@ -54,7 +52,6 @@
         """
         Provides decode of PON list from within
         """
-
         def __init__(self, packet):
             assert 'pon-id' in packet
             self._packet = packet
@@ -65,8 +62,6 @@
 
         @staticmethod
         def decode(pon_list):
-            log.info('Decoding PON List:{}{}'.format(os.linesep,
-                                                     pprint.PrettyPrinter().pformat(pon_list)))
             pons = {}
             for pon_data in pon_list:
                 pon = OltConfig.Pon(pon_data)
@@ -110,18 +105,17 @@
             """
             Provides decode of onu list for a PON port
             """
-
             def __init__(self, packet):
                 assert 'onu-id' in packet
                 self._packet = packet
+                self._tconts = None
+                self._gem_ports = None
 
             def __str__(self):
                 return "OltConfig.Pon.Onu: onu-id: {}".format(self.onu_id)
 
             @staticmethod
             def decode(onu_list):
-                log.debug('onus:{}{}'.format(os.linesep,
-                                             pprint.PrettyPrinter().pformat(onu_list)))
                 onus = {}
                 for onu_data in onu_list:
                     onu = OltConfig.Pon.Onu(onu_data)
@@ -150,4 +144,148 @@
                 """If true, places the ONU in service"""
                 return self._packet.get('enable', False)
 
-                # TODO: TCONT and GEM lists
+            @property
+            def tconts(self):
+                if self._tconts is None:
+                    self._tconts = OltConfig.Pon.Onu.TCont.decode(self._packet.get('t-conts', None))
+                return self._tconts
+
+            @property
+            def gem_ports(self):
+                if self._gem_ports is None:
+                    self._gem_ports = OltConfig.Pon.Onu.GemPort.decode(self._packet.get('gem-ports', None))
+                return self._tconts
+
+            class TCont(object):
+                """
+                Provides decode of onu list for the T-CONT container
+                """
+                def __init__(self, packet):
+                    assert 'alloc-id' in packet
+                    self._packet = packet
+                    self._traffic_descriptor = None
+                    self._best_effort = None
+
+                def __str__(self):
+                    return "OltConfig.Pon.Onu.TCont: alloc-id: {}".format(self.alloc_id)
+
+                @staticmethod
+                def decode(tcont_container):
+                    tconts = {}
+                    for tcont_data in tcont_container.get('t-cont', []):
+                        tcont = OltConfig.Pon.Onu.TCont(tcont_data)
+                        assert tcont.alloc_id not in tconts
+                        tconts[tcont.alloc_id] = tcont
+
+                    return tconts
+
+                @property
+                def alloc_id(self):
+                    """The ID used to identify the T-CONT"""
+                    return self._packet['alloc-id']
+
+                @property
+                def traffic_descriptor(self):
+                    """
+                    Each Alloc-ID is provisioned with a traffic descriptor that specifies
+                    the three bandwidth component parameters: fixed bandwidth, assured
+                    bandwidth, and maximum bandwidth, as well as the ternary eligibility
+                    indicator for additional bandwidth assignment
+                    """
+                    if self._traffic_descriptor is None and 'traffic-descriptor' in self._packet:
+                        self._traffic_descriptor = OltConfig.Pon.Onu.TCont.\
+                            TrafficDescriptor(self._packet['traffic-descriptor'])
+                    return self._traffic_descriptor
+
+                class TrafficDescriptor(object):
+                    def __init__(self, packet):
+                        self._packet = packet
+
+                    def __str__(self):
+                        return "OltConfig.Pon.Onu.TCont.TrafficDescriptor: {}/{}/{}".\
+                            format(self.fixed_bandwidth, self.assured_bandwidth,
+                                   self.maximum_bandwidth)
+
+                    @property
+                    def fixed_bandwidth(self):
+                        return self._packet['fixed-bandwidth']
+
+                    @property
+                    def assured_bandwidth(self):
+                        return self._packet['assured-bandwidth']
+
+                    @property
+                    def maximum_bandwidth(self):
+                        return self._packet['maximum-bandwidth']
+
+                    @property
+                    def additional_bandwidth_eligibility(self):
+                        return self._packet.get('additional-bandwidth-eligibility', 'none')
+
+                @property
+                def best_effort(self):
+                    if self._best_effort is None:
+                        self._best_effort = OltConfig.Pon.Onu.TCont.BestEffort.decode(
+                            self._packet.get('best-effort', None))
+                    return self._best_effort
+
+                class BestEffort(object):
+                    def __init__(self, packet):
+                        self._packet = packet
+
+                    def __str__(self):
+                        return "OltConfig.Pon.Onu.TCont.BestEffort: {}".format(self.bandwidth)
+
+                    @property
+                    def bandwidth(self):
+                        return self._packet['bandwidth']
+
+                    @property
+                    def priority(self):
+                        return self._packet['priority']
+
+                    @property
+                    def weight(self):
+                        return self._packet['weight']
+
+            class GemPort(object):
+                """
+                Provides decode of onu list for the gem-ports container
+                """
+                def __init__(self, packet):
+                    assert 'port-id' in packet
+                    self._packet = packet
+
+                def __str__(self):
+                    return "OltConfig.Pon.Onu.GemPort: port-id: {}/{}".\
+                        format(self.port_id, self.alloc_id)
+
+                @staticmethod
+                def decode(gem_port_container):
+                    gem_ports = {}
+                    for gem_port_data in gem_port_container.get('gem-port', []):
+                        gem_port = OltConfig.Pon.Onu.GemPort(gem_port_data)
+                        assert gem_port.port_id not in gem_port
+                        gem_ports[gem_port.port_id] = gem_port
+
+                    return gem_ports
+
+                @property
+                def port_id(self):
+                    """The ID used to identify the GEM Port"""
+                    return self._packet['port-id']
+
+                @property
+                def alloc_id(self):
+                    """The Alloc-ID of the T-CONT to which this GEM port is mapped"""
+                    return self._packet['alloc-id']
+
+                @property
+                def omci_transport(self):
+                    """If true, this GEM port is used to transport the OMCI virtual connection"""
+                    return self._packet.get('omci-transport', False)
+
+                @property
+                def encryption(self):
+                    """If true, enable encryption using the advanced encryption standard(AES)"""
+                    return self._packet.get('encryption', False)
diff --git a/voltha/adapters/adtran_olt/codec/physical_entities_state.py b/voltha/adapters/adtran_olt/codec/physical_entities_state.py
index ed0156f..947eab6 100644
--- a/voltha/adapters/adtran_olt/codec/physical_entities_state.py
+++ b/voltha/adapters/adtran_olt/codec/physical_entities_state.py
@@ -1,3 +1,17 @@
+# 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.
+
 from ..net.adtran_netconf import adtran_module_url
 from twisted.internet.defer import inlineCallbacks, returnValue
 import xmltodict
@@ -21,7 +35,7 @@
 
     @inlineCallbacks
     def get_state(self):
-        self._rpc_reply =  None
+        self._rpc_reply = None
         request = self._session.get(_phys_entities_rpc)
         self._rpc_reply = yield request
         returnValue(self._rpc_reply)
diff --git a/voltha/adapters/adtran_olt/flow/evc.py b/voltha/adapters/adtran_olt/flow/evc.py
index e350db1..4fa404c 100644
--- a/voltha/adapters/adtran_olt/flow/evc.py
+++ b/voltha/adapters/adtran_olt/flow/evc.py
@@ -1,32 +1,30 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
 
+import xmltodict
+import re
 from enum import Enum
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from voltha.core.flow_decomposer import *
 
 log = structlog.get_logger()
 
-EVC_NAME_FORMAT = 'EVC-VOLTHA-{}-{}'
-EVC_NAME_REGEX = 'EVC-VOLTHA-{}'.format('regex-here')
+EVC_NAME_FORMAT = 'VOLTHA-{}'                       # format(flow.id)
+EVC_NAME_REGEX_ALL = EVC_NAME_FORMAT.format('*')
 DEFAULT_STPID = 0x8100
 
-_xml_header = '<evcs xmlns="http://www.adtran.com/ns/yang/adtran-evcs"><evc>'
-_xml_trailer = '</evc></evcs>'
-
 
 class EVC(object):
     """
@@ -54,17 +52,17 @@
             raise ValueError('Invalid SwitchingMethod enumeration')
 
     class Men2UniManipulation(Enum):
-        SYMETRIC = 1
+        SYMMETRIC = 1
         POP_OUT_TAG_ONLY = 2
-        DEFAULT = SYMETRIC
+        DEFAULT = SYMMETRIC
 
         @staticmethod
         def xml(value):
             if value is None:
                 value = EVC.Men2UniManipulation.DEFAULT
             fmt = '<men-to-uni-tag-manipulation>{}</men-to-uni-tag-manipulation>'
-            if value == EVC.Men2UniManipulation.SYMETRIC:
-                return fmt.format('<symetric/>')
+            if value == EVC.Men2UniManipulation.SYMMETRIC:
+                return fmt.format('<symmetric/>')
             elif value == EVC.Men2UniManipulation.POP_OUT_TAG_ONLY:
                 return fmt.format('<pop-outer-tag-only/>')
             raise ValueError('Invalid Men2UniManipulation enumeration')
@@ -73,16 +71,18 @@
         NNI_TO_UNI = 1
         UNI_TO_NNI = 2
         NNI_TO_NNI = 3
-        ACL_FILTER = 4
-        UNKNOWN = 5
-        UNSUPPORTED = 5    # Or Invalid
+        UNI_TO_UNI = 4
+        ACL_FILTER = 5
+        UNKNOWN = 6
+        UNSUPPORTED = 7    # Or Invalid
 
     def __init__(self, flow_entry):
         self._installed = False
         self._status_message = None
         self._flow = flow_entry
         self._name = self._create_name()
-        self._evc_maps = {}             # Map Name -> evc-map
+        self._evc_maps = {}               # Map Name -> evc-map
+        self._install_deferred = None
 
         self._flow_type = EVC.ElineFlowType.UNKNOWN
 
@@ -103,11 +103,14 @@
             log.exception('Failure during EVC decode', e=e)
             self._valid = False
 
+    def __str__(self):
+        return "EVC-{}: MEN: {}, S-Tag: {}".format(self._name, self._men_ports, self._s_tag)
+
     def _create_name(self):
         #
         # TODO: Take into account selection criteria and output to make the name
         #
-        return EVC_NAME_FORMAT.format(self._flow.device_id, self._flow.flow_id)
+        return EVC_NAME_FORMAT.format(self._flow.flow_id)
 
     @property
     def name(self):
@@ -158,7 +161,7 @@
     @ce_vlan_preservation.setter
     def ce_vlan_preservation(self, value):
         assert self._ce_vlan_preservation is None or self._ce_vlan_preservation == value
-        self.ce_vlan_preservation = value
+        self._ce_vlan_preservation = value
 
     @property
     def men_to_uni_tag_manipulation(self):
@@ -189,14 +192,40 @@
         if self._evc_maps is not None and evc_map.name in self._evc_maps:
             del self._evc_maps[evc_map.name]
 
+    def schedule_install(self):
+        """
+        Try to install EVC and all MAPs in a single operational sequence
+        """
+        if self._valid and self._install_deferred is None:
+                self._install_deferred = reactor.callLater(0, self._do_install)
+
+        return self._install_deferred
+
+    @staticmethod
+    def _xml_header(operation=None):
+        return '<evcs xmlns="http://www.adtran.com/ns/yang/adtran-evcs"><evc{}>'.\
+            format('' if operation is None else ' operation="{}"'.format(operation))
+
+    @staticmethod
+    def _xml_trailer():
+        return '</evc></evcs>'
+
     @inlineCallbacks
-    def install(self):
+    def _do_install(self):
+        self._install_deferred = None
+
+        # Install the EVC if needed
+
         if self._valid and not self._installed:
-            xml = _xml_header
+            # TODO: Currently install EVC and then MAPs. Can do it all in a single edit-config operation
+
+            xml = EVC._xml_header()
             xml += '<name>{}</name>'.format(self.name)
-            xml += '<enabled>{}</enabled>'.format(self._enabled)
-            xml += '<ce-vlan-preservation>{}</ce-vlan-preservation>'.\
-                format(self._ce_vlan_preservation or True)
+            xml += '<enabled>{}</enabled>'.format('true' if self._enabled else 'false')
+
+            if self._ce_vlan_preservation is not None:
+                xml += '<ce-vlan-preservation>{}</ce-vlan-preservation>'.\
+                    format('true' if self._ce_vlan_preservation else 'false')
 
             if self._s_tag is not None:
                 xml += '<stag>{}</stag>'.format(self._s_tag)
@@ -209,15 +238,16 @@
 
             xml += EVC.Men2UniManipulation.xml(self._men_to_uni_tag_manipulation)
             xml += EVC.SwitchingMethod.xml(self._switching_method)
-            xml += _xml_trailer
+            xml += EVC._xml_trailer()
 
             log.debug("Creating EVC {}: '{}'".format(self.name, xml))
 
             try:
-                results = yield self._flow.handler.netconf_client.edit_config(xml,
-                                                                              default_operation='create',
-                                                                              lock_timeout=30)
+                # Set installed to true while request is in progress
+                self._installed = True
+                results = yield self._flow.handler.netconf_client.edit_config(xml, lock_timeout=30)
                 self._installed = results.ok
+
                 if results.ok:
                     self.status = ''
                 else:
@@ -227,19 +257,33 @@
                 log.exception('Failed to install EVC', name=self.name, e=e)
                 raise
 
+        # Install any associated EVC Maps
+
+        if self._installed:
+            for evc_map in self.evc_maps:
+                try:
+                    results = yield evc_map.install()
+                    pass  # TODO: What to do on error?
+
+                except Exception as e:
+                    evc_map.status = 'Exception during EVC-MAP Install: {}'.format(e.message)
+                    log.exception(evc_map.status, e=e)
+
         returnValue(self._installed and self._valid)
 
     @inlineCallbacks
     def remove(self):
-        if self._installed:
-            xml = _xml_header + '<name>{}</name>'.format(self.name) + _xml_trailer
+        d, self._install_deferred = self._install_deferred, None
+        if d is not None:
+            d.cancel()
 
-            log.debug("Deleting EVC {}: '{}'".format(self.name, xml))
+        if self._installed:
+            xml = EVC._xml_header('delete') + '<name>{}</name>'.format(self.name) + EVC._xml_trailer()
+
+            log.debug('removing', evc=self.name, xml=xml)
 
             try:
-                results = yield self._flow.handler.netconf_client.edit_config(xml,
-                                                                              default_operation='delete',
-                                                                              lock_timeout=30)
+                results = yield self._flow.handler.netconf_client.edit_config(xml, lock_timeout=30)
                 self._installed = not results.ok
                 if results.ok:
                     self.status = ''
@@ -247,7 +291,7 @@
                     self.status = results.error             # TODO: Save off error status
 
             except Exception as e:
-                log.exception('Failed to remove EVC', name=self.name, e=e)
+                log.exception('removing', name=self.name, e=e)
                 raise
 
             # TODO: Do we remove evc-maps as well reference here or maybe have a 'delete' function?
@@ -258,15 +302,13 @@
     @inlineCallbacks
     def enable(self):
         if self.installed and not self._enabled:
-            xml = _xml_header + '<name>{}</name>'.format(self.name)
-            xml += '<enabled>true</enabled>' + _xml_trailer
+            xml = EVC._xml_header() + '<name>{}</name>'.format(self.name)
+            xml += '<enabled>true</enabled>' + EVC._xml_trailer()
 
-            log.debug("Enabling EVC {}: '{}'".format(self.name, xml))
+            log.debug('enabling', evc=self.name, xml=xml)
 
             try:
-                results = yield self._flow.handler.netconf_client.edit_config(xml,
-                                                                              default_operation='merge',
-                                                                              lock_timeout=30)
+                results = yield self._flow.handler.netconf_client.edit_config(xml, lock_timeout=30)
                 self._enabled = results.ok
                 if results.ok:
                     self.status = ''
@@ -274,7 +316,7 @@
                     self.status = results.error       # TODO: Save off error status
 
             except Exception as e:
-                log.exception('Failed to enable EVC', name=self.name, e=e)
+                log.exception('enabling', name=self.name, e=e)
                 raise
 
         returnValue(self.installed and self._enabled)
@@ -282,15 +324,13 @@
     @inlineCallbacks
     def disable(self):
         if self.installed and self._enabled:
-            xml = _xml_header + '<name>{}</name>'.format(self.name)
-            xml += '<enabled>false</enabled>' + _xml_trailer
+            xml = EVC._xml_header() + '<name>{}</name>'.format(self.name)
+            xml += '<enabled>false</enabled>' + EVC._xml_trailer()
 
-            log.debug("Disabling EVC {}: '{}'".format(self.name, xml))
+            log.debug('disabling', evc=self.name, xml=xml)
 
             try:
-                results = yield self._flow.handler.netconf_client.edit_config(xml,
-                                                                              default_operation='merge',
-                                                                              lock_timeout=30)
+                results = yield self._flow.handler.netconf_client.edit_config(xml, lock_timeout=30)
                 self._enabled = not results.ok
                 if results.ok:
                     self.status = ''
@@ -298,7 +338,7 @@
                     self.status = results.error      # TODO: Save off error status
 
             except Exception as e:
-                log.exception('Failed to disable EVC', name=self.name, e=e)
+                log.exception('disabling', name=self.name, e=e)
                 raise
 
         returnValue(self.installed and not self._enabled)
@@ -334,8 +374,8 @@
 
         self._s_tag = self._flow.vlan_id
 
-        if self._flow.inner_vid is not None:
-            self._switching_method = EVC.SwitchingMethod.DOUBLE_TAGGED
+        # if self._flow.inner_vid is not None:
+        #    self._switching_method = EVC.SwitchingMethod.DOUBLE_TAGGED         TODO: Future support
 
         # Note: The following fields will get set when the first EVC-MAP
         #       is associated with this object. Once set, they cannot be changed to
@@ -349,19 +389,62 @@
     # BULK operations
 
     @staticmethod
-    def enable_all(regex_=EVC_NAME_REGEX):
-        raise NotImplemented("TODO: Implement this")
-
-    @staticmethod
-    def disable_all(regex_=EVC_NAME_REGEX):
-        raise NotImplemented("TODO: Implement this")
-
-    @staticmethod
-    def remove_all(regex_=EVC_NAME_REGEX):
+    def remove_all(client, regex_=EVC_NAME_REGEX_ALL):
         """
-        Remove all matching EVCs and associated EVC MAPs from hardware
-
+        Remove all matching EVCs from hardware
+        :param client: (ncclient) NETCONF Client to use
         :param regex_: (String) Regular expression for name matching
+        :return: (deferred)
         """
-        raise NotImplemented("TODO: Implement this")
+        # Do a 'get' on the evc config an you should get the names
+        get_xml = """
+        <filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+          <evcs xmlns="http://www.adtran.com/ns/yang/adtran-evcs">
+            <evc><name/></evc>
+          </evcs>
+        </filter>
+        """
+        log.debug('query', xml=get_xml)
 
+        def request_failed(results, operation):
+            log.error('{}-failed'.format(operation), results=results)
+            # No further actions. Periodic poll later on will scrub any old EVCs if needed
+
+        def delete_complete(results):
+            log.debug('delete-complete', results=results)
+
+        def do_delete(rpc_reply, regexpr):
+            log.debug('query-complete', rpc_reply=rpc_reply)
+
+            if rpc_reply.ok:
+                result_dict = xmltodict.parse(rpc_reply.data_xml)
+                entries = result_dict['data']['evcs'] if 'evcs' in result_dict['data'] else {}
+
+                if 'evc' in entries:
+                    p = re.compile(regexpr)
+
+                    if isinstance(entries['evc'], list):
+                        names = {entry['name'] for entry in entries['evc'] if 'name' in entry
+                                 and p.match(entry['name'])}
+                    else:
+                        names = set()
+                        for item in entries['evc-map'].items():
+                            if isinstance(item, tuple) and item[0] == 'name':
+                                names.add(item[1])
+                                break
+
+                    if len(names) > 0:
+                        del_xml = EVC._xml_header('delete')
+                        for name in names:
+                            del_xml += '<name>{}</name>'.format(name)
+                            del_xml += EVC._xml_trailer()
+
+                        log.debug('removing', xml=del_xml)
+                        return client.edit_config(del_xml, lock_timeout=30)
+
+            return succeed('no entries')
+
+        d = client.get(get_xml)
+        d.addCallbacks(do_delete, request_failed, callbackArgs=[regex_], errbackArgs=['get'])
+        d.addCallbacks(delete_complete, request_failed, errbackArgs=['edit-config'])
+        return d
diff --git a/voltha/adapters/adtran_olt/flow/evc_map.py b/voltha/adapters/adtran_olt/flow/evc_map.py
index d5e29ad..14a66b1 100644
--- a/voltha/adapters/adtran_olt/flow/evc_map.py
+++ b/voltha/adapters/adtran_olt/flow/evc_map.py
@@ -1,33 +1,32 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
 
+import xmltodict
+import re
 import structlog
 from enum import Enum
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 
 log = structlog.get_logger()
 
-EVC_MAP_NAME_INGRESS_FORMAT = 'EVCMap-VOLTHA-ingress-{}'
-EVC_MAP_NAME_EGRESS_FORMAT = 'EVCMap-VOLTHA-egress-{}'
+# NOTE: For the EVC Map name, the ingress-port number is the VOLTHA port number (not pon-id since
+#       it covers NNI ports as well in order to handle the NNI-NNI case.  For flows that
+#       cover an entire pon, the name will have the ONU ID and GEM ID appended to it upon
+#       installation with a period as a separator.
 
-EVC_MAP_NAME_INGRESS_REGEX_FORMAT = EVC_MAP_NAME_INGRESS_FORMAT.format('regex here')
-EVC_MAP_NAME_EGRESS_REGEX_FORMAT = EVC_MAP_NAME_EGRESS_FORMAT.format('regex here')
-
-_xml_header = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"><evc-map>'
-_xml_trailer = '</evc-map></evc-maps>'
+EVC_MAP_NAME_FORMAT = 'VOLTHA-{}-{}'   # format(ingress-port, flow.id)
+EVC_MAP_NAME_REGEX_ALL = 'VOLTHA-*'
 
 
 class EVCMap(object):
@@ -59,6 +58,7 @@
     def __init__(self, flow, evc, is_ingress_map):
         self._flow = flow
         self._evc = evc
+        self._gem_ids = None
         self._is_ingress_map = is_ingress_map
         self._installed = False
         self._status_message = None
@@ -96,7 +96,7 @@
             self._valid = self._decode()
 
         except Exception as e:
-            log.exception('Failure during EVCMap decode', e=e)
+            log.exception('decode', e=e)
             self._valid = False
 
         if self._valid:
@@ -104,6 +104,10 @@
         else:
             self._evc = None
 
+    def __str__(self):
+        return "EVCMap-{}: UNI: {}, isACL: {}".format(self._name, self._uni_port,
+                                                      self._needs_acl_support)
+
     @staticmethod
     def create_ingress_map(flow, evc):
         return EVCMap(flow, evc, True)
@@ -134,16 +138,27 @@
 
     @property
     def _needs_acl_support(self):
-        return self._eth_type is None and self._ip_protocol is None and\
-               self._ipv4_dst is None and self._udp_dst is None and self._udp_src is None
+        return self._eth_type is not None or self._ip_protocol is not None or\
+                self._ipv4_dst is not None or self._udp_dst is not None or self._udp_src is not None
+
+    @staticmethod
+    def _xml_header(operation=None):
+        return '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"><evc-map{}>'.\
+            format('' if operation is None else ' operation="{}"'.format(operation))
+
+    @staticmethod
+    def _xml_trailer():
+        return '</evc-map></evc-maps>'
 
     @inlineCallbacks
     def install(self):
-        if self._valid and not self._installed:
-            xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps">' \
-                  '<evc-map>'
+        if self._gem_ids is not None:
+            self.pon_install()
+
+        elif self._valid and not self._installed:
+            xml = EVCMap._xml_header()
             xml += '<name>{}</name>'.format(self.name)
-            xml += '<enabled>{}</enabled>'.format(self._enabled)
+            xml += '<enabled>{}</enabled>'.format('true' if self._enabled else 'false')
             xml += '<uni>{}</uni>'.format(self._uni_port)
 
             if self._evc_name is not None:
@@ -156,17 +171,15 @@
             elif self._c_tag is not None:
                 xml += '<ctag>{}</ctag>'.format(self._c_tag)
 
-            xml += _xml_trailer
+            xml += EVCMap._xml_trailer()
 
-            log.debug("Creating EVC-MAP {}: '{}'".format(self.name, xml))
+            log.debug('creating', name=self.name, xml=xml)
 
             if self._needs_acl_support:
-                self._installed = True              # TODO: Support ACLs
+                self._installed = True               # TODO: Support ACLs
             else:
                 try:
-                    results = yield self._flow.handler.netconf_client.edit_config(xml,
-                                                                                  default_operation='create',
-                                                                                  lock_timeout=30)
+                    results = yield self._flow.handler.netconf_client.edit_config(xml, lock_timeout=30)
                     self._installed = results.ok
                     if results.ok:
                         self.status = ''
@@ -174,7 +187,7 @@
                         self.status = results.error        # TODO: Save off error status
 
                 except Exception as e:
-                    log.exception('Failed to install EVC-MAP', name=self.name, e=e)
+                    log.exception('install', name=self.name, e=e)
                     raise
 
         # TODO: The following is not yet supported
@@ -188,11 +201,77 @@
         # self._match_ce_vlan_id = None
         # self._match_untagged = True
         # self._match_destination_mac_address = None
-        # self._match_l2cp = False
-        # self._match_broadcast = False
-        # self._match_multicast = False
-        # self._match_unicast = False
-        # self._match_igmp = False
+        # self._eth_type = None
+        # self._ip_protocol = None
+        # self._ipv4_dst = None
+        # self._udp_dst = None
+        # self._udp_src = None
+
+        returnValue(self._installed and self._valid)
+
+    @inlineCallbacks
+    def pon_install(self):
+        """
+        Install a flow on all ONU's of a PON port
+        """
+        from ..onu import Onu
+
+        if self._valid and not self._installed:
+            # Install in per ONU batches
+
+            self._installed = True
+
+            for onu_id, gem_ids in self._gem_ids.iteritems():
+                xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps">'
+
+                for gem_id in gem_ids:
+                    xml += '<evc-map>'
+                    xml += '<name>{}.{}.{}</name>'.format(self.name, onu_id, gem_id)
+                    xml += '<enabled>{}</enabled>'.format('true' if self._enabled else 'false')
+                    xml += '<uni>{}</uni>'.format(self._uni_port)
+
+                    if self._evc_name is not None:
+                        xml += '<evc>{}</evc>'.format(self._evc_name)
+                    else:
+                        xml += EVCMap.EvcConnection.xml(self._evc_connection)
+
+                    xml += '<ce-vlan-id>{}</ce-vlan-id>'.format(Onu.gem_id_to_gvid(gem_id))
+
+                    # if self._match_untagged:
+                    #    xml += '<match-untagged>True</match-untagged>'
+                    if self._c_tag is not None:
+                        xml += '<ctag>{}</ctag>'.format(self._c_tag)
+
+                    xml += '</evc-map>'
+                xml += '</evc-maps>'
+
+                log.debug('creating', name=self.name, onu_id=onu_id, xml=xml)
+
+                try:
+                    # Set installed to true while request is in progress
+                    results = yield self._flow.handler.netconf_client.edit_config(xml, lock_timeout=30)
+                    self._installed = results.ok   # TODO: Need per-ONU results?
+
+                    if results.ok:
+                        self.status = ''
+                    else:
+                        self.status = results.error        # TODO: Save off error status
+
+                except Exception as e:
+                    log.exception('install', name=self.name, onu_id=onu_id, e=e)
+                    self._installed = False
+                    raise
+
+        # TODO: The following is not yet supported
+        # self._men_priority = EVCMap.PriorityOption.INHERIT_PRIORITY
+        # self._men_pri = 0  # If Explicit Priority
+        #
+        # self._c_tag = None
+        # self._men_ctag_priority = EVCMap.PriorityOption.INHERIT_PRIORITY
+        # self._men_ctag_pri = 0  # If Explicit Priority
+        #
+        # self._match_untagged = True
+        # self._match_destination_mac_address = None
         # self._eth_type = None
         # self._ip_protocol = None
         # self._ipv4_dst = None
@@ -204,16 +283,16 @@
     @inlineCallbacks
     def remove(self):
         if self._installed:
-            xml = _xml_header + '<name>{}</name>'.format(self.name) + _xml_trailer
+            xml = EVCMap._xml_header('remove') + '<name>{}</name>'.format(self.name) + EVCMap._xml_trailer()
 
-            log.debug("Deleting EVC-MAP {}: '{}'".format(self.name, xml))
+            log.debug('removing', name=self.name, xml=xml)
 
             if self._needs_acl_support:
                 self._installed = False              # TODO: Support ACLs
             else:
                 try:
                     results = yield self._flow.handler.netconf_client.edit_config(xml,
-                                                                                  default_operation='delete',
+                                                                                  default_operation='remove',
                                                                                   lock_timeout=30)
                     self._installed = not results.ok
                     if results.ok:
@@ -222,7 +301,7 @@
                         self.status = results.error      # TODO: Save off error status
 
                 except Exception as e:
-                    log.exception('Failed to remove EVC-MAP', name=self.name, e=e)
+                    log.exception('removing', name=self.name, e=e)
                     raise
 
             # TODO: Do we remove evc reference here or maybe have a 'delete' function?
@@ -232,18 +311,16 @@
     @inlineCallbacks
     def enable(self):
         if self.installed and not self._enabled:
-            xml = _xml_header + '<name>{}</name>'.format(self.name)
-            xml += '<enabled>true</enabled>' + _xml_trailer
+            xml = EVCMap._xml_header() + '<name>{}</name>'.format(self.name)
+            xml += '<enabled>true</enabled>' + EVCMap._xml_trailer()
 
-            log.debug("Enabling EVC-MAP {}: '{}'".format(self.name, xml))
+            log.debug('enabling', name=self.name, xml=xml)
 
             if self._needs_acl_support:
                 self._enabled = True             # TODO: Support ACLs
             else:
                 try:
-                    results = yield self._flow.handler.netconf_client.edit_config(xml,
-                                                                                  default_operation='merge',
-                                                                                  lock_timeout=30)
+                    results = yield self._flow.handler.netconf_client.edit_config(xml, lock_timeout=30)
                     self._enabled = results.ok
                     if results.ok:
                         self.status = ''
@@ -251,7 +328,7 @@
                         self.status = results.error      # TODO: Save off error status
 
                 except Exception as e:
-                    log.exception('Failed to enable EVC-MAP', name=self.name, e=e)
+                    log.exception('enabling', name=self.name, e=e)
                     raise
 
         returnValue(self.installed and self._enabled)
@@ -259,18 +336,16 @@
     @inlineCallbacks
     def disable(self):
         if self.installed and self._enabled:
-            xml = _xml_header + '<name>{}</name>'.format(self.name)
-            xml += '<enabled>false</enabled>' + _xml_trailer
+            xml = EVCMap._xml_header() + '<name>{}</name>'.format(self.name)
+            xml += '<enabled>false</enabled>' + EVCMap._xml_trailer()
 
-            log.debug("Disabling EVC-MAP {}: '{}'".format(self.name, xml))
+            log.debug('disabling', name=self.name, xml=xml)
 
             if self._needs_acl_support:
                 self._enabled = False              # TODO: Support ACLs
             else:
                 try:
-                    results = yield self._flow.handler.netconf_client.edit_config(xml,
-                                                                                  default_operation='merge',
-                                                                                  lock_timeout=30)
+                    results = yield self._flow.handler.netconf_client.edit_config(xml, lock_timeout=30)
                     self._enabled = not results.ok
                     if results.ok:
                         self.status = ''
@@ -278,7 +353,7 @@
                         self.status = results.error     # TODO: Save off error status
 
                 except Exception as e:
-                    log.exception('Failed to disable EVC-MAP', name=self.name, e=e)
+                    log.exception('disabling', name=self.name, e=e)
                     raise
 
         returnValue(self.installed and not self._enabled)
@@ -310,7 +385,7 @@
 
         flow = self._flow
 
-        self._name = 'EVC-MAP-{}-{}'.format('i' if self._is_ingress_map else 'e', flow.flow_id)
+        self._name = EVC_MAP_NAME_FORMAT.format(flow.in_port, flow.flow_id)
 
         if self._evc:
             self._evc_connection = EVCMap.EvcConnection.EVC
@@ -319,25 +394,51 @@
             self._status_message = 'Can only create EVC-MAP if EVC supplied'
             return False
 
-        if flow.handler.is_pon_port(flow.in_port) or flow.handler.is_uni_port(flow.in_port):
+        is_pon = flow.handler.is_pon_port(flow.in_port)
+        is_uni = flow.handler.is_uni_port(flow.in_port)
+
+        if is_pon or is_uni:
             self._uni_port = self._flow.handler.get_port_name(flow.in_port)
+            self._evc.ce_vlan_preservation = False
         else:
             self._status_message = 'EVC-MAPS without UNI or PON ports are not supported'
             return False    # UNI Ports handled in the EVC Maps
 
-        # If no match of VLAN this may be for untagged traffic
+        # ACL logic
 
-        if flow.vlan_id is None and flow.inner_vid is None:
-            self._match_untagged = True
-        else:
-            self._match_untagged = False
-            self._c_tag = flow.inner_vid
+        self._eth_type = flow.eth_type
+
+        if self._eth_type == FlowEntry.EtherType.IPv4.value:
+            self._ip_protocol = flow.ip_protocol
+            self._ipv4_dst = flow.ipv4_dst
+
+            if self._ip_protocol == FlowEntry.IpProtocol.UDP.value:
+                self._udp_dst = flow.udp_dst
+                self._udp_src = flow.udp_src
+
+        # If no match of VLAN this may be for untagged traffic or upstream and needs to
+        # match the gem-port vid
+
+        if self._is_ingress_map and is_pon:
+            pon_port = flow.handler.get_southbound_port(flow.in_port)
+
+            if pon_port is not None:
+                self._gem_ids = pon_port.gem_ids(self._needs_acl_support)
+                # TODO: Only EAPOL ACL support for the first demo
+                if self._needs_acl_support and self._eth_type != FlowEntry.EtherType.EAPOL.value:
+                    self._gem_ids = set()
+
+        # if flow.vlan_id is None and flow.inner_vid is None:
+        #     self._match_untagged = True
+        # else:
+        #     self._match_untagged = False
+        self._c_tag = flow.inner_vid
 
         # If a push of a single VLAN is present with a POP of the VLAN in the EVC's
         # flow, then this is a traditional EVC flow
 
         if len(flow.push_vlan_id) == 1 and self._evc.flow_entry.pop_vlan == 1:
-            self._evc.men_to_uni_tag_manipulation = EVC.Men2UniManipulation.SYMETRIC
+            self._evc.men_to_uni_tag_manipulation = EVC.Men2UniManipulation.SYMMETRIC
             self._evc.switching_method = EVC.SwitchingMethod.SINGLE_TAGGED
             self._evc.stpid = flow.push_vlan_tpid[0]
 
@@ -347,16 +448,68 @@
             # self._match_ce_vlan_id = 'TODO: something maybe'
             raise NotImplementedError('TODO: Not supported/needed yet')
 
-        # ACL logic
-
-        self._eth_type = flow.eth_type
-
-        if self._eth_type == FlowEntry.EtherType.IPv4:
-            self._ip_protocol = flow.ip_protocol
-            self._ipv4_dst = flow.ipv4_dst
-
-            if self._ip_protocol == FlowEntry.IpProtocol.UDP:
-                self._udp_dst = flow.udp_dst
-                self._udp_src = flow.udp_src
-
         return True
+
+    @staticmethod
+    def remove_all(client, regex_=EVC_MAP_NAME_REGEX_ALL):
+        """
+        Remove all matching EVC Maps from hardware
+
+        :param client: (ncclient) NETCONF Client to use
+        :param regex_: (String) Regular expression for name matching
+        :return: (deferred)
+        """
+        # Do a 'get' on the evc-map config an you should get the names
+        get_xml = """
+        <filter>
+          <evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps">
+            <evc-map>
+              <name/>
+            </evc-map>
+          </evc-maps>
+        </filter>
+        """
+        log.debug('query', xml=get_xml)
+
+        def request_failed(results, operation):
+            log.error('{}-failed'.format(operation), results=results)
+            # No further actions. Periodic poll later on will scrub any old EVC-Maps if needed
+
+        def delete_complete(results):
+            log.debug('delete-complete', results=results)
+
+        def do_delete(rpc_reply, regexpr):
+            log.debug('query-complete', rpc_reply=rpc_reply)
+
+            if rpc_reply.ok:
+                result_dict = xmltodict.parse(rpc_reply.data_xml)
+                entries = result_dict['data']['evc-maps'] if 'evc-maps' in result_dict['data'] else {}
+
+                if 'evc-map' in entries:
+                    p = re.compile(regexpr)
+
+                    if isinstance(entries['evc-map'], list):
+                        names = {entry['name'] for entry in entries['evc-map']
+                                 if 'name' in entry and p.match(entry['name'])}
+                    else:
+                        names = set()
+                        for item in entries['evc-map'].items():
+                            if isinstance(item, tuple) and item[0] == 'name':
+                                names.add(item[1])
+                                break
+
+                    if len(names) > 0:
+                        del_xml = EVCMap._xml_header('delete')
+                        for name in names:
+                            del_xml += '<name>{}</name>'.format(name)
+                            del_xml += EVCMap._xml_trailer()
+
+                        log.debug('removing', xml=del_xml)
+                        return client.edit_config(del_xml, lock_timeout=30)
+
+            return succeed('no entries')
+
+        d = client.get(get_xml)
+        d.addCallbacks(do_delete, request_failed, callbackArgs=[regex_], errbackArgs=['get'])
+        d.addCallbacks(delete_complete, request_failed, errbackArgs=['edit-config'])
+        return d
diff --git a/voltha/adapters/adtran_olt/flow/flow_entry.py b/voltha/adapters/adtran_olt/flow/flow_entry.py
index fed24e0..ac53659 100644
--- a/voltha/adapters/adtran_olt/flow/flow_entry.py
+++ b/voltha/adapters/adtran_olt/flow/flow_entry.py
@@ -1,18 +1,17 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# 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.
-#
+
 from evc import EVC
 from evc_map import EVCMap
 from enum import Enum
@@ -41,7 +40,7 @@
     """
     Provide a class that wraps the flow rule and also provides state/status for a FlowEntry.
 
-    When a new flow is sent, it is first decoded to check for any potential errors.  If None are
+    When a new flow is sent, it is first decoded to check for any potential errors. If None are
     found, the entry is created and it is analyzed to see if it can be combined to with any other flows
     to create or modify an existing EVC.
 
@@ -61,9 +60,9 @@
         (FlowDirection.NNI, FlowDirection.NNI): FlowDirection.NNI,
     }
 
-    # Well known EtherType
+    # Well known EtherTypes
     class EtherType(Enum):
-        EAPOL = 0x88E8
+        EAPOL = 0x888E
         IPv4 = 0x0800
         ARP = 0x0806
 
@@ -200,7 +199,7 @@
             return flow_entry, FlowEntry._create_evc_and_maps(downstream_flow, upstream_flows)
 
         except Exception as e:
-            log.exception('Error during flow_entry processing', e=e)
+            log.exception('flow_entry-processing', e=e)
 
     @staticmethod
     def _create_evc_and_maps(downstream_flow, upstream_flows):
@@ -228,7 +227,7 @@
 
         all_valid = all(flow.evc_map.valid for flow in upstream_flows)
 
-        return evc if all(flow.evc_map.valid for flow in upstream_flows) else None
+        return evc if all_valid else None
 
     def _decode(self):
         """
@@ -268,7 +267,10 @@
         outer = self.vlan_id or None if push_len == 0 else self.push_vlan_id[0]
 
         # 4 - The inner VID.
-        inner = self.inner_vid or None if push_len <= 1 else self.push_vlan_id[1]
+        if self.inner_vid is not None:
+            inner = self.inner_vid
+        else:
+            inner = self.vlan_id if (push_len > 0 and outer is not None) else None
 
         self.signature = '{}'.format(dev_id)
         for port in ports:
@@ -284,7 +286,7 @@
         self.in_port = fd.get_in_port(self._flow)
 
         if self.in_port > OFPP_MAX:
-            log.warn('Logical input ports are not supported at this time')
+            log.warn('Logical-input-ports-not-supported')
             return False
 
         for field in fd.get_ofb_fields(self._flow):
@@ -292,43 +294,43 @@
                 pass   # Handled earlier
 
             elif field.type == VLAN_VID:
-                log.info('*** field.type == VLAN_VID', value=field.vlan_vid & 0xfff)
+                # log.info('*** field.type == VLAN_VID', value=field.vlan_vid & 0xfff)
                 self.vlan_id = field.vlan_vid & 0xfff
 
             elif field.type == VLAN_PCP:
-                log.info('*** field.type == VLAN_PCP', value=field.vlan_pcp)
+                # log.info('*** field.type == VLAN_PCP', value=field.vlan_pcp)
                 self.pcp = field.vlan_pcp
 
             elif field.type == ETH_TYPE:
-                log.info('*** field.type == ETH_TYPE', value=field.eth_type)
+                # log.info('*** field.type == ETH_TYPE', value=field.eth_type)
                 self.eth_type = field.eth_type
 
             elif field.type == IP_PROTO:
-                log.info('*** field.type == IP_PROTO', value=field.ip_proto)
+                # log.info('*** field.type == IP_PROTO', value=field.ip_proto)
                 self.ip_protocol = field.ip_proto
 
                 if self.ip_protocol not in _supported_ip_protocols:
-                    log.error('Unsupported IP Protocol')
+                    # log.error('Unsupported IP Protocol')
                     return False
 
             elif field.type == IPV4_DST:
-                log.info('*** field.type == IPV4_DST', value=field.ipv4_dst)
+                # log.info('*** field.type == IPV4_DST', value=field.ipv4_dst)
                 self.ipv4_dst = field.ipv4_dst
 
             elif field.type == UDP_DST:
-                log.info('*** field.type == UDP_DST', value=field.udp_dst)
+                # log.info('*** field.type == UDP_DST', value=field.udp_dst)
                 self.udp_dst = field.udp_dst
 
             elif field.type == UDP_SRC:
-                log.info('*** field.type == UDP_SRC', value=field.udp_src)
+                # log.info('*** field.type == UDP_SRC', value=field.udp_src)
                 self.udp_src = field.udp_src
 
             elif field.type == METADATA:
-                log.info('*** field.type == METADATA', value=field.table_metadata)
+                # log.info('*** field.type == METADATA', value=field.table_metadata)
                 self.inner_vid = field.table_metadata
 
             else:
-                log.warn('Found unsupported selection field', type=field.type)
+                log.warn('unsupported-selection-field', type=field.type)
                 self._status_message = 'Unsupported field.type={}'.format(field.type)
                 return False
 
@@ -338,7 +340,7 @@
         self.output = fd.get_out_port(self._flow)
 
         if self.output > OFPP_MAX:
-            log.warn('Logical output ports are not supported at this time')
+            log.warn('Logical-output-ports-not-supported')
             return False
 
         for act in fd.get_actions(self._flow):
@@ -346,17 +348,17 @@
                 pass           # Handled earlier
 
             elif act.type == POP_VLAN:
-                log.info('*** action.type == POP_VLAN')
+                # log.info('*** action.type == POP_VLAN')
                 self.pop_vlan += 1
 
             elif act.type == PUSH_VLAN:
-                log.info('*** action.type == PUSH_VLAN', value=act.push)
+                # log.info('*** action.type == PUSH_VLAN', value=act.push)
                 # TODO: Do we want to test the ethertype for support?
                 tpid = act.push.ethertype
                 self.push_vlan_tpid.append(tpid)
 
             elif act.type == SET_FIELD:
-                log.info('*** action.type == SET_FIELD', value=act.set_field.field)
+                # log.info('*** action.type == SET_FIELD', value=act.set_field.field)
                 assert (act.set_field.field.oxm_class == ofp.OFPXMC_OPENFLOW_BASIC)
                 field = act.set_field.field.ofb_field
                 if field.type == VLAN_VID:
@@ -364,7 +366,7 @@
 
             else:
                 # TODO: May need to modify ce-preservation
-                log.warn('Found unsupported action', action=act)
+                log.warn('unsupported-action', action=act)
                 self._status_message = 'Unsupported action.type={}'.format(act.type)
                 return False
 
@@ -382,7 +384,7 @@
                     yield flow.remove()
 
                 except Exception as e:
-                    log.exception('Exception while removing stale flow', flow=flow, e=e)
+                    log.exception('stale-flow', flow=flow, e=e)
 
     @inlineCallbacks
     def remove(self):
diff --git a/voltha/adapters/adtran_olt/net/adtran_netconf.py b/voltha/adapters/adtran_olt/net/adtran_netconf.py
index d019da6..e31811f 100644
--- a/voltha/adapters/adtran_olt/net/adtran_netconf.py
+++ b/voltha/adapters/adtran_olt/net/adtran_netconf.py
@@ -1,18 +1,16 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
 
 import structlog
 from lxml import etree
@@ -45,7 +43,6 @@
     """
     Performs NETCONF requests
     """
-
     def __init__(self, host_ip, port=830, username='', password='', timeout=20):
         self._ip = host_ip
         self._port = port
@@ -100,17 +97,14 @@
                                             hostkey_verify=False,
                                             timeout=timeout)
 
-            log.debug('Dumping Server Capabilities')
-            for cap in self.capabilities:
-                log.debug('  {}'.format(cap))
         except SSHError as e:
             # Log and rethrow exception so any errBack is called
-            log.exception('SSH Error during connect: {}'.format(e.message))
+            log.exception('SSHError-during-connect', e=e)
             raise e
 
         except Exception as e:
             # Log and rethrow exception so any errBack is called
-            log.exception('Connect request failed: {}'.format(e.message))
+            log.exception('Connect-failed: {}', e=e)
             raise e
 
         # If debug logging is enabled, decrease the level, DEBUG is a significant
@@ -172,7 +166,7 @@
         Get the requested data from the server
 
         :param payload: Payload/filter
-        :return: (defeered) for GetReply
+        :return: (deferred) for GetReply
         """
         log.debug('get', filter=payload)
 
@@ -189,11 +183,13 @@
         :return: (GetReply) response
         """
         try:
+            log.debug('get', payload=payload)
             response = self._session.get(payload)
             # To get XML, use response.xml
+            log.debug('response', response=response)
 
         except RPCError as e:
-            log.exception('get Exception: {}'.format(e.message))
+            log.exception('get', e=e)
             raise
 
         return response
@@ -201,7 +197,7 @@
     def lock(self, source, lock_timeout):
         """
         Lock the configuration system
-        :return: (defeered) for RpcReply
+        :return: (deferred) for RpcReply
         """
         log.debug('lock', source=source, timeout=lock_timeout)
 
@@ -219,7 +215,7 @@
             # To get XML, use response.xml
 
         except RPCError as e:
-            log.exception('lock Exception: {}'.format(e.message))
+            log.exception('lock', e=e)
             raise
 
         return response
@@ -229,7 +225,7 @@
         Get the requested data from the server
         :param rpc_string: RPC request
 
-        :return: (defeered) for RpcReply
+        :return: (deferred) for RpcReply
         """
         log.debug('unlock', source=source)
 
@@ -247,13 +243,13 @@
             # To get XML, use response.xml
 
         except RPCError as e:
-            log.exception('unlock Exception: {}'.format(e.message))
+            log.exception('unlock', e=e)
             raise
 
         return response
 
     @inlineCallbacks
-    def edit_config(self, config, target='running', default_operation=None,
+    def edit_config(self, config, target='running', default_operation='none',
                     test_option=None, error_option=None, lock_timeout=-1):
         """
         Loads all or part of the specified config to the target configuration datastore with the ability to lock
@@ -270,20 +266,20 @@
         :param lock_timeout if >0, the maximum number of seconds to hold a lock on the datastore while the edit
                             operation is underway
 
-        :return: (defeered) for RpcReply
+        :return: (deferred) for RpcReply
         """
         if not self._session or not self._session.connected:
             raise NotImplemented('TODO: Support auto-connect if needed')
 
         rpc_reply = None
-        if lock_timeout > 0:
-            try:
-                request = self._session.lock(target, lock_timeout)
-                rpc_reply = yield request
-
-            except Exception as e:
-                log.exception('edit_config Lock Exception: {}'.format(e.message))
-                raise
+        # if lock_timeout > 0:
+        #     try:
+        #         request = self._session.lock(target, lock_timeout)
+        #         rpc_reply = yield request
+        #
+        #     except Exception as e:
+        #         log.exception('edit_config-Lock', e=e)
+        #         raise
         try:
             if config[:7] != '<config':
                 config = '<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">' + \
@@ -293,17 +289,18 @@
                                                     config, default_operation,
                                                     test_option, error_option)
         except Exception as e:
-            log.exception('edit_config Edit Exception: {}'.format(e.message))
+            log.exception('edit_config', e=e)
             raise
 
         finally:
-            if lock_timeout > 0:
-                try:
-                    yield self._session.unlock(target)
-
-                except Exception as e:
-                    log.exception('edit_config unlock Exception: {}'.format(e.message))
-                    # Note that we just fall through and do not re-raise this exception
+            pass
+            # if lock_timeout > 0:
+            #     try:
+            #         yield self._session.unlock(target)
+            #
+            #     except Exception as e:
+            #         log.exception('edit_config-unlock', e=e)
+            #         # Note that we just fall through and do not re-raise this exception
 
         returnValue(rpc_reply)
 
@@ -312,17 +309,21 @@
         Lock the configuration system
         """
         try:
+            log.debug('edit-config', target=target, config=config)
+            
             response = self._session.edit_config(target=target, config=config
                                                  # TODO: Support additional options later
                                                  # ,default_operation=default_operation,
                                                  # test_option=test_option,
                                                  # error_option=error_option
                                                  )
+
+            log.debug('response', response=response)
             # To get XML, use response.xml
             # To check status, use response.ok  (boolean)
 
         except RPCError as e:
-            log.exception('edit_config Exception: {}'.format(e.message))
+            log.exception('do_edit_config', e=e)
             raise
 
         return response
@@ -331,7 +332,7 @@
         """
         Custom RPC request
         :param rpc_string: (string) RPC request
-        :return: (defeered) for GetReply
+        :return: (deferred) for GetReply
         """
         log.debug('rpc', rpc=rpc_string)
 
@@ -346,7 +347,7 @@
             # To get XML, use response.xml
 
         except RPCError as e:
-            log.exception('rpc Exception: {}'.format(e.message))
+            log.exception('rpc', e=e)
             raise
 
         return response
diff --git a/voltha/adapters/adtran_olt/net/adtran_rest.py b/voltha/adapters/adtran_olt/net/adtran_rest.py
index 049b94a..edb64ad 100644
--- a/voltha/adapters/adtran_olt/net/adtran_rest.py
+++ b/voltha/adapters/adtran_olt/net/adtran_rest.py
@@ -1,24 +1,23 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
+
 import json
 
 import structlog
 import treq
 from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.internet.error import ConnectionClosed
+from twisted.internet.error import ConnectionClosed, ConnectionDone, ConnectionLost
 
 log = structlog.get_logger()
 
@@ -87,7 +86,7 @@
         return "AdtranRestClient {}@{}:{}".format(self._username, self._ip, self._port)
 
     @inlineCallbacks
-    def request(self, method, uri, data=None, name='', timeout=None):
+    def request(self, method, uri, data=None, name='', timeout=None, is_retry=False):
         """
         Send a REST request to the Adtran device
 
@@ -95,9 +94,12 @@
         :param uri: (string) fully URL to perform method on
         :param data: (string) optional data for the request body
         :param name: (string) optional name of the request, useful for logging purposes
+        :param timeout: (int) Number of seconds to wait for a response before timing out
+        :param is_retry: (boolean) True if this method called recursively in order to recover
+                                   from a connection loss. Can happen sometimes in debug sessions
+                                   and in the real world.
         :return: (deferred)
         """
-
         if method.upper() not in self._valid_methods:
             raise NotImplementedError("REST method '{}' is not supported".format(method))
 
@@ -136,6 +138,12 @@
         except NotImplementedError:
             raise
 
+        except (ConnectionDone, ConnectionLost) as e:
+            if is_retry:
+                returnValue(e)
+            returnValue(self.request(method, uri, data=data, name=name,
+                                     timeout=timeout, is_retry=True))
+
         except ConnectionClosed:
             returnValue(ConnectionClosed)
 
diff --git a/voltha/adapters/adtran_olt/net/adtran_zmq.py b/voltha/adapters/adtran_olt/net/adtran_zmq.py
index 9cbeae6..1c83ae1 100644
--- a/voltha/adapters/adtran_olt/net/adtran_zmq.py
+++ b/voltha/adapters/adtran_olt/net/adtran_zmq.py
@@ -1,18 +1,17 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
+
 import binascii
 import struct
 
@@ -26,18 +25,15 @@
 
 # An OMCI message minimally has a 32-bit PON index and 32-bit ONU ID.
 
-_OLT_TASK_ZEROMQ_OMCI_TCP_PORT = 25656
+DEFAULT_ZEROMQ_OMCI_TCP_PORT = 5656
 
 
 class AdtranZmqClient(object):
     """
     Adtran ZeroMQ Client for PON Agent packet in/out service
-
     PON Agent expects and external PAIR socket with
     """
-
-    def __init__(self, ip_address, rx_callback=None,
-                 port=_OLT_TASK_ZEROMQ_OMCI_TCP_PORT):
+    def __init__(self, ip_address, rx_callback=None, port=DEFAULT_ZEROMQ_OMCI_TCP_PORT):
         self.external_conn = 'tcp://{}:{}'.format(ip_address, port)
 
         self.zmq_endpoint = ZmqEndpoint('connect', self.external_conn)
@@ -51,7 +47,7 @@
             self.socket.send(data)
 
         except Exception as e:
-            log.exception(e.message)
+            log.exception('send', e=e)
 
     def shutdown(self):
         self.socket.onReceive = AdtranZmqClient.rx_nop
@@ -59,7 +55,7 @@
 
     @staticmethod
     def rx_nop(message):
-        log.debug('Discarding ZMQ message, no receiver specified')
+        log.debug('discarding-no-receiver')
 
     @staticmethod
     def encode_omci_message(msg, pon_index, onu_id):
@@ -73,8 +69,6 @@
         :return: (bytes) octet string to send
         """
         assert msg
-        # log.debug("Encoding OMCI: PON: {}, ONU: {}, Message: '{}'".
-        #           format(pon_index, onu_id, msg))
         s = struct.Struct('!II')
 
         return s.pack(pon_index, onu_id) + binascii.unhexlify(msg)
diff --git a/voltha/adapters/adtran_olt/net/mock_netconf_client.py b/voltha/adapters/adtran_olt/net/mock_netconf_client.py
index e28b800..c1d40dd 100644
--- a/voltha/adapters/adtran_olt/net/mock_netconf_client.py
+++ b/voltha/adapters/adtran_olt/net/mock_netconf_client.py
@@ -1,18 +1,16 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
 
 import structlog
 import random
@@ -64,7 +62,6 @@
     def connect(self, connect_timeout=None):
         """
         Connect to the NETCONF server
-
           o To disable attempting publickey authentication altogether, call with
             allow_agent and look_for_keys as False.`
 
@@ -160,7 +157,7 @@
         returnValue(RPCReply(_dummy_xml))
 
     @inlineCallbacks
-    def edit_config(self, config, target='running', default_operation=None,
+    def edit_config(self, config, target='running', default_operation='merge',
                     test_option=None, error_option=None, lock_timeout=-1):
         """
         Loads all or part of the specified config to the target configuration datastore with the ability to lock
@@ -185,13 +182,13 @@
                 yield request
 
             except Exception as e:
-                log.exception('edit_config Lock Exception: {}'.format(e.message))
+                log.exception('edit_config-lock', e=e)
                 raise
         try:
             yield asleep(random.uniform(0.1, 2.0))  # Simulate NETCONF request delay
 
         except Exception as e:
-            log.exception('edit_config Edit Exception: {}'.format(e.message))
+            log.exception('edit_config', e=e)
             raise
 
         finally:
diff --git a/voltha/adapters/adtran_olt/onu.py b/voltha/adapters/adtran_olt/onu.py
index 144d053..6ba06c2 100644
--- a/voltha/adapters/adtran_olt/onu.py
+++ b/voltha/adapters/adtran_olt/onu.py
@@ -1,11 +1,10 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# 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,
@@ -14,23 +13,22 @@
 # limitations under the License.
 
 import base64
+import binascii
 import json
-
 import structlog
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 
 from adtran_olt_handler import AdtranOltHandler
 
-log = structlog.get_logger()
-
+# Following is only used in autoactivate/demo mode. Otherwise xPON
 _VSSN_TO_VENDOR = {
-    'adtn': 'adtran_onu',
-    'adtr': 'adtran_onu',
-    'bcm?': 'broadcom_onu',  # TODO: Get actual VSSN for this vendor
-    'dp??': 'dpoe_onu',  # TODO: Get actual VSSN for this vendor
-    'pmc?': 'pmcs_onu',  # TODO: Get actual VSSN for this vendor
-    'psm?': 'ponsim_onu',  # TODO: Get actual VSSN for this vendor
-    'sim?': 'simulated_onu',  # TODO: Get actual VSSN for this vendor
-    'tbt?': 'tibit_onu',  # TODO: Get actual VSSN for this vendor
+    'ADTN': 'adtran_onu',
+    'BCM?': 'broadcom_onu',   # TODO: Get actual VSSN for this vendor
+    'DP??': 'dpoe_onu',       # TODO: Get actual VSSN for this vendor
+    'PMC?': 'pmcs_onu',       # TODO: Get actual VSSN for this vendor
+    'PSMO': 'ponsim_onu',
+    'SIM?': 'simulated_onu',  # TODO: Get actual VSSN for this vendor
+    'TBT?': 'tibit_onu',      # TODO: Get actual VSSN for this vendor
 }
 
 
@@ -39,39 +37,61 @@
     Wraps an ONU
     """
     MIN_ONU_ID = 0
-    MAX_ONU_ID = 254
+    MAX_ONU_ID = 253            # G.984. 0..253, 254=reserved, 255=broadcast
     BROADCAST_ONU_ID = 255
-    # MAX_ONU_ID = 1022
+    # MAX_ONU_ID = 1022           # G.987. 0..1022, 1023=broadcast
     # BROADCAST_ONU_ID = 1023
     DEFAULT_PASSWORD = ''
 
-    def __init__(self, serial_number, pon, password=DEFAULT_PASSWORD):
-        self._onu_id = pon.get_next_onu_id()
-
+    def __init__(self, onu_info):
+        # onu_info = {
+        #     'serial-number': serial_number,
+        #     'xpon-name': None,
+        #     'pon-id': self.pon_id,
+        #     'onu-id': None,  # Set later (mandatory)
+        #     'enabled': True,
+        #     'upstream-channel-speed': 0,
+        #     't-cont': get_tconts(self.pon_id, serial_number),
+        #     'gem-ports': get_gem_ports(self.pon_id, serial_number),
+        # }
+        self._onu_id = onu_info['onu-id']
         if self._onu_id is None:
             raise ValueError('No ONU ID available')
 
-        self._serial_number = serial_number
-        self._password = password
-        self._pon = pon
-        self._name = 'xpon {}/{}'.format(pon.pon_id, self._onu_id)
+        self._serial_number_base64 = Onu.string_to_serial_number(onu_info['serial-number'])
+        self._serial_number_string = onu_info['serial-number']
+        self._password = onu_info['password']
+        self._pon = onu_info['pon']
+        self._name = '{}@{}'.format(self._pon.name, self._onu_id)
+        self._xpon_name = onu_info['xpon-name']
+        self._gem_ports = {}                           # gem-id -> GemPort
+        self._tconts = {}                              # alloc-id -> TCont
 
-        try:
-            sn_ascii = base64.decodestring(serial_number).lower()[:4]
-        except Exception:
-            sn_ascii = 'Invalid_VSSN'
+        # TODO: enable and upstream-channel-speed not yet supported
 
-        self._vendor_device = _VSSN_TO_VENDOR.get(sn_ascii,
-                                                  'Unsupported_{}'.format(sn_ascii))
+        self.log = structlog.get_logger(pon_id=self._pon.pon_id, onu_id=self._onu_id)
+        self._vendor_id = _VSSN_TO_VENDOR.get(self._serial_number_string.upper()[:4],
+                                              'Unsupported_{}'.format(self._serial_number_string))
 
     def __del__(self):
         # self.stop()
         pass
 
     def __str__(self):
-        return "Onu-{}-{}/{} parent: {}".format(self._onu_id, self._serial_number,
-                                                base64.decodestring(self._serial_number),
-                                                self._pon)
+        return "Onu-{}-{}, PON: {}".format(self._onu_id, self._serial_number_string, self._pon)
+    
+    @staticmethod
+    def serial_number_to_string(value):
+        sval = base64.decodestring(value)
+        unique = [elem.encode("hex") for elem in sval[4:8]]
+        return '{}{}{}{}{}'.format(sval[:4], unique[0], unique[1], unique[2], unique[3]).upper()
+
+    @staticmethod
+    def string_to_serial_number(value):
+        bvendor = [octet for octet in value[:4]]
+        bunique = [binascii.a2b_hex(value[offset:offset + 2]) for offset in xrange(4, 12, 2)]
+        bvalue = ''.join(bvendor + bunique)
+        return base64.b64encode(bvalue)
 
     @property
     def pon(self):
@@ -90,26 +110,178 @@
         return self._name
 
     @property
-    def vendor_device(self):
-        return self._vendor_device
+    def serial_number(self):
+        return self._serial_number_base64
 
-    def create(self, enabled):
+    @property
+    def vendor_id(self):
+        return self._vendor_id
+
+    @inlineCallbacks
+    def create(self, onu_info):
         """
         POST -> /restconf/data/gpon-olt-hw:olt/pon=<pon-id>/onus/onu ->
         """
+        self.log.debug('create')
+
         pon_id = self.pon.pon_id
         data = json.dumps({'onu-id': self._onu_id,
-                           'serial-number': self._serial_number,
-                           'enable': enabled})
-        uri = AdtranOltHandler.GPON_PON_ONU_CONFIG_URI.format(pon_id)
-        name = 'onu-create-{}-{}-{}: {}'.format(pon_id, self._onu_id, self._serial_number, enabled)
+                           'serial-number': self._serial_number_base64,
+                           'enable': onu_info['enabled']})
+        uri = AdtranOltHandler.GPON_ONU_CONFIG_LIST_URI.format(pon_id)
+        name = 'onu-create-{}-{}-{}: {}'.format(pon_id, self._onu_id,
+                                                self._serial_number_base64, onu_info['enabled'])
 
-        return self.olt.rest_client.request('POST', uri, data=data, name=name)
+        try:
+            results = yield self.olt.rest_client.request('POST', uri, data=data, name=name)
+
+        except Exception as e:
+            self.log.exception('onu-create', e=e)
+            raise
+
+        # Now set up all tconts & gem-ports
+
+        for _, tcont in onu_info['t-conts'].items():
+            try:
+                results = yield self.add_tcont(tcont)
+
+            except Exception as e:
+                self.log.exception('add-tcont', tcont=tcont, e=e)
+
+        for _, gem_port in onu_info['gem-ports'].items():
+            try:
+                if gem_port.multicast:
+                    self.log.warning('multicast-not-yet-supported', gem_port=gem_port)  # TODO Support it
+                    continue
+                results = yield self.add_gem_port(gem_port)
+
+            except Exception as e:
+                self.log.exception('add-gem_port', gem_port=gem_port, e=e)
+
+        returnValue(results)
 
     def set_config(self, leaf, value):
+        self.log.debug('set-config', leaf=leaf, value=value)
+
         pon_id = self.pon.pon_id
         data = json.dumps({'onu-id': self._onu_id,
                            leaf: value})
-        uri = AdtranOltHandler.GPON_PON_ONU_CONFIG_URI.format(pon_id)
+        uri = AdtranOltHandler.GPON_ONU_CONFIG_LIST_URI.format(pon_id)
         name = 'onu-set-config-{}-{}-{}: {}'.format(pon_id, self._onu_id, leaf, value)
         return self.olt.rest_client.request('PATCH', uri, data=data, name=name)
+
+    @property
+    def alloc_ids(self):
+        """
+        Get alloc-id's of all T-CONTs
+        """
+        return frozenset(self._tconts.keys())
+
+    @inlineCallbacks
+    def add_tcont(self, tcont):
+        """
+        Creates/ a T-CONT with the given alloc-id
+
+        :param tcont: (TCont) Object that maintains the TCONT properties
+        """
+        from tcont import TrafficDescriptor
+
+        if tcont.alloc_id in self._tconts:
+            returnValue(succeed('already created'))
+
+        pon_id = self.pon.pon_id
+        uri = AdtranOltHandler.GPON_TCONT_CONFIG_LIST_URI.format(pon_id, self.onu_id)
+        data = json.dumps({'alloc-id': tcont.alloc_id})
+        name = 'tcont-create-{}-{}: {}'.format(pon_id, self._onu_id, tcont.alloc_id)
+
+        try:
+            results = yield self.olt.rest_client.request('POST', uri, data=data, name=name)
+            self._tconts[tcont.alloc_id] = tcont
+
+        except Exception as e:
+            self.log.exception('tcont', tcont=tcont, e=e)
+            raise
+
+        # TODO May want to pull this out and have it accessible elsewhere once xpon work supports TDs
+
+        if tcont.traffic_descriptor is not None:
+            uri = AdtranOltHandler.GPON_TCONT_CONFIG_URI.format(pon_id, self.onu_id, tcont.alloc_id)
+            data = json.dumps({'traffic-descriptor': tcont.traffic_descriptor.to_dict()})
+            name = 'tcont-td-{}-{}: {}'.format(pon_id, self._onu_id, tcont.alloc_id)
+            try:
+                results = yield self.olt.rest_client.request('PATCH', uri, data=data, name=name)
+
+            except Exception as e:
+                self.log.exception('traffic-descriptor', td=tcont.traffic_descriptor, e=e)
+
+            if tcont.traffic_descriptor.additional_bandwidth_eligibility == \
+               TrafficDescriptor.AdditionalBwEligibility.BEST_EFFORT_SHARING:
+                if tcont.best_effort is None:
+                    raise ValueError('TCONT {} is best-effort but does not define best effort sharing'.
+                                     format(tcont.name))
+
+                data = json.dumps({'best-effort': tcont.best_effort.to_dict()})
+                name = 'tcont-best-effort-{}-{}: {}'.format(pon_id, self._onu_id, tcont.alloc_id)
+                try:
+                    results = yield self.olt.rest_client.request('PATCH', uri, data=data, name=name)
+
+                except Exception as e:
+                    self.log.exception('best-effort', best_effort=tcont.best_effort, e=e)
+                    raise
+
+        returnValue(results)
+
+    def remove_tcont(self, alloc_id):
+        if alloc_id in self._tconts:
+            del self._tconts[alloc_id]
+
+        # Always remove from OLT hardware
+        pon_id = self.pon.pon_id
+        uri = AdtranOltHandler.GPON_TCONT_CONFIG_URI.format(pon_id, self.onu_id, alloc_id)
+        name = 'tcont-delete-{}-{}: {}'.format(pon_id, self._onu_id, alloc_id)
+        return self.olt.rest_client.request('DELETE', uri, name=name)
+
+    #@property
+    def gem_ids(self, exception_gems):
+        """Get all GEM Port IDs used by this ONU"""
+        return frozenset([gem_id for gem_id, gem in self._gem_ports.items()
+                         if gem.exception == exception_gems])
+        # return frozenset(self._gem_ports.keys())
+
+    @inlineCallbacks
+    def add_gem_port(self, gem_port):
+        if gem_port.gem_id in self._gem_ports:
+            returnValue(succeed('already created'))
+
+        pon_id = self.pon.pon_id
+        uri = AdtranOltHandler.GPON_GEM_CONFIG_LIST_URI.format(pon_id, self.onu_id)
+        data = json.dumps(gem_port.to_dict())
+        name = 'gem-port-create-{}-{}: {}/{}'.format(pon_id, self._onu_id,
+                                                     gem_port.gem_id,
+                                                     gem_port.alloc_id)
+        try:
+            results = yield self.olt.rest_client.request('POST', uri, data=data, name=name)
+            self._gem_ports[gem_port.gem_id] = gem_port
+            # TODO: May need to update flow tables/evc-maps
+
+        except Exception as e:
+            self.log.exception('gem-port', e=e)
+            raise
+
+        returnValue(results)
+
+    def remove_gem_id(self, gem_id):
+        if gem_id in self._gem_ports:
+            del self._gem_ports[gem_id]
+            # TODO: May need to update flow tables/evc-maps
+
+        # Always remove from OLT hardware
+        pon_id = self.pon.pon_id
+        uri = AdtranOltHandler.GPON_GEM_CONFIG_URI.format(pon_id, self.onu_id, gem_id)
+        name = 'gem-port-delete-{}-{}: {}'.format(pon_id, self._onu_id, gem_id)
+        return self.olt.rest_client.request('DELETE', uri, name=name)
+
+    @staticmethod
+    def gem_id_to_gvid(gem_id):
+        """Calculate GEM VID for a given GEM port id"""
+        return gem_id - 2048
diff --git a/voltha/adapters/adtran_olt/pon_port.py b/voltha/adapters/adtran_olt/pon_port.py
index 99f3652..f03ef78 100644
--- a/voltha/adapters/adtran_olt/pon_port.py
+++ b/voltha/adapters/adtran_olt/pon_port.py
@@ -1,23 +1,21 @@
-#
-# Copyright 2017-present Adtran, Inc.
+# 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
+# http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
+
 import json
 import pprint
 import random
 
-import os
 import structlog
 from enum import Enum
 from twisted.internet import reactor
@@ -34,7 +32,7 @@
 class PonPort(object):
     """
     A class similar to the 'Port' class in the VOLTHA
-    
+
     TODO: Merge this with the Port class or cleanup where possible
           so we do not duplicate fields/properties/methods
     """
@@ -47,46 +45,51 @@
         STOPPED = 2  # Disabled
         DELETING = 3  # Cleanup
 
+    _SUPPORTED_ACTIVATION_METHODS = ['autodiscovery', 'autoactivate']
+    _SUPPORTED_AUTHENTICATION_METHODS = ['serial-number']
+
     def __init__(self, pon_index, port_no, parent, admin_state=AdminState.UNKNOWN, label=None):
         # TODO: Weed out those properties supported by common 'Port' object (future)
         assert admin_state != AdminState.UNKNOWN
 
-        self.log = structlog.get_logger(pon_id=pon_index)
+        self.log = structlog.get_logger(device_id=parent.device_id, pon_id=pon_index)
 
         self._parent = parent
         self._pon_id = pon_index
         self._port_no = port_no
-        self._name = 'xpon {}'.format(pon_index)
+        self._name = 'xpon 0/{}'.format(pon_index+1)
         self._label = label or 'PON-{}'.format(pon_index)
         self._port = None
         self._no_onu_discover_tick = 5.0  # TODO: Decrease to 1 or 2 later
         self._discovery_tick = 20.0
         self._discovered_onus = []  # List of serial numbers
-        self._onus = {}  # serial_number -> ONU  (allowed list)
+        self._onus = {}         # serial_number-base64 -> ONU  (allowed list)
+        self._onu_by_id = {}    # onu-id -> ONU
         self._next_onu_id = Onu.MIN_ONU_ID
 
-        # TODO: Currently cannot update admin/oper status, so create this enabled and active
-        # self._admin_state = admin_state
-        # self._oper_status = OperStatus.UNKNOWN
-        self._admin_state = AdminState.ENABLED
-        self._oper_status = OperStatus.ACTIVE
-        self._deferred = None
+        self._admin_state = AdminState.DISABLED
+        self._oper_status = OperStatus.DISCOVERED
+        self._deferred = None                   # General purpose
+        self._discovery_deferred = None           # Specifically for ONU discovery
         self._state = PonPort.State.INITIAL
 
         # Local cache of PON configuration
 
+        self._xpon_name = None
         self._enabled = None
         self._downstream_fec_enable = None
         self._upstream_fec_enable = None
+        self._authentication_method = 'serial-number'
+        self._activation_method = 'autoactivate' if self.olt.autoactivate else 'autodiscovery'
 
     def __del__(self):
         self.stop()
 
     def __str__(self):
-        return "PonPort-{}: Admin: {}, Oper: {}, parent: {}".format(self._label,
-                                                                    self._admin_state,
-                                                                    self._oper_status,
-                                                                    self._parent)
+        return "PonPort-{}: Admin: {}, Oper: {}, OLT: {}".format(self._label,
+                                                                 self._admin_state,
+                                                                 self._oper_status,
+                                                                 self.olt)
 
     def get_port(self):
         """
@@ -97,8 +100,11 @@
             self._port = Port(port_no=self._port_no,
                               label=self._label,
                               type=Port.PON_OLT,
-                              admin_state=self._admin_state,
-                              oper_status=self._oper_status)
+                              admin_state=AdminState.ENABLED,
+                              oper_status=OperStatus.ACTIVE)
+            # TODO: For now, no way to report the proper ADMIN or OPER status
+            # admin_state=self._admin_state,
+            # oper_status=self._oper_status)
         return self._port
 
     @property
@@ -110,6 +116,14 @@
         return self._name
 
     @property
+    def xpon_name(self):
+        return self._xpon_name
+
+    @xpon_name.setter
+    def xpon_name(self, value):
+        self._xpon_name = value
+
+    @property
     def pon_id(self):
         return self._pon_id
 
@@ -125,6 +139,48 @@
     def adapter_agent(self):
         return self.olt.adapter_agent
 
+    @property
+    def discovery_tick(self):
+        return self._discovery_tick * 10
+    
+    @discovery_tick.setter
+    def discovery_tick(self, value):
+        if value < 0:
+            raise ValueError("Polling interval must be >= 0")
+
+        if self.discovery_tick != value:
+            self._discovery_tick = value / 10
+
+            if self._discovery_deferred is not None:
+                self._discovery_deferred.cancel()
+                self._discovery_deferred = None
+
+            if self._discovery_tick > 0:
+                self._discovery_deferred = reactor.callLater(self._discovery_tick,
+                                                             self._discover_onus)
+
+    @property
+    def activation_method(self):
+        return self._activation_method
+
+    @activation_method.setter
+    def activation_method(self, value):
+        value = value.lower()
+        if value not in PonPort._SUPPORTED_ACTIVATION_METHODS:
+            raise ValueError('Invalid ONU activation method')
+        self._activation_method = value
+
+    @property
+    def authentication_method(self):
+        return self._authentication_method
+
+    @authentication_method.setter
+    def authentication_method(self, value):
+        value = value.lower()
+        if value not in PonPort._SUPPORTED_AUTHENTICATION_METHODS:
+            raise ValueError('Invalid ONU authentication method')
+        self._authentication_method = value
+
     def get_logical_port(self):
         """
         Get the VOLTHA logical port for this port. For PON ports, a logical port
@@ -135,9 +191,13 @@
         return None
 
     def _cancel_deferred(self):
-        d, self._deferred = self._deferred, None
-        if d is not None:
-            d.cancel()
+        d1, self._deferred = self._deferred, None
+        d2, self._discovery_deferred = self._discovery_deferred, None
+        
+        if d1 is not None:
+            d1.cancel()            
+        if d2 is not None:
+            d2.cancel()
 
     def _update_adapter_agent(self):
         # TODO: Currently the adapter_agent does not allow 'update' of port status
@@ -152,13 +212,16 @@
         if self._state == PonPort.State.RUNNING:
             return succeed('Running')
 
-        self.log.info('Starting {}'.format(self._label))
+        self.log.info('start')
 
         self._cancel_deferred()
         self._state = PonPort.State.INITIAL
+        self._oper_status = OperStatus.ACTIVATING
 
         # Do the rest of the startup in an async method
         self._deferred = reactor.callLater(0.5, self._finish_startup)
+        self._update_adapter_agent()
+
         return self._deferred
 
     @inlineCallbacks
@@ -169,7 +232,7 @@
         if self._state != PonPort.State.INITIAL:
             returnValue('Done')
 
-        self.log.debug('Performing final port startup')
+        self.log.debug('final-startup')
 
         if self._enabled is None or self._downstream_fec_enable is None or self._upstream_fec_enable is None:
             try:
@@ -177,7 +240,7 @@
                 results = yield self._deferred
 
             except Exception as e:
-                self.log.exception('Initial GET of config failed: {}'.format(e.message))
+                self.log.exception('initial-GET', e=e)
                 self._deferred = reactor.callLater(5, self._finish_startup)
                 returnValue(self._deferred)
 
@@ -194,7 +257,7 @@
                 self._enabled = True
 
             except Exception as e:
-                self.log.exception('enabled failed: {}'.format(str(e)))
+                self.log.exception('final-startup-enable', e=e)
                 self._deferred = reactor.callLater(3, self._finish_startup)
                 returnValue(self._deferred)
 
@@ -205,7 +268,7 @@
                 self._downstream_fec_enable = True
 
             except Exception as e:
-                self.log.exception('downstream FEC enable failed: {}'.format(str(e)))
+                self.log.exception('final-startup-downstream-FEC', e=e)
                 self._deferred = reactor.callLater(5, self._finish_startup)
                 returnValue(self._deferred)
 
@@ -216,20 +279,20 @@
                 self._upstream_fec_enable = True
 
             except Exception as e:
-                self.log.exception('upstream FEC enable failed: {}'.format(str(e)))
+                self.log.exception('final-startup-upstream-FEC', e=e)
                 self._deferred = reactor.callLater(5, self._finish_startup)
                 returnValue(self._deferred)
 
-            self.log.debug('ONU Startup complete: results: {}'.format(pprint.PrettyPrinter().pformat(results)))
+            self.log.debug('startup-complete', results=pprint.PrettyPrinter().pformat(results))
 
         if self._enabled:
             self._admin_state = AdminState.ENABLED
             self._oper_status = OperStatus.ACTIVE  # TODO: is this correct, how do we tell GRPC
             self._state = PonPort.State.RUNNING
 
-            # Begin to ONU discovery. Once a second if no ONUs found and once every 20
-            #                         seconds after one or more ONUs found on the PON
-            self._deferred = reactor.callLater(1, self.discover_onus)
+            # Begin to ONU discovery
+
+            self._discovery_deferred = reactor.callLater(5, self._discover_onus)
 
             self._update_adapter_agent()
             returnValue('Enabled')
@@ -238,7 +301,7 @@
             # Startup failed. Could be due to object creation with an invalid initial admin_status
             #                 state.  May want to schedule a start to occur again if this happens
             self._admin_state = AdminState.DISABLED
-            self._oper_status = OperStatus.UNKNOWN
+            self._oper_status = OperStatus.FAILED
             self._state = PonPort.State.STOPPED
 
             self._update_adapter_agent()
@@ -248,7 +311,7 @@
         if self._state == PonPort.State.STOPPED:
             return succeed('Stopped')
 
-        self.log.info('Stopping {}'.format(self._label))
+        self.log.info('stopping')
 
         self._cancel_deferred()
         self._deferred = self.set_pon_config("enabled", False)
@@ -272,41 +335,55 @@
         PON 'Start' is done elsewhere
         """
         if self._state != PonPort.State.INITIAL:
-            self.log.error('Reset ignored, only valid during initial startup', state=self._state)
+            self.log.error('reset-ignored', state=self._state)
             returnValue('Ignored')
 
-        self.log.info('Reset {}'.format(self._label))
+        self.log.info('reset')
 
-        if self._admin_state != self._parent.initial_port_state:
+        try:
+            self._deferred = self.get_pon_config()
+            results = yield self._deferred
+
+            # Load cache
+            self._enabled = results.get('enabled', False)
+
+        except Exception as e:
+            self._enabled = None
+            self.log.exception('GET-failed', e=e)
+
+        initial_port_state = AdminState.ENABLED if self.olt.autoactivate else AdminState.DISABLED
+
+        if self._admin_state != initial_port_state:
             try:
-                enable = self._parent.initial_port_state == AdminState.ENABLED
-                yield self.set_pon_config("enabled", enable)
+                enable = initial_port_state == AdminState.ENABLED
+                if self._enabled is None or self._enabled != enable:
+                    yield self.set_pon_config("enabled", enable)
 
                 # TODO: Move to 'set_pon_config' method and also make sure GRPC/Port is ok
-                self._admin_state = AdminState.ENABLED if enable else AdminState.DISABLE
+                self._admin_state = AdminState.ENABLED if enable else AdminState.DISABLED
 
             except Exception as e:
-                self.log.exception('Reset of PON to initial state failed', e=e)
+                self.log.exception('reset', e=e)
                 raise
 
-        if self._admin_state == AdminState.ENABLED and self._parent.initial_onu_state == AdminState.DISABLED:
-            try:
-                # Walk the provisioned ONU list and disable any exiting ONUs
-                results = yield self.get_onu_config()
+        # Walk the provisioned ONU list and disable any exiting ONUs
 
-                if isinstance(results, list) and len(results) > 0:
-                    onu_configs = OltConfig.Pon.Onu.decode(results)
-                    for onu_id in onu_configs.iterkeys():
-                        try:
-                            yield self.delete_onu(onu_id)
+        try:
+            results = yield self.get_onu_config()
 
-                        except Exception as e:
-                            self.log.exception('Delete of ONU {} on PON failed'.format(onu_id), e=e)
-                            pass  # Non-fatal
+            if isinstance(results, list) and len(results) > 0:
+                onu_configs = OltConfig.Pon.Onu.decode(results)
+                for onu_id in onu_configs.iterkeys():
+                    try:
+                        yield self.delete_onu(onu_id)
 
-            except Exception as e:
-                self.log.exception('Failed to get current ONU config', e=e)
-                raise
+                    except Exception as e:
+                        self.log.exception('rest-ONU-delete', onu_id=onu_id, e=e)
+                        pass  # Non-fatal
+
+        except Exception as e:
+            self.log.exception('onu-delete', e=e)
+            raise
 
         returnValue('Reset complete')
 
@@ -315,19 +392,33 @@
         Parent device is being deleted. Do not change any config but
         stop all polling
         """
-        self.log.info('Deleteing {}'.format(self._label))
+        self.log.info('Deleting')
         self._state = PonPort.State.DELETING
         self._cancel_deferred()
 
+    # @property
+    def gem_ids(self, exception_gems):
+        """
+        Get all GEM Port IDs used on a given PON
+
+        :return: (dict) key -> onu-id, value -> frozenset of GEM Port IDs
+        """
+        gem_ids = {}
+        for onu_id, onu in self._onu_by_id.iteritems():
+            gem_ids[onu_id] = onu.gem_ids(exception_gems)
+        return gem_ids
+
     def get_pon_config(self):
         uri = AdtranOltHandler.GPON_PON_CONFIG_URI.format(self._pon_id)
         name = 'pon-get-config-{}'.format(self._pon_id)
         return self._parent.rest_client.request('GET', uri, name=name)
 
     def get_onu_config(self, onu_id=None):
-        uri = AdtranOltHandler.GPON_PON_ONU_CONFIG_URI.format(self._pon_id)
-        if onu_id is not None:
-            uri += '={}'.format(onu_id)
+        if onu_id is None:
+            uri = AdtranOltHandler.GPON_ONU_CONFIG_LIST_URI.format(self._pon_id)
+        else:
+            uri = AdtranOltHandler.GPON_ONU_CONFIG_URI.format(self._pon_id, onu_id)
+
         name = 'pon-get-onu_config-{}-{}'.format(self._pon_id, onu_id)
         return self._parent.rest_client.request('GET', uri, name=name)
 
@@ -337,30 +428,28 @@
         name = 'pon-set-config-{}-{}-{}'.format(self._pon_id, leaf, str(value))
         return self._parent.rest_client.request('PATCH', uri, data=data, name=name)
 
-    def discover_onus(self):
-        self.log.debug('Initiating discover of ONU/ONTs')
+    def _discover_onus(self):
+        self.log.debug('discovery')
 
         if self._admin_state == AdminState.ENABLED:
             data = json.dumps({'pon-id': self._pon_id})
             uri = AdtranOltHandler.GPON_PON_DISCOVER_ONU
             name = 'pon-discover-onu-{}'.format(self._pon_id)
 
-            self._deferred = self._parent.rest_client.request('POST', uri, data, name=name)
-            self._deferred.addBoth(self.onu_discovery_init_complete)
+            self._discovery_deferred = self._parent.rest_client.request('POST', uri, data, name=name)
+            self._discovery_deferred.addBoth(self._onu_discovery_init_complete)
 
-    def onu_discovery_init_complete(self, _):
+    def _onu_discovery_init_complete(self, _):
         """
         This method is called after the REST POST to request ONU discovery is
         completed.  The results (body) of the post is always empty / 204 NO CONTENT
         """
-        self.log.debug('ONU Discovery requested')
-
         # Reschedule
 
         delay = self._no_onu_discover_tick if len(self._onus) == 0 else self._discovery_tick
         delay += random.uniform(-delay / 10, delay / 10)
 
-        self._deferred = reactor.callLater(delay, self.discover_onus)
+        self._discovery_deferred = reactor.callLater(delay, self._discover_onus)
 
     def process_status_poll(self, status):
         """
@@ -368,7 +457,7 @@
         
         :param status: (OltState.Pon object) results from RESTCONF GET
         """
-        self.log.debug('process_status_poll:  {}{}'.format(os.linesep, status))
+        self.log.debug('process-status-poll', status=status)
 
         if self._admin_state != AdminState.ENABLED:
             return
@@ -380,7 +469,7 @@
 
         for onu_id in new:
             # self.add_new_onu(serial_number, status)
-            self.log.info('Found ONU {} in status list'.format(onu_id))
+            self.log.info('found-ONU', onu_id=onu_id)
             raise NotImplementedError('TODO: Adding ONUs from existing ONU (status list) not supported')
 
         # Get new/missing from the discovered ONU leaf
@@ -389,7 +478,7 @@
 
         # TODO: Do something useful (Does the discovery list clear out activated ONU's?)
         # if len(missing):
-        #     self.log.info('Missing ONUs are: {}'.format(missing))
+        #     self.log.info('missing-ONUs', missing=missing)
 
         for serial_number in new:
             reactor.callLater(0, self.add_onu, serial_number, status)
@@ -406,7 +495,7 @@
 
         :param onus: (dict) Set of known ONUs
         """
-        self.log.debug('Processing ONU list: {}'.format(onus))
+        self.log.debug('ONU-list', onus=onus)
 
         my_onu_ids = frozenset([o.onu_id for o in self._onus.itervalues()])
         discovered_onus = frozenset(onus.keys())
@@ -425,7 +514,13 @@
         
         :param discovered_onus: (frozenset) Set of ONUs currently discovered
         """
-        self.log.debug('Processing discovered ONU list: {}'.format(discovered_onus))
+        self.log.debug('discovered-ONUs', list=discovered_onus)
+
+        # Only request discovery if activation is auto-discovery or auto-activate
+        continue_discovery = ['autodiscovery', 'autoactivate']
+
+        if self._activation_method not in continue_discovery:
+            return set(), set()
 
         my_onus = frozenset(self._onus.keys())
 
@@ -434,36 +529,90 @@
 
         return new_onus, missing_onus
 
+    def _get_onu_info(self, serial_number):
+        """
+        Parse through available xPON information for ONU configuration settings
+        :param serial_number: (string) Decoded (not base64) serial number string
+        :return: (dict) onu config data or None on lookup failure
+        """
+        try:
+            from flow.demo_data import get_tconts, get_gem_ports
+            
+            if self.activation_method == "autoactivate":
+                onu_id = self.get_next_onu_id()
+                enabled = True
+                channel_speed = 0
+
+            elif self.activation_method == "autodiscovery":
+                if self.authentication_method == 'serial-number':
+                    gpon_info = self.olt.get_xpon_info(self.pon_id)
+
+                    try:
+                        vont_info = next(info for _, info in gpon_info['v_ont_anis'].items()
+                                         if info.get('expected-serial-number') == serial_number)
+
+                        onu_id = vont_info['onu-id']
+                        enabled = vont_info['enabled']
+                        channel_speed = vont_info['upstream-channel-speed']
+
+                    except StopIteration:
+                        return None
+                else:
+                    return None
+            else:
+                return None
+
+            onu_info = {
+                'serial-number': serial_number,
+                'xpon-name': None,
+                'pon': self,
+                'onu-id': onu_id,
+                'enabled': enabled,
+                'upstream-channel-speed': channel_speed,
+                'password': Onu.DEFAULT_PASSWORD,
+                't-conts': get_tconts(self.pon_id, serial_number, onu_id),
+                'gem-ports': get_gem_ports(self.pon_id, serial_number, onu_id),
+            }
+            return onu_info
+
+        except Exception as e:
+            self.log.exception('get-onu-info', e=e)
+            return None
+
     @inlineCallbacks
     def add_onu(self, serial_number, status):
-        self.log.info('Add ONU: {}'.format(serial_number))
+        self.log.info('add-ONU', serial_number=serial_number)
 
         if serial_number not in status.onus:
             # Newly found and not enabled ONU, enable it now if not at max
 
-            if len(self._onus) < self.MAX_ONUS_SUPPORTED:
-                # TODO: For now, always allow any ONU to be activated
+            if len(self._onus) >= self.MAX_ONUS_SUPPORTED:
+                self.log.warning('max-onus-provisioned')
+            else:
+                onu_info = self._get_onu_info(Onu.serial_number_to_string(serial_number))
 
-                if serial_number not in self._onus:
+                if onu_info is None:
+                    self.log.info('lookup-failure', serial_number=serial_number)
+
+                elif serial_number in self._onus or onu_info['onu-id'] in self._onu_by_id:
+                    self.log.warning('onu-already-added', serial_number=serial_number)
+
+                else:
+                    # TODO: Make use of upstream_channel_speed variable
+                    onu = Onu(onu_info)
+                    self._onus[serial_number] = onu
+                    self._onu_by_id[onu.onu_id] = onu
+
                     try:
-                        onu = Onu(serial_number, self)
-                        yield onu.create(True)
-
-                        self.on_new_onu_discovered(onu)
-                        self._onus[serial_number] = onu
+                        yield onu.create(onu_info)
+                        self.activate_onu(onu)
 
                     except Exception as e:
-                        self.log.exception('Exception during add_onu, onu: {}'.format(onu.onu_id), e=e)
-                else:
-                    self.log.info('TODO: Code this')
+                        del self._onus[serial_number]
+                        del self._onu_by_id[onu.onu_id]
+                        self.log.exception('add_onu', serial_number=serial_number, e=e)
 
-            else:
-                self.log.warning('Maximum number of ONUs already provisioned')
-        else:
-            # ONU has been enabled
-            pass
-
-    def on_new_onu_discovered(self, onu):
+    def activate_onu(self, onu):
         """
         Called when a new ONU is discovered and VOLTHA device adapter needs to be informed
         :param onu: 
@@ -477,7 +626,7 @@
 
         adapter.child_device_detected(parent_device_id=olt.device_id,
                                       parent_port_no=self._port_no,
-                                      child_device_type=onu.vendor_device,
+                                      child_device_type=onu.vendor_id,
                                       proxy_address=proxy,
                                       admin_state=AdminState.ENABLED,
                                       vlan=channel_id)
@@ -496,10 +645,108 @@
                 return onu_id
 
     def delete_onu(self, onu_id):
-        uri = AdtranOltHandler.GPON_PON_ONU_CONFIG_URI.format(self._pon_id)
-        uri += '={}'.format(onu_id)
+        uri = AdtranOltHandler.GPON_ONU_CONFIG_URI.format(self._pon_id, onu_id)
         name = 'pon-delete-onu-{}-{}'.format(self._pon_id, onu_id)
 
+        # Remove from any local dictionary
+        if onu_id in self._onu_by_id:
+            del self._onu_by_id[onu_id]
+        for sn in [onu.serial_numbers for onu in self._onus.itervalues() if onu.onu_id == onu_id]:
+            del self._onus[sn]
+
         # TODO: Need removal from VOLTHA child_device method
 
         return self._parent.rest_client.request('DELETE', uri, name=name)
+
+    @inlineCallbacks
+    def channel_partition(self, name, partition=0, xpon_system=0, operation=None):
+        """
+        Delete/enable/disable a specified channel partition on this PON.
+
+        When creating a new Channel Partition, create it disabled, then define any associated
+        Channel Pairs. Then enable the Channel Partition.
+
+        :param name: (string) Name of the channel partition
+        :param partition: (int: 0..15) An index of the operator-specified channel subset
+                          in a NG-PON2 system. For XGS-PON, this is typically 0
+        :param xpon_system: (int: 0..1048575) Identifies a specific xPON system
+        :param operation: (string) 'delete', 'enable', or 'disable'
+        """
+        if operation.lower() not in ['delete', 'enable', 'disable']:
+            raise ValueError('Unsupported operation: {}'.format(operation))
+
+        try:
+            xml = 'interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"'
+
+            if operation.lower() is 'delete':
+                xml += '<interface operation="delete">'
+            else:
+                xml += '<interface>'
+                xml += '<type xmlns:adtn-xp="http://www.adtran.com/ns/yang/adtran-xpon">' +\
+                       'adtn-xp:xpon-channel-partition</type>'
+                xml += '<adtn-xp:channel-partition xmlns:adtn-xp="http://www.adtran.com/ns/yang/adtran-xpon">'
+                xml += '  <adtn-xp:partition-id>{}</adtn-xp:partition-id>'.format(partition)
+                xml += '  <adtn-xp:xpon-system>{}</adtn-xp:xpon-system>'.format(xpon_system)
+                xml += '</adtn-xp:channel-partition>'
+                xml += '<enabled>{}</enabled>'.format('true' if operation.lower() == 'enable' else 'false')
+
+            xml += '<name>{}</name>'.format(name)
+            xml += '</interface></interfaces>'
+
+            results = yield self.olt.netconf_client.edit_config(xml)
+            returnValue(results)
+
+        except Exception as e:
+            self.log.exception('channel_partition')
+            raise
+
+    @inlineCallbacks
+    def channel_pair(self, name, partition, operation=None, **kwargs):
+        """
+        Create/delete a channel pair on a specific channel_partition for a PON
+
+        :param name: (string) Name of the channel pair
+        :param partition: (string) Name of the channel partition
+        :param operation: (string) 'delete', 'enable', or 'disable'
+        :param kwargs: (dict) Additional leaf settings if desired
+        """
+        if operation.lower() not in ['delete', 'enable', 'disable']:
+            raise ValueError('Unsupported operation: {}'.format(operation))
+
+        try:
+            xml = 'interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"'
+
+            if operation.lower() is 'delete':
+                xml += '<interface operation="delete">'
+            else:
+                xml += '<interface>'
+                xml += '<type xmlns:adtn-xp="http://www.adtran.com/ns/yang/adtran-xpon">' +\
+                       'adtn-xp:xpon-channel-pair</type>'
+                xml += '<adtn-xp:channel-pair xmlns:adtn-xp="http://www.adtran.com/ns/yang/adtran-xpon">'
+                xml += '  <adtn-xp:channel-partition>{}</adtn-xp:channel-partition>'.format(partition)
+                xml += '  <adtn-xp:channel-termination>channel-termination {}</adtn-xp:channel-termination>'.\
+                    format(self.pon_id)
+                xml += '  <adtn-xp:upstream-admin-label>{}</adtn-xp:upstream-admin-label>'.\
+                    format(kwargs.get('upstream-admin-label', 1))
+                xml += '  <adtn-xp:downstream-admin-label>{}</adtn-xp:downstream-admin-label>'.\
+                    format(kwargs.get('downstream-admin-label', 1))
+                xml += '  <adtn-xp:upstream-channel-id>{}</adtn-xp:upstream-channel-id>'.\
+                    format(kwargs.get('upstream-channel-id', 15))
+                xml += '  <adtn-xp:downstream-channel-id>{}</adtn-xp:downstream-channel-id>'.\
+                    format(kwargs.get('downstream-channel-id', 15))
+                xml += '  <adtn-xp:downstream-channel-fec-enable>{}</adtn-xp:downstream-channel-fec-enable>'. \
+                    format('true' if kwargs.get('downstream-channel-fec-enable', True) else 'false')
+                xml += '  <adtn-xp:upstream-channel-fec-enable>{}</adtn-xp:upstream-channel-fec-enable>'. \
+                    format('true' if kwargs.get('upstream-channel-fec-enable', True) else 'false')
+                xml += '</adtn-xp:channel-pair>'
+                # TODO: Add support for upstream/downstream FEC-enable coming from here and not hard-coded
+
+            xml += '<name>{}</name>'.format(name)
+            xml += '</interface></interfaces>'
+
+            results = yield self.olt.netconf_client.edit_config(xml)
+            returnValue(results)
+
+        except Exception as e:
+            self.log.exception('channel_pair')
+            raise