ADTRAN Adapter Tests

Change-Id: Ib965b35616fc9691f4ab5ed399430bc676369f28
diff --git a/.gitignore b/.gitignore
index 4dccc3d..3990dc6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -85,7 +85,7 @@
 docs/manual/user/*.pdf
 docs/manuals/user/node_modules/
 .coverage
-coverage.xml
+*coverage.xml
 nosetests.xml
 
 # OpenOLT repo
diff --git a/voltha/adapters/adtran_olt/.coveragerc b/voltha/adapters/adtran_olt/.coveragerc
new file mode 100644
index 0000000..aa8118f
--- /dev/null
+++ b/voltha/adapters/adtran_olt/.coveragerc
@@ -0,0 +1,8 @@
+[run]
+branch = True
+omit = venv/*, test/*
+parallel = True
+
+[report]
+
+[html]
diff --git a/voltha/adapters/adtran_olt/.gitignore b/voltha/adapters/adtran_olt/.gitignore
new file mode 100644
index 0000000..88158a1
--- /dev/null
+++ b/voltha/adapters/adtran_olt/.gitignore
@@ -0,0 +1,2 @@
+htmlcov/
+prof/
diff --git a/voltha/adapters/adtran_olt/.pylintrc b/voltha/adapters/adtran_olt/.pylintrc
new file mode 100644
index 0000000..41cf299
--- /dev/null
+++ b/voltha/adapters/adtran_olt/.pylintrc
@@ -0,0 +1,551 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint.
+jobs=1
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Specify a configuration file.
+#rcfile=
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=print-statement,
+        parameter-unpacking,
+        unpacking-in-except,
+        old-raise-syntax,
+        backtick,
+        long-suffix,
+        old-ne-operator,
+        old-octal-literal,
+        import-star-module-level,
+        non-ascii-bytes-literal,
+        invalid-unicode-literal,
+        raw-checker-failed,
+        bad-inline-option,
+        locally-disabled,
+        locally-enabled,
+        file-ignored,
+        suppressed-message,
+        useless-suppression,
+        deprecated-pragma,
+        apply-builtin,
+        basestring-builtin,
+        buffer-builtin,
+        cmp-builtin,
+        coerce-builtin,
+        execfile-builtin,
+        file-builtin,
+        long-builtin,
+        raw_input-builtin,
+        reduce-builtin,
+        standarderror-builtin,
+        unicode-builtin,
+        xrange-builtin,
+        coerce-method,
+        delslice-method,
+        getslice-method,
+        setslice-method,
+        no-absolute-import,
+        old-division,
+        dict-iter-method,
+        dict-view-method,
+        next-method-called,
+        metaclass-assignment,
+        indexing-exception,
+        raising-string,
+        reload-builtin,
+        oct-method,
+        hex-method,
+        nonzero-method,
+        cmp-method,
+        input-builtin,
+        round-builtin,
+        intern-builtin,
+        unichr-builtin,
+        map-builtin-not-iterating,
+        zip-builtin-not-iterating,
+        range-builtin-not-iterating,
+        filter-builtin-not-iterating,
+        using-cmp-argument,
+        eq-without-hash,
+        div-method,
+        idiv-method,
+        rdiv-method,
+        exception-message-attribute,
+        invalid-str-codec,
+        sys-max-int,
+        bad-python3-import,
+        deprecated-string-function,
+        deprecated-str-translate-call,
+        deprecated-itertools-function,
+        deprecated-types-field,
+        next-method-defined,
+        dict-items-not-iterating,
+        dict-keys-not-iterating,
+        dict-values-not-iterating,
+        deprecated-operator-function,
+        deprecated-urllib-function,
+        xreadlines-attribute,
+        deprecated-sys-function,
+        exception-escape,
+        comprehension-escape,
+        useless-object-inheritance
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[REPORTS]
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio).You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages
+reports=yes
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=optparse.Values,sys.exit
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+      XXX,
+      TODO
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+          _cb
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,io,builtins
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Number of spaces of indent required inside a hanging  or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,
+               dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+
+[BASIC]
+
+# Naming style matching correct argument names
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style
+#argument-rgx=
+
+# Naming style matching correct attribute names
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,
+          bar,
+          baz,
+          toto,
+          tutu,
+          tata
+
+# Naming style matching correct class attribute names
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style
+#class-attribute-rgx=
+
+# Naming style matching correct class names
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-style
+#class-rgx=
+
+# Naming style matching correct constant names
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,
+           j,
+           k,
+           x,
+           y,
+           ex,
+           Run,
+           _
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style
+#inlinevar-rgx=
+
+# Naming style matching correct method names
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style
+#method-rgx=
+
+# Naming style matching correct module names
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style
+#variable-rgx=
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,
+                   TERMIOS,
+                   Bastion,
+                   rexec
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+                      __new__,
+                      setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+                  _fields,
+                  _replace,
+                  _source,
+                  _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in a if statement
+max-bool-expr=5
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/voltha/adapters/adtran_olt/Makefile b/voltha/adapters/adtran_olt/Makefile
new file mode 100644
index 0000000..bbd25fb
--- /dev/null
+++ b/voltha/adapters/adtran_olt/Makefile
@@ -0,0 +1 @@
+include test.mk
\ No newline at end of file
diff --git a/voltha/adapters/adtran_olt/__init__.py b/voltha/adapters/adtran_olt/__init__.py
index b0fb0b2..18d64b2 100644
--- a/voltha/adapters/adtran_olt/__init__.py
+++ b/voltha/adapters/adtran_olt/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2017-present Open Networking Foundation
+# Copyright 2017-present Adtran, Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/voltha/adapters/adtran_olt/adtran_device_handler.py b/voltha/adapters/adtran_olt/adtran_device_handler.py
index c67fb40..6460df4 100644
--- a/voltha/adapters/adtran_olt/adtran_device_handler.py
+++ b/voltha/adapters/adtran_olt/adtran_device_handler.py
@@ -35,6 +35,8 @@
 from voltha.extensions.kpi.olt.olt_pm_metrics import OltPmMetrics
 from common.utils.asleep import asleep
 from flow.flow_tables import DeviceFlows, DownstreamFlows
+from net.pio_zmq import DEFAULT_PIO_TCP_PORT
+from net.pon_zmq import DEFAULT_PON_AGENT_TCP_PORT
 
 _ = third_party
 
@@ -54,6 +56,12 @@
 _DEFAULT_RESOURCE_MGR_KEY = "adtran"
 
 
+#############################################################
+# Raise any Parsing Errors rather than sys.exit
+def _parser_error(message):
+    raise argparse.ArgumentTypeError(message)
+
+
 class AdtranDeviceHandler(object):
     """
     A device that supports the ADTRAN RESTCONF protocol for communications
@@ -85,10 +93,43 @@
     # RPC XML shortcuts
     RESTART_RPC = '<system-restart xmlns="urn:ietf:params:xml:ns:yang:ietf-system"/>'
 
-    def __init__(self, **kwargs):
-        from net.pio_zmq import DEFAULT_PIO_TCP_PORT
-        from net.pon_zmq import DEFAULT_PON_AGENT_TCP_PORT
+    # CONFIG PARSING
+    PARSER = argparse.ArgumentParser(description='Adtran Device Adapter')
+    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=int, choices=range(1, 65536), help='NETCONF 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=int, choices=range(1, 65536), help='RESTCONF TCP Port')
+    PARSER.add_argument('--zmq_port', '-z', action='store', default=DEFAULT_PON_AGENT_TCP_PORT,
+                        type=int, choices=range(1, 65536), help='PON Agent ZeroMQ Port')
+    PARSER.add_argument('--pio_port', '-Z', action='store', default=DEFAULT_PIO_TCP_PORT,
+                        type=int, choices=range(1, 65536), help='PIO Service ZeroMQ Port')
+    PARSER.add_argument('--multicast_vlan', '-M', action='store',
+                        metavar='int', type=int, choices=range(1, 4095),
+                        default=[DEFAULT_MULTICAST_VLAN],
+                        nargs='+', help='Multicast VLANs are 1..4094'),
+    PARSER.add_argument('--utility_vlan', '-B', action='store',
+                        metavar='int', type=int, choices=range(1, 4095),
+                        default=DEFAULT_UTILITY_VLAN,
+                        help='VLAN for Controller based upstream flows from ONUs')
+    PARSER.add_argument('--resource_mgr_key', '-o', action='store',
+                        default=_DEFAULT_RESOURCE_MGR_KEY,
+                        help='OLT Type to look up associated resource manager configuration')
+    PARSER.error = _parser_error
 
+    # Timeout Waiting on Rest Connectivity before initiating next HEARTBEAT
+    HEARTBEAT_TIMEOUT = 5
+
+    NC_CLIENT = AdtranNetconfClient
+
+    def __init__(self, **kwargs):
         super(AdtranDeviceHandler, self).__init__()
 
         adapter = kwargs['adapter']
@@ -159,9 +200,6 @@
         self.heartbeat = None
         self.heartbeat_last_reason = ''
 
-        # Virtualized OLT Support
-        self.is_virtual_olt = False
-
         # Installed flows
         self._evcs = {}  # Flow ID/name -> FlowEntry
 
@@ -185,15 +223,8 @@
 
     def __del__(self):
         # Kill any startup or heartbeat defers
-
-        d, self.startup = self.startup, None
-        h, self.heartbeat = self.heartbeat, None
-
-        if d is not None and not d.called:
-            d.cancel()
-
-        if h is not None and not h.called:
-            h.cancel()
+        self._cancel_tasks()
+        self._suspend_heartbeat()
 
         # Remove the logical device
         self._delete_logical_device()
@@ -216,6 +247,13 @@
     def evcs(self):
         return list(self._evcs.values())
 
+    @property
+    def all_ports(self):
+        for port in self.northbound_ports.itervalues():
+            yield port
+        for port in self.southbound_ports.itervalues():
+            yield port
+
     def add_evc(self, evc):
         if self._evcs is not None and evc.name not in self._evcs:
             self._evcs[evc.name] = evc
@@ -225,9 +263,6 @@
             del self._evcs[evc.name]
 
     def parse_provisioning_options(self, device):
-        from net.pon_zmq import DEFAULT_PON_AGENT_TCP_PORT
-        from net.pio_zmq import DEFAULT_PIO_TCP_PORT
-
         if device.ipv4_address:
             self.ip_address = device.ipv4_address
             self.host_and_port = '{}:{}'.format(self.ip_address,
@@ -241,51 +276,12 @@
         else:
             self.activate_failed(device, 'No IP_address field provided')
 
-        #############################################################
-        # Now optional parameters
-        def check_tcp_port(value):
-            ivalue = int(value)
-            if ivalue <= 0 or ivalue > 65535:
-                raise argparse.ArgumentTypeError("%s is a not a valid port number" % value)
-            return ivalue
-
-        def check_vid(value):
-            ivalue = int(value)
-            if ivalue < 1 or ivalue > 4094:
-                raise argparse.ArgumentTypeError("Valid VLANs are 1..4094")
-            return ivalue
-
-        parser = argparse.ArgumentParser(description='Adtran Device Adapter')
-        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=_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_PON_AGENT_TCP_PORT,
-                            type=check_tcp_port, help='PON Agent ZeroMQ Port')
-        parser.add_argument('--pio_port', '-Z', action='store', default=DEFAULT_PIO_TCP_PORT,
-                            type=check_tcp_port, help='PIO Service ZeroMQ Port')
-        parser.add_argument('--multicast_vlan', '-M', action='store',
-                            default='{}'.format(DEFAULT_MULTICAST_VLAN),
-                            help='Multicast VLAN'),
-        parser.add_argument('--utility_vlan', '-B', action='store',
-                            default='{}'.format(DEFAULT_UTILITY_VLAN),
-                            type=check_vid, help='VLAN for Controller based upstream flows from ONUs')
-        parser.add_argument('--resource_mgr_key', '-o', action='store',
-                            default=_DEFAULT_RESOURCE_MGR_KEY,
-                            help='OLT Type to look up associated resource manager configuration')
         try:
-            args = parser.parse_args(shlex.split(device.extra_args))
+            args = self.PARSER.parse_args(shlex.split(device.extra_args))
 
             # May have multiple multicast VLANs
-            self.multicast_vlans = [int(vid.strip()) for vid in args.multicast_vlan.split(',')]
+            self.multicast_vlans = args.multicast_vlan
+            self.utility_vlan = args.utility_vlan
 
             self.netconf_username = args.nc_username
             self.netconf_password = args.nc_password
@@ -312,7 +308,7 @@
                 self.netconf_password = 'NDI0ZjUzNDM0Zg==\n'.\
                     decode('base64').decode('hex')
 
-        except argparse.ArgumentError as e:
+        except argparse.ArgumentTypeError as e:
             self.activate_failed(device,
                                  'Invalid arguments: {}'.format(e.message),
                                  reachable=False)
@@ -338,10 +334,6 @@
                 self.parse_provisioning_options(device)
 
                 ############################################################################
-                # Currently, only virtual OLT (pizzabox) is supported
-                # self.is_virtual_olt = Add test for MOCK Device if we want to support it
-
-                ############################################################################
                 # Start initial discovery of NETCONF support (if any)
                 try:
                     device.reason = 'establishing NETCONF connection'
@@ -498,10 +490,7 @@
                     device.reason = 'setting device to a known initial state'
                     self.adapter_agent.update_device(device)
                     try:
-                        for port in self.northbound_ports.itervalues():
-                            self.startup = yield port.reset()
-
-                        for port in self.southbound_ports.itervalues():
+                        for port in self.all_ports:
                             self.startup = yield port.reset()
 
                     except Exception as e:
@@ -577,12 +566,7 @@
         :param done_deferred: (deferred) Deferred to fire upon completion of activation
         :param reconciling: (bool) If true, we are reconciling after moving to a new vCore
         """
-        d, self.startup = self.startup, None
-        try:
-            if d is not None and not d.called:
-                d.cancel()
-        except:
-            pass
+        self._cancel_tasks()
         device = self.adapter_agent.get_device(self.device_id)
         device.reason = 'Failed during {}, retrying'.format(device.reason)
         self.adapter_agent.update_device(device)
@@ -593,7 +577,7 @@
     @inlineCallbacks
     def ready_network_access(self):
         # Override in device specific class if needed
-        returnValue('nop')
+        yield defer.Deferred()
 
     def activate_failed(self, device, reason, reachable=True):
         """
@@ -615,32 +599,32 @@
         raise Exception('Failed to activate OLT: {}'.format(device.reason))
 
     @inlineCallbacks
+    def _close_netconf_connection(self):
+        resp = None
+        if self.netconf_client:
+            try:
+                resp = yield self.netconf_client.close()
+            except Exception as e:
+                self.log.exception('NETCONF-shutdown', e, device_id=self.device_id)
+            finally:
+                self._netconf_client = None
+        returnValue(resp)
+
+    @inlineCallbacks
     def make_netconf_connection(self, connect_timeout=None,
                                 close_existing_client=False):
 
-        if close_existing_client and self._netconf_client is not None:
-            try:
-                yield self._netconf_client.close()
-            except:
-                pass
-            self._netconf_client = None
+        if close_existing_client:
+            yield self._close_netconf_connection()
 
-        client = self._netconf_client
+        client = self.netconf_client
 
         if client is None:
-            if not self.is_virtual_olt:
-                client = AdtranNetconfClient(self.ip_address,
-                                             self.netconf_port,
-                                             self.netconf_username,
-                                             self.netconf_password,
-                                             self.timeout)
-            else:
-                from voltha.adapters.adtran_olt.net.mock_netconf_client import MockNetconfClient
-                client = MockNetconfClient(self.ip_address,
-                                           self.netconf_port,
-                                           self.netconf_username,
-                                           self.netconf_password,
-                                           self.timeout)
+            client = self.NC_CLIENT(self.ip_address,
+                                    self.netconf_port,
+                                    self.netconf_username,
+                                    self.netconf_password,
+                                    self.timeout)
         if client.connected:
             self._netconf_client = client
             returnValue(True)
@@ -648,8 +632,7 @@
         timeout = connect_timeout or self.timeout
 
         try:
-            request = client.connect(timeout)
-            results = yield request
+            results = yield client.connect(timeout)
             self._netconf_client = client
             returnValue(results)
 
@@ -713,12 +696,7 @@
         if not reconciling:
             # Add the ports to the logical device
 
-            for port in self.northbound_ports.itervalues():
-                lp = port.get_logical_port()
-                if lp is not None:
-                    self.adapter_agent.add_logical_port(ld_initialized.id, lp)
-
-            for port in self.southbound_ports.itervalues():
+            for port in self.all_ports:
                 lp = port.get_logical_port()
                 if lp is not None:
                     self.adapter_agent.add_logical_port(ld_initialized.id, lp)
@@ -766,7 +744,6 @@
         for port in self.southbound_ports.itervalues():
             try:
                 dl.append(port.start() if port.admin_state == AdminState.ENABLED else port.stop())
-
             except Exception as e:
                 self.log.exception('southbound-port-startup', e=e)
 
@@ -872,7 +849,7 @@
     @inlineCallbacks
     def complete_device_specific_activation(self, _device, _reconciling):
         # NOTE: Override this in your derived class for any device startup completion
-        return defer.succeed('NOP')
+        yield defer.Deferred()
 
     @inlineCallbacks
     def disable(self):
@@ -880,14 +857,7 @@
         This is called when a previously enabled device needs to be disabled based on a NBI call.
         """
         self.log.info('disabling', device_id=self.device_id)
-
-        # Cancel any running enable/disable/... in progress
-        d, self.startup = self.startup, None
-        try:
-            if d is not None and not d.called:
-                d.cancel()
-        except:
-            pass
+        self._cancel_tasks()
 
         # Get the latest device reference
         device = self.adapter_agent.get_device(self.device_id)
@@ -896,16 +866,9 @@
 
         # Drop registration for ONU detection
         # self.adapter_agent.unregister_for_onu_detect_state(self.device.id)
-        # Suspend any active healthchecks / pings
+        self._suspend_heartbeat()
 
-        h, self.heartbeat = self.heartbeat, None
-        try:
-            if h is not None and not h.called:
-                h.cancel()
-        except:
-            pass
         # Update the operational status to UNKNOWN
-
         device.oper_status = OperStatus.UNKNOWN
         device.connect_status = ConnectStatus.UNREACHABLE
         self.adapter_agent.update_device(device)
@@ -925,10 +888,7 @@
         self.adapter_agent.disable_all_ports(self.device_id)
 
         dl = []
-        for port in self.northbound_ports.itervalues():
-            dl.append(port.stop())
-
-        for port in self.southbound_ports.itervalues():
+        for port in self.all_ports:
             dl.append(port.stop())
 
         # NOTE: Flows removed before this method is called
@@ -955,14 +915,7 @@
         :param done_deferred: (Deferred) Deferred to fire when done
         """
         self.log.info('re-enabling', device_id=self.device_id)
-
-        # Cancel any running enable/disable/... in progress
-        d, self.startup = self.startup, None
-        try:
-            if d is not None and not d.called:
-                d.cancel()
-        except:
-            pass
+        self._cancel_tasks()
 
         if not self._initial_enable_complete:
             # Never contacted the device on the initial startup, do 'activate' steps instead
@@ -1040,31 +993,23 @@
         This is called to reboot a device based on a NBI call.  The admin state of the device
         will not change after the reboot.
         """
-        self.log.debug('reboot')
+        self.log.debug('reboot', device_id=self.device_id)
 
         if not self._initial_enable_complete:
             # Never contacted the device on the initial startup, do 'activate' steps instead
             returnValue('failed')
 
-        # Cancel any running enable/disable/... in progress
-        d, self.startup = self.startup, None
-        try:
-            if d is not None and not d.called:
-                d.cancel()
-        except:
-            pass
+        self._cancel_tasks()
         # Issue reboot command
 
-        if not self.is_virtual_olt:
-            try:
-                yield self.netconf_client.rpc(AdtranDeviceHandler.RESTART_RPC)
+        try:
+            yield self.netconf_client.rpc(AdtranDeviceHandler.RESTART_RPC)
 
-            except Exception as e:
-                self.log.exception('NETCONF-shutdown', e=e)
-                returnValue(defer.fail(Failure()))
+        except Exception as e:
+            self.log.exception('NETCONF-shutdown', e=e)
+            returnValue(defer.fail(Failure()))
 
         # self.adapter_agent.unregister_for_onu_detect_state(self.device.id)
-
         # Update the operational status to ACTIVATING and connect status to
         # UNREACHABLE
 
@@ -1082,16 +1027,10 @@
         # Shutdown communications with OLT. Typically it takes about 2 seconds
         # or so after the reply before the restart actually occurs
 
-        try:
-            response = yield self.netconf_client.close()
+        response = yield self._close_netconf_connection()
+        if hasattr(response, 'ok'):
             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)
-
-        # Clear off clients
-
-        self._netconf_client = None
         self._rest_client = None
 
         # Run remainder of reboot process as a new task. The OLT then may be up in a
@@ -1123,17 +1062,10 @@
         if self.netconf_client is None:
             try:
                 yield self.make_netconf_connection(connect_timeout=10)
+            except:
+                yield self._close_netconf_connection()
 
-            except Exception as e:
-                try:
-                    if self.netconf_client is not None:
-                        yield self.netconf_client.close()
-                except Exception as e:
-                    self.log.exception(e.message)
-                finally:
-                    self._netconf_client = None
-
-        if (self.netconf_client is None and not self.is_virtual_olt) or self.rest_client is None:
+        if self.netconf_client is None or self.rest_client is None:
             current_time = time.time()
             if current_time < timeout:
                 self.startup = reactor.callLater(5, self._finish_reboot, timeout,
@@ -1141,7 +1073,7 @@
                                                  previous_conn_status)
                 returnValue(self.startup)
 
-            if self.netconf_client is None and not self.is_virtual_olt:
+            if self.netconf_client is None:
                 self.log.error('NETCONF-restore-failure')
                 pass        # TODO: What is best course of action if cannot get clients back?
 
@@ -1165,10 +1097,7 @@
         # Restart ports to previous state
         dl = []
 
-        for port in self.northbound_ports.itervalues():
-            dl.append(port.restart())
-
-        for port in self.southbound_ports.itervalues():
+        for port in self.all_ports:
             dl.append(port.restart())
 
         try:
@@ -1193,6 +1122,15 @@
         self.log.info('rebooted', device_id=self.device_id)
         returnValue('Rebooted')
 
+    def _cancel_tasks(self):
+        # Cancel any outstanding tasks
+        d, self.startup = self.startup, None
+        try:
+            if d is not None and not d.called:
+                d.cancel()
+        except:
+            pass
+
     @inlineCallbacks
     def delete(self):
         """
@@ -1200,21 +1138,8 @@
         If the device is an OLT then the whole PON will be deleted.
         """
         self.log.info('deleting', device_id=self.device_id)
-
-        # Cancel any outstanding tasks
-
-        d, self.startup = self.startup, None
-        try:
-            if d is not None and not d.called:
-                d.cancel()
-        except:
-            pass
-        h, self.heartbeat = self.heartbeat, None
-        try:
-            if h is not None and not h.called:
-                h.cancel()
-        except:
-            pass
+        self._cancel_tasks()
+        self._suspend_heartbeat()
 
         # Get the latest device reference
         device = self.adapter_agent.get_device(self.device_id)
@@ -1243,28 +1168,17 @@
 
         # Tell all ports to stop any background processing
 
-        for port in self.northbound_ports.itervalues():
-            port.delete()
-
-        for port in self.southbound_ports.itervalues():
+        for port in self.all_ports:
             port.delete()
 
         self.northbound_ports.clear()
         self.southbound_ports.clear()
 
         # Shutdown communications with OLT
-
-        if self.netconf_client is not None:
-            try:
-                yield self.netconf_client.close()
-            except Exception as e:
-                self.log.exception('NETCONF-shutdown', e=e)
-
-            self._netconf_client = None
-
+        yield self._close_netconf_connection()
         self._rest_client = None
         mgr, self.resource_mgr = self.resource_mgr, None
-        if mgr is not None:
+        if mgr:
             del mgr
 
         self.log.info('deleted', device_id=self.device_id)
@@ -1307,6 +1221,7 @@
                 specific extensions. Such extensions shall be described as part of
                 the device type specification returned by device_types().
         """
+        yield None
         device = {}
         returnValue(device)
 
@@ -1316,18 +1231,26 @@
         self.heartbeat = reactor.callLater(delay, self.check_pulse)
         return self.heartbeat
 
+    def _suspend_heartbeat(self):
+        # Suspend any active health-checks / pings
+        h, self.heartbeat = self.heartbeat, None
+        try:
+            if h is not None and not h.called:
+                h.cancel()
+        except:
+            pass
+
     def check_pulse(self):
         if self.logical_device_id is not None:
             try:
                 self.heartbeat = self.rest_client.request('GET', self.HELLO_URI,
-                                                          name='hello', timeout=5)
-                self.heartbeat.addCallbacks(self._heartbeat_success, self._heartbeat_fail)
-
+                                                          name='hello', timeout=self.HEARTBEAT_TIMEOUT)
+                self.heartbeat.addCallbacks(self._heartbeat_success)
             except Exception as e:
-                self.heartbeat = reactor.callLater(5, self._heartbeat_fail, e)
+                self.heartbeat = reactor.callLater(self.HEARTBEAT_TIMEOUT, self._heartbeat_fail, e)
 
     def on_heatbeat_alarm(self, active):
-        if active and self.netconf_client is None or not self.netconf_client.connected:
+        if active and (self.netconf_client is None or not self.netconf_client.connected):
             self.make_netconf_connection(close_existing_client=True)
 
     def heartbeat_check_status(self, _):
diff --git a/voltha/adapters/adtran_olt/adtran_olt.py b/voltha/adapters/adtran_olt/adtran_olt.py
index b38843b..81402af 100644
--- a/voltha/adapters/adtran_olt/adtran_olt.py
+++ b/voltha/adapters/adtran_olt/adtran_olt.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+
 """
 Adtran 1-U OLT adapter.
 """
@@ -325,7 +326,7 @@
         handler = self.devices_handlers.get(device.id)
         if handler is not None:
             reactor.callLater(0, handler.delete)
-            del self.device_handlers[device.id]
+            del self.devices_handlers[device.id]
             del self.logical_device_id_to_root_device_id[device.parent_id]
 
         return device
diff --git a/voltha/adapters/adtran_olt/adtran_olt_handler.py b/voltha/adapters/adtran_olt/adtran_olt_handler.py
index 3cb68e5..fd48d77 100644
--- a/voltha/adapters/adtran_olt/adtran_olt_handler.py
+++ b/voltha/adapters/adtran_olt/adtran_olt_handler.py
@@ -132,7 +132,7 @@
             self.rest_client.request('PATCH', uri, data=data, name='olt-system-id')
 
     @inlineCallbacks
-    def get_device_info(self, device):
+    def get_device_info(self, _device):
         """
         Perform an initial network operation to discover the device hardware
         and software version. Serial Number would be helpful as well.
@@ -140,7 +140,7 @@
         Upon successfully retrieving the information, remember to call the
         'start_heartbeat' method to keep in contact with the device being managed
 
-        :param device: A voltha.Device object, with possible device-type
+        :param _device: A voltha.Device object, with possible device-type
                 specific extensions. Such extensions shall be described as part of
                 the device type specification returned by device_types().
         """
diff --git a/voltha/adapters/adtran_olt/codec/__init__.py b/voltha/adapters/adtran_olt/codec/__init__.py
index b0fb0b2..18d64b2 100644
--- a/voltha/adapters/adtran_olt/codec/__init__.py
+++ b/voltha/adapters/adtran_olt/codec/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2017-present Open Networking Foundation
+# Copyright 2017-present Adtran, Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/voltha/adapters/adtran_olt/codec/ietf_interfaces.py b/voltha/adapters/adtran_olt/codec/ietf_interfaces.py
index 600923d..5f0d8c4 100644
--- a/voltha/adapters/adtran_olt/codec/ietf_interfaces.py
+++ b/voltha/adapters/adtran_olt/codec/ietf_interfaces.py
@@ -63,6 +63,10 @@
 
 class IetfInterfacesConfig(object):
     def __init__(self, session):
+        """
+
+        :param session: this should be a netconf session
+        """
         self._session = session
 
     @inlineCallbacks
diff --git a/voltha/adapters/adtran_olt/codec/olt_config.py b/voltha/adapters/adtran_olt/codec/olt_config.py
index cd3416b..31e13ce 100644
--- a/voltha/adapters/adtran_olt/codec/olt_config.py
+++ b/voltha/adapters/adtran_olt/codec/olt_config.py
@@ -164,7 +164,7 @@
             @property
             def tconts_dict(self):               # TODO: Remove if not used
                 if self._tconts_dict is None:
-                    self._tconts_dict = {tcont.alloc_id: tcont for tcont in self.tconts}
+                    self._tconts_dict = {self.tconts[tcont].alloc_id: self.tconts[tcont] for tcont in self.tconts}
                 return self._tconts_dict
 
             @property
@@ -176,7 +176,7 @@
             @property
             def gem_ports_dict(self):               # TODO: Remove if not used
                 if self._gem_ports_dict is None:
-                    self._gem_ports_dict = {gem.gem_id: gem for gem in self.gem_ports}
+                    self._gem_ports_dict = {self.gem_ports[gem].gem_id: self.gem_ports[gem] for gem in self.gem_ports}
                 return self._gem_ports_dict
 
             class TCont(object):
@@ -270,6 +270,10 @@
                     def __str__(self):
                         return "OltConfig.Pon.Onu.TCont.BestEffort: {}".format(self.bandwidth)
 
+                    @staticmethod
+                    def decode(best_effort_container):
+                        return OltConfig.Pon.Onu.TCont.BestEffort(best_effort_container)
+
                     @property
                     def bandwidth(self):
                         return self._packet['bandwidth']
diff --git a/voltha/adapters/adtran_olt/codec/olt_state.py b/voltha/adapters/adtran_olt/codec/olt_state.py
index 4103a1d..05df6d1 100644
--- a/voltha/adapters/adtran_olt/codec/olt_state.py
+++ b/voltha/adapters/adtran_olt/codec/olt_state.py
@@ -248,7 +248,7 @@
                 self._packet = packet
 
             def __str__(self):
-                return "OltState.Pon.Gem: onu-id: {}, gem-id".\
+                return "OltState.Pon.Gem: onu-id: {}, gem-id: {}".\
                     format(self.onu_id, self.gem_id)
 
             @staticmethod
diff --git a/voltha/adapters/adtran_olt/codec/physical_entities_state.py b/voltha/adapters/adtran_olt/codec/physical_entities_state.py
index cf2f085..c1a95e6 100644
--- a/voltha/adapters/adtran_olt/codec/physical_entities_state.py
+++ b/voltha/adapters/adtran_olt/codec/physical_entities_state.py
@@ -47,7 +47,7 @@
         """
         if self._rpc_reply is None:
             # TODO: Support auto-get?
-            return None
+            return
 
         result_dict = xmltodict.parse(self._rpc_reply.data_xml)
         return result_dict['data']['physical-entities-state']['physical-entity']
diff --git a/voltha/adapters/adtran_olt/docker-compose-integration-test.yml b/voltha/adapters/adtran_olt/docker-compose-integration-test.yml
new file mode 100644
index 0000000..a11d554
--- /dev/null
+++ b/voltha/adapters/adtran_olt/docker-compose-integration-test.yml
@@ -0,0 +1,344 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+version: '2'
+services:
+  #
+  # Single-node zookeeper service
+  #
+  zookeeper:
+    image: "${REGISTRY}wurstmeister/zookeeper:latest"
+    ports:
+    - 2181
+    environment:
+      SERVICE_2181_NAME: "zookeeper"
+  #
+  # Single-node kafka service
+  #
+  kafka:
+    image: "${REGISTRY}wurstmeister/kafka:latest"
+    ports:
+     - 9092
+    environment:
+      KAFKA_ADVERTISED_HOST_NAME: ${DOCKER_HOST_IP}
+      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+      KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
+      KAFKA_HEAP_OPTS: "-Xmx256M -Xms128M"
+      SERVICE_9092_NAME: "kafka"
+    depends_on:
+    - vconsul
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock
+  #
+  # Single-node consul agent
+  #
+  vconsul:
+    image: "${REGISTRY}consul:0.9.2"
+    command: agent -server -bootstrap -client 0.0.0.0 -ui
+    ports:
+    - 8300
+    - 8400
+    - 8500
+    - 8600
+    environment:
+      #SERVICE_53_IGNORE: "yes"
+      SERVICE_8300_IGNORE: "yes"
+      SERVICE_8400_IGNORE: "yes"
+      SERVICE_8500_NAME: "consul-rest"
+  #
+  # Registrator
+  #
+  registrator:
+    image: "${REGISTRY}gliderlabs/registrator:latest"
+    command: [
+      "-ip=${DOCKER_HOST_IP}",
+      "-retry-attempts", "100",
+      "-cleanup",
+      # "-internal",
+      "consul://vconsul:8500"
+    ]
+    links:
+    - vconsul
+    volumes:
+    - "/var/run/docker.sock:/tmp/docker.sock"
+
+  #
+  # Fluentd log server
+  #
+  fluentd:
+    image: "${REGISTRY}fluent/fluentd:v0.12.42"
+    ports:
+    - 24224
+    volumes:
+    - "/tmp/fluentd:/fluentd/log"
+    environment:
+      SERVICE_24224_NAME: "fluentd-intake"
+
+  #
+  # Graphite-Grafana-statsd service instance
+  # (demo place-holder for external KPI system)
+  #
+  grafana:
+    image: "${REGISTRY}${REPOSITORY}voltha-grafana${TAG}"
+    ports:
+    - 8883
+    - 2003
+    - 2004
+    - 8126
+    - 8125
+    environment:
+      SERVICE_80_NAME:   "grafana-web-ui"
+      SERVICE_2003_NAME: "carbon-plain-text-intake"
+      SERVICE_2004_NAME: "carbon-pickle-intake"
+      SERVICE_8126_NAME: "statsd-tcp-intake"
+      SERVICE_8125_NAME: "statsd-udp-intake"
+      GR_SERVER_ROOT_URL: "http://localhost:80/grafana/"
+
+  #
+  # Shovel (Kafka-graphite-gateway)
+  #
+  shovel:
+    image: "${REGISTRY}${REPOSITORY}voltha-shovel${TAG}"
+    command: [
+      "/shovel/shovel/main.py",
+      "--kafka=@kafka",
+      "--consul=${DOCKER_HOST_IP}:8500",
+      "--topic=voltha.kpis",
+      "--host=${DOCKER_HOST_IP}"
+    ]
+    depends_on:
+    - vconsul
+    - kafka
+    - grafana
+    restart: unless-stopped
+
+  #
+  # Voltha server instance(s)
+  #
+  voltha:
+    image: "${REGISTRY}${REPOSITORY}voltha-voltha${TAG}"
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "10m"
+        max-file: "3"
+#      Use the fluentd driver to push logs to fluentd instead
+#      driver: "fluentd"
+#      options:
+#        fluentd-address: ${DOCKER_HOST_IP}:24224
+    command: [
+      "/voltha/voltha/main.py",
+      "-v",
+      "--consul=${DOCKER_HOST_IP}:8500",
+      "--rest-port=8880",
+      "--grpc-port=50556",
+      "--kafka=@kafka",
+      "--instance-id-is-container-name",
+      "--interface=eth1",
+      "--backend=consul",
+      "-v"
+    ]
+    ports:
+    - 8880
+    - 50556
+    - 18880
+    - 60001
+    depends_on:
+    - vconsul
+    links:
+    - vconsul
+    environment:
+      SERVICE_8880_NAME: "voltha-health"
+      SERVICE_8880_CHECK_HTTP: "/health"
+      SERVICE_8880_CHECK_INTERVAL: "5s"
+      SERVICE_8880_CHECK_TIMEOUT: "1s"
+      SERVICE_18880_NAME: "voltha-sim-rest"
+      SERVICE_HOST_IP: "${DOCKER_HOST_IP}"
+    volumes:
+    - "/var/run/docker.sock:/tmp/docker.sock"
+    networks:
+    - default
+    - ponmgmt
+
+  envoy:
+    image: "${REGISTRY}${REPOSITORY}voltha-envoy${TAG}"
+    entrypoint:
+      - /usr/local/bin/envoyd
+      - -envoy-cfg-template
+      - "/envoy/voltha-grpc-proxy.template.json"
+      - -envoy-config
+      - "/envoy/voltha-grpc-proxy.json"
+      - -consul-svc-nme
+      - "vconsul"
+      - -kv-svc-name
+      - "vconsul"
+    ports:
+      - 50555
+      - 8882
+      - 8443
+      - 8001
+    environment:
+      SERVICE_50555_NAME: "voltha-grpc"
+    volumes:
+    - "/var/run/docker.sock:/tmp/docker.sock"
+    networks:
+    - default
+    - ponmgmt
+    links:
+    - voltha:vcore
+  #
+  # Voltha cli container
+  #
+  cli:
+    image: "${REGISTRY}${REPOSITORY}voltha-cli${TAG}"
+    command: [
+      "/cli/cli/setup.sh",
+      "-L",
+      "-C vconsul:8500",
+      "-G"
+    ]
+    environment:
+      DOCKER_HOST_IP: "${DOCKER_HOST_IP}"
+    ports:
+    - 22
+    depends_on:
+    - voltha
+    
+  #
+  # ofagent server instance
+  #
+  ofagent:
+    image: "${REGISTRY}${REPOSITORY}voltha-ofagent${TAG}"
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "10m"
+        max-file: "3"
+#      Use the fluentd driver to push logs to fluentd instead
+#      driver: "fluentd"
+#      options:
+#        fluentd-address: ${DOCKER_HOST_IP}:24224
+    command: [
+      "/ofagent/ofagent/main.py",
+      "-v",
+      "--consul=${DOCKER_HOST_IP}:8500",
+      "--controller=${DOCKER_HOST_IP}:6653",
+      "--grpc-endpoint=@voltha-grpc",
+      "--instance-id-is-container-name",
+      "--enable-tls",
+      "--key-file=/ofagent/pki/voltha.key",
+      "--cert-file=/ofagent/pki/voltha.crt",
+      "-v"
+    ]
+    depends_on:
+    - vconsul
+    - voltha
+    links:
+    - vconsul
+    volumes:
+    - "/var/run/docker.sock:/tmp/docker.sock"
+    restart: unless-stopped
+
+  #
+  # Netconf server instance(s)
+  #
+  netconf:
+    image: "${REGISTRY}${REPOSITORY}voltha-netconf${TAG}"
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "10m"
+        max-file: "3"
+#      Use the fluentd driver to push logs to fluentd instead
+#      driver: "fluentd"
+#      options:
+#        fluentd-address: ${DOCKER_HOST_IP}:24224
+    privileged: true
+    command: [
+      "/netconf/netconf/main.py",
+      "-v",
+      "--consul=${DOCKER_HOST_IP}:8500",
+      "--grpc-endpoint=@voltha-grpc",
+      "--instance-id-is-container-name",
+      "-v"
+    ]
+    ports:
+    - 1830
+    depends_on:
+    - vconsul
+    - voltha
+    links:
+    - vconsul
+    environment:
+      SERVICE_1830_NAME: "netconf-server"
+    volumes:
+    - "/var/run/docker.sock:/tmp/docker.sock"
+
+  #
+  # Dashboard daemon
+  #
+  dashd:
+    image: "${REGISTRY}${REPOSITORY}voltha-dashd${TAG}"
+    command: [
+      "/dashd/dashd/main.py",
+      "--kafka=@kafka",
+      "--consul=${DOCKER_HOST_IP}:8500",
+      "--grafana_url=http://admin:admin@${DOCKER_HOST_IP}:8883/api",
+      "--topic=voltha.kpis",
+      "--docker_host=${DOCKER_HOST_IP}"
+    ]
+    depends_on:
+    - vconsul
+    - kafka
+    - grafana
+    restart: unless-stopped
+
+  #
+  # Nginx service consolidation
+  #
+  nginx:
+    image: "${REGISTRY}${REPOSITORY}voltha-nginx${TAG}"
+    ports:
+    - 80
+    environment:
+      CONSUL_ADDR: "${DOCKER_HOST_IP}:8500"
+    command: [
+      "/nginx_config/start_service.sh"
+    ]
+    depends_on:
+    - vconsul
+    - grafana
+    - portainer
+    restart: unless-stopped
+
+  #
+  # Docker ui
+  #
+  portainer:
+    image: "${REGISTRY}${REPOSITORY}voltha-portainer${TAG}"
+    ports:
+    - 9000
+    environment:
+      CONSUL_ADDR: "${DOCKER_HOST_IP}:8500"
+    restart: unless-stopped
+    entrypoint: ["/portainer", "--logo", "/docker/images/logo_alt.png"]
+    volumes:
+    - "/var/run/docker.sock:/var/run/docker.sock"
+
+networks:
+  default:
+    driver: bridge
+  ponmgmt:
+    driver: bridge
+    driver_opts:
+      com.docker.network.bridge.name: "ponmgmt"
\ No newline at end of file
diff --git a/voltha/adapters/adtran_olt/flow/__init__.py b/voltha/adapters/adtran_olt/flow/__init__.py
index b0fb0b2..18d64b2 100644
--- a/voltha/adapters/adtran_olt/flow/__init__.py
+++ b/voltha/adapters/adtran_olt/flow/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2017-present Open Networking Foundation
+# Copyright 2017-present Adtran, Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/voltha/adapters/adtran_olt/flow/acl.py b/voltha/adapters/adtran_olt/flow/acl.py
index 67f8c08..3900436 100644
--- a/voltha/adapters/adtran_olt/flow/acl.py
+++ b/voltha/adapters/adtran_olt/flow/acl.py
@@ -350,7 +350,7 @@
 
             if rpc_reply.ok:
                 result_dict = xmltodict.parse(rpc_reply.data_xml)
-                entries = result_dict['data']['access-lists'] if 'access-lists' in result_dict['data'] else {}
+                entries = result_dict['data'].get('access-lists') or {}
 
                 if 'acl' in entries:
                     p = re.compile(regexpr)
diff --git a/voltha/adapters/adtran_olt/flow/evc.py b/voltha/adapters/adtran_olt/flow/evc.py
index 6aa8990..091c2f7 100644
--- a/voltha/adapters/adtran_olt/flow/evc.py
+++ b/voltha/adapters/adtran_olt/flow/evc.py
@@ -17,7 +17,7 @@
 from enum import IntEnum
 from twisted.internet import reactor, defer
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
-from voltha.core.flow_decomposer import *
+import structlog
 
 log = structlog.get_logger()
 
@@ -105,7 +105,7 @@
             self._valid = False
 
     def __str__(self):
-        return "EVC-{}: MEN: {}, S-Tag: {}".format(self._name, self._men_ports, self._s_tag)
+        return "EVC-{}: MEN: {}, S-Tag: {}".format(self.name, self._men_ports, self.s_tag)
 
     def _create_name(self):
         #
@@ -242,7 +242,7 @@
         self._cancel_deferred()
 
         self._deferred = reactor.callLater(delay, self._do_install) \
-            if self._valid else succeed('Not VALID')
+            if self.valid else succeed('Not VALID')
 
         return self._deferred
 
@@ -258,9 +258,9 @@
     @inlineCallbacks
     def _do_install(self):
         # Install the EVC if needed
-        log.debug('do-install', valid=self._valid, installed=self._installed)
+        log.debug('do-install', valid=self.valid, installed=self.installed)
 
-        if self._valid and not self._installed:
+        if self.valid and not self.installed:
             # TODO: Currently install EVC and then MAPs. Can do it all in a single edit-config operation
 
             xml = EVC._xml_header()
@@ -272,7 +272,7 @@
 
             if self._s_tag is not None:
                 xml += '<stag>{}</stag>'.format(self._s_tag)
-                xml += '<stag-tpid>{}</stag-tpid>'.format(self._stpid or DEFAULT_STPID)
+                xml += '<stag-tpid>{}</stag-tpid>'.format(self.stpid or DEFAULT_STPID)
             else:
                 xml += 'no-stag/'
 
@@ -297,7 +297,7 @@
 
         # Install any associated EVC Maps
 
-        if self._installed:
+        if self.installed:
             for evc_map in self.evc_maps:
                 try:
                     yield evc_map.install()
@@ -306,7 +306,7 @@
                     evc_map.status = 'Exception during EVC-MAP Install: {}'.format(e.message)
                     log.exception('evc-map-install-failed', e=e)
 
-        returnValue(self._installed and self._valid)
+        returnValue(self.installed and self.valid)
 
     def remove(self, remove_maps=True):
         """
@@ -394,13 +394,13 @@
         self._s_tag = self._flow.vlan_id
 
         if self._flow.inner_vid is not None:
-           self._switching_method = EVC.SwitchingMethod.DOUBLE_TAGGED
+            self._switching_method = EVC.SwitchingMethod.DOUBLE_TAGGED
 
         # For the Utility VLAN, multiple ingress ACLs (different GEMs) will need to
         # be trapped on this EVC. Since these are usually untagged, we have to force
         # the EVC to preserve CE VLAN tags.
 
-        if self._s_tag == self._flow.handler.utility_vlan:
+        if self.s_tag == self._flow.handler.utility_vlan:
             self._ce_vlan_preservation = True
 
         # Note: The following fields may get set when the first EVC-MAP
@@ -424,12 +424,11 @@
         """
         # 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>
-        """
+<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>""".strip().replace('\n', '')
         log.debug('query', xml=get_xml, regex=regex_)
 
         def request_failed(results, operation):
@@ -444,25 +443,19 @@
 
             if rpc_reply.ok:
                 result_dict = xmltodict.parse(rpc_reply.data_xml)
-                entries = result_dict['data']['evcs'] if 'evcs' in result_dict['data'] else {}
+                entries = result_dict['data'].get('evcs') or {}
 
-                if 'evc' in entries:
+                evcs = entries.get('evc') or None
+                if evcs:
                     p = re.compile(regexpr)
+                    if isinstance(evcs, dict):
+                        evcs = [evcs]
+                    names = {entry.get('name') for entry in evcs if p.match(entry.get('name', ''))}
 
-                    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'].items():
-                            if isinstance(item, tuple) and item[0] == 'name':
-                                names.add(item[1])
-                                break
-
-                    if len(names) > 0:
-                        del_xml = '<evcs xmlns="http://www.adtran.com/ns/yang/adtran-evcs"' + \
-                                 ' xc:operation = "delete">'
-                        for name in names:
+                    if names:
+                        del_xml = ('<evcs xmlns="http://www.adtran.com/ns/yang/adtran-evcs" '
+                                   'xc:operation="delete">')
+                        for name in sorted(names):
                             del_xml += '<evc>'
                             del_xml += '<name>{}</name>'.format(name)
                             del_xml += '</evc>'
diff --git a/voltha/adapters/adtran_olt/flow/evc_map.py b/voltha/adapters/adtran_olt/flow/evc_map.py
index f9d81ef..d1d9385 100644
--- a/voltha/adapters/adtran_olt/flow/evc_map.py
+++ b/voltha/adapters/adtran_olt/flow/evc_map.py
@@ -81,7 +81,7 @@
         self._onu_id = None               # Remains None if associated with a multicast flow
         self._installed = False
         self._needs_update = False
-        self._status_message = None
+        self.status = None
         self._deferred = None
         self._name = None
         self._enabled = True
@@ -104,7 +104,7 @@
 
         from common.tech_profile.tech_profile import DEFAULT_TECH_PROFILE_TABLE_ID
         self._tech_profile_id = DEFAULT_TECH_PROFILE_TABLE_ID
-        self._gem_ids_and_vid = None      # { key -> onu-id, value -> tuple(sorted GEM Port IDs, onu_vid) }
+        self._gem_ids_and_vid = {}      # { key -> onu-id, value -> tuple(sorted GEM Port IDs, onu_vid) }
         self._upstream_bandwidth = None
         self._shaper_name = None
 
@@ -169,14 +169,6 @@
         return self._name
 
     @property
-    def status(self):
-        return self._status_message
-
-    @status.setter
-    def status(self, value):
-        self._status_message = value
-
-    @property
     def evc(self):
         return self._evc
 
@@ -884,7 +876,7 @@
         if evc:
             self._evc_connection = EVCMap.EvcConnection.EVC
         else:
-            self._status_message = 'Can only create EVC-MAP if EVC supplied'
+            self.status = 'Can only create EVC-MAP if EVC supplied'
             return False
 
         is_pon = flow.handler.is_pon_port(flow.in_port)
@@ -898,7 +890,7 @@
             self._uni_port = flow.handler.get_port_name(flow.in_port)
             evc.ce_vlan_preservation = evc.ce_vlan_preservation or False
         else:
-            self._status_message = 'EVC-MAPS without UNI or PON ports are not supported'
+            self.status = 'EVC-MAPS without UNI or PON ports are not supported'
             return False    # UNI Ports handled in the EVC Maps
 
         # ACL logic
@@ -973,7 +965,7 @@
 
             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 {}
+                entries = result_dict['data'].get('evc-maps') or {}
 
                 if 'evc-map' in entries:
                     p = re.compile(regexpr)
diff --git a/voltha/adapters/adtran_olt/flow/flow_entry.py b/voltha/adapters/adtran_olt/flow/flow_entry.py
index a28367a..6fa658e 100644
--- a/voltha/adapters/adtran_olt/flow/flow_entry.py
+++ b/voltha/adapters/adtran_olt/flow/flow_entry.py
@@ -92,7 +92,7 @@
         self._bandwidth = None
 
         # A value used to locate possible related flow entries
-        self.signature = None
+        self._signature = None
         self.downstream_signature = None  # Valid for upstream EVC-MAP Flows
 
         # Selection properties
@@ -112,7 +112,7 @@
         self.push_vlan_tpid = None
         self.push_vlan_id = None
 
-        self._name = self.create_flow_name()
+        self._name = None
 
     def __str__(self):
         return 'flow_entry: {}, in: {}, out: {}, vid: {}, inner:{}, eth: {}, IP: {}'.format(
@@ -124,11 +124,10 @@
 
     @property
     def name(self):
+        if self._name is None:
+            self._name = 'flow-{}-{}'.format(self.device_id, self.flow_id)
         return self._name    # TODO: Is a name really needed in production?
 
-    def create_flow_name(self):
-        return 'flow-{}-{}'.format(self.device_id, self.flow_id)
-
     @property
     def flow(self):
         return self._flow
@@ -194,8 +193,8 @@
             ######################################################################
             # Initialize flow_entry database (dicts) if needed and determine if
             # the flows have already been handled.
-            downstream_sig_table = handler.downstream_flows
-            upstream_flow_table = handler.upstream_flows
+            downstream_sig_table = flow_entry.handler.downstream_flows
+            upstream_flow_table = flow_entry.handler.upstream_flows
 
             log.debug('flow-entry-decoded', flow=flow_entry, signature=flow_entry.signature,
                       downstream_signature=flow_entry.downstream_signature)
@@ -278,544 +277,579 @@
             log.exception('flow-entry-processing', e=e)
             return None, None
 
-    @staticmethod
-    def _create_evc_and_maps(evc, downstream_flow, upstream_flows):
-        """
-        Give a set of flows, find (or create) the EVC and any needed EVC-MAPs
+@staticmethod
+def _create_evc_and_maps(evc, downstream_flow, upstream_flows):
+    """
+    Give a set of flows, find (or create) the EVC and any needed EVC-MAPs
 
-        :param evc: (EVC) Existing EVC for downstream flow. May be null if not created
-        :param downstream_flow: (FlowEntry) NNI -> UNI flow (provides much of the EVC values)
-        :param upstream_flows: (list of FlowEntry) UNI -> NNI flows (provides much of the EVC-MAP values)
+    :param evc: (EVC) Existing EVC for downstream flow. May be null if not created
+    :param downstream_flow: (FlowEntry) NNI -> UNI flow (provides much of the EVC values)
+    :param upstream_flows: (list of FlowEntry) UNI -> NNI flows (provides much of the EVC-MAP values)
 
-        :return: EVC object
-        """
-        log.debug('flow-evc-and-maps', downstream_flow=downstream_flow,
-                  upstream_flows=upstream_flows)
+    :return: EVC object
+    """
+    log.debug('flow-evc-and-maps', downstream_flow=downstream_flow,
+              upstream_flows=upstream_flows)
 
-        if (evc is None and downstream_flow is None) or upstream_flows is None:
-            return None
+    if (evc is None and downstream_flow is None) or upstream_flows is None:
+        return None
 
-        # Get any existing EVC if a flow is already created
-        if downstream_flow.evc is None:
-            if evc is not None:
-                downstream_flow.evc = evc
+    # Get any existing EVC if a flow is already created
+    if downstream_flow.evc is None:
+        if evc is not None:
+            downstream_flow.evc = evc
 
-            elif downstream_flow.is_multicast_flow:
-                from mcast import MCastEVC
-                downstream_flow.evc = MCastEVC.create(downstream_flow)
+        elif downstream_flow.is_multicast_flow:
+            from mcast import MCastEVC
+            downstream_flow.evc = MCastEVC.create(downstream_flow)
 
-            elif downstream_flow.is_acl_flow:
-                downstream_flow.evc = downstream_flow.get_utility_evc()
-            else:
-                downstream_flow.evc = EVC(downstream_flow)
+        elif downstream_flow.is_acl_flow:
+            downstream_flow.evc = downstream_flow.get_utility_evc()
+        else:
+            downstream_flow.evc = EVC(downstream_flow)
 
-        if not downstream_flow.evc.valid:
-            log.debug('flow-evc-and-maps-downstream-invalid',
-                      downstream_flow=downstream_flow,
-                      upstream_flows=upstream_flows)
-            return None
-
-        # Create EVC-MAPs. Note upstream_flows is empty list for multicast
-        # For Packet In/Out support. The upstream flows for will have matching
-        # signatures. So the first one to get created should create the EVC and
-        # if it needs and ACL, do so then. The second one should just reference
-        # the first map.
-        #
-        #    If the second has and ACL, then it should add it to the map.
-        #    TODO: What to do if the second (or third, ...) is the data one.
-        #          What should it do then?
-        sig_map_map = {f.signature: f.evc_map for f in upstream_flows
-                       if f.evc_map is not None}
-
-        for flow in upstream_flows:
-            if flow.evc_map is None:
-                if flow.signature in sig_map_map:
-                    # Found an explicitly matching existing EVC-MAP. Add flow to this EVC-MAP
-                    flow.evc_map = sig_map_map[flow.signature].add_flow(flow, downstream_flow.evc)
-                else:
-                    # May need to create a MAP or search for an existing ACL/user EVC-Map
-                    # upstream_flow_table = _existing_upstream_flow_entries[flow.device_id]
-                    upstream_flow_table = flow.handler.upstream_flows
-                    existing_flow = EVCMap.find_matching_ingress_flow(flow, upstream_flow_table)
-
-                    if existing_flow is None:
-                        flow.evc_map = EVCMap.create_ingress_map(flow, downstream_flow.evc)
-                    else:
-                        flow.evc_map = existing_flow.add_flow(flow, downstream_flow.evc)
-
-        all_maps_valid = all(flow.evc_map.valid for flow in upstream_flows) \
-            or downstream_flow.is_multicast_flow
-
-        log.debug('flow-evc-and-maps-downstream',
+    if not downstream_flow.evc.valid:
+        log.debug('flow-evc-and-maps-downstream-invalid',
                   downstream_flow=downstream_flow,
-                  upstream_flows=upstream_flows, all_valid=all_maps_valid)
+                  upstream_flows=upstream_flows)
+        return None
 
-        return downstream_flow.evc if all_maps_valid else None
+    # Create EVC-MAPs. Note upstream_flows is empty list for multicast
+    # For Packet In/Out support. The upstream flows for will have matching
+    # signatures. So the first one to get created should create the EVC and
+    # if it needs and ACL, do so then. The second one should just reference
+    # the first map.
+    #
+    #    If the second has and ACL, then it should add it to the map.
+    #    TODO: What to do if the second (or third, ...) is the data one.
+    #          What should it do then?
+    sig_map_map = {f.signature: f.evc_map for f in upstream_flows
+                   if f.evc_map is not None}
 
-    def get_utility_evc(self, use_default_vlan_id=False):
-        assert self.is_acl_flow, 'Utility evcs are for acl flows only'
-        return UtilityEVC.create(self, use_default_vlan_id)
+    for flow in upstream_flows:
+        if flow.evc_map is None:
+            if flow.signature in sig_map_map:
+                # Found an explicitly matching existing EVC-MAP. Add flow to this EVC-MAP
+                flow.evc_map = sig_map_map[flow.signature].add_flow(flow, downstream_flow.evc)
+            else:
+                # May need to create a MAP or search for an existing ACL/user EVC-Map
+                # upstream_flow_table = _existing_upstream_flow_entries[flow.device_id]
+                upstream_flow_table = flow.handler.upstream_flows
+                existing_flow = EVCMap.find_matching_ingress_flow(flow, upstream_flow_table)
 
-    @property
-    def _needs_acl_support(self):
-        if self.ipv4_dst is not None:  # In case MCAST downstream has ACL on it
+                if existing_flow is None:
+                    flow.evc_map = EVCMap.create_ingress_map(flow, downstream_flow.evc)
+                else:
+                    flow.evc_map = existing_flow.add_flow(flow, downstream_flow.evc)
+
+    all_maps_valid = all(flow.evc_map.valid for flow in upstream_flows) \
+        or downstream_flow.is_multicast_flow
+
+    log.debug('flow-evc-and-maps-downstream',
+              downstream_flow=downstream_flow,
+              upstream_flows=upstream_flows, all_valid=all_maps_valid)
+
+    return downstream_flow.evc if all_maps_valid else None
+
+def get_utility_evc(self, use_default_vlan_id=False):
+    assert self.is_acl_flow, 'Utility evcs are for acl flows only'
+    return UtilityEVC.create(self, use_default_vlan_id)
+
+@property
+def _needs_acl_support(self):
+    if self.ipv4_dst is not None:  # In case MCAST downstream has ACL on it
+        return False
+
+    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
+
+@property
+def signature(self):
+    if self._signature is None:
+        # These are not exact, just ones that may be put together to make an EVC. The
+        # basic rules are:
+        #
+        # 1 - Port numbers in increasing order
+        ports = sorted(filter(None, [self.in_port, self.output]))
+        assert len(ports) == 2, 'Invalid port count: {}'.format(len(ports))
+
+        # 3 - The outer VID
+        # 4 - The inner VID.  Wildcard if downstream
+        if self.push_vlan_id is None:
+            outer = self.vlan_id
+            inner = self.inner_vid
+        else:
+            outer = self.push_vlan_id
+            inner = self.vlan_id
+
+        downstream_sig = '.'.join(map(str, (
+            ports[0],
+            ports[1] if self.handler.is_nni_port(ports[1]) else '*',
+            outer,
+            '*'
+        )))
+
+        if self._flow_direction in FlowEntry.downstream_flow_types:
+            self._signature = downstream_sig
+        elif self._flow_direction in FlowEntry.upstream_flow_types:
+            self._signature = '.'.join(map(str, (ports[0], ports[1], outer, inner)))
+            self.downstream_signature = downstream_sig
+        else:
+            log.error('unsupported-flow')
+    return self._signature
+
+def _decode(self, flow):
+    """
+    Examine flow rules and extract appropriate settings
+    """
+    log.debug('start-decode')
+    status = self._decode_traffic_selector(flow) and self._decode_traffic_treatment(flow)
+
+    # Determine direction of the flow and apply appropriate modifications
+    # to the decoded flows
+    if status:
+        if not self._decode_flow_direction():
             return False
 
-        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
+        if self._flow_direction in FlowEntry.downstream_flow_types:
+            status = self._apply_downstream_mods()
 
-    def _decode(self, flow):
-        """
-        Examine flow rules and extract appropriate settings
-        """
-        log.debug('start-decode')
-        status = self._decode_traffic_selector(flow) and self._decode_traffic_treatment(flow)
-
-        # Determine direction of the flow and apply appropriate modifications
-        # to the decoded flows
-        if status:
-            if not self._decode_flow_direction():
-                return False
-
-            if self._flow_direction in FlowEntry.downstream_flow_types:
-                status = self._apply_downstream_mods()
-
-            elif self._flow_direction in FlowEntry.upstream_flow_types:
-                status = self._apply_upstream_mods()
-
-            else:
-                # TODO: Need to code this - Perhaps this is an NNI_PON for Multicast support?
-                log.error('unsupported-flow-direction')
-                status = False
-
-            log.debug('flow-evc-decode', direction=self._flow_direction, is_acl=self._is_acl_flow,
-                      inner_vid=self.inner_vid, vlan_id=self.vlan_id, pop_vlan=self.pop_vlan,
-                      push_vid=self.push_vlan_id, status=status)
-
-        # Create a signature that will help locate related flow entries on a device.
-        if status:
-            # These are not exact, just ones that may be put together to make an EVC. The
-            # basic rules are:
-            #
-            # 1 - Port numbers in increasing order
-            ports = [self.in_port, self.output]
-            ports.sort()
-            assert len(ports) == 2, 'Invalid port count: {}'.format(len(ports))
-
-            # 3 - The outer VID
-            # 4 - The inner VID.  Wildcard if downstream
-            if self.push_vlan_id is None:
-                outer = self.vlan_id
-                inner = self.inner_vid
-            else:
-                outer = self.push_vlan_id
-                inner = self.vlan_id
-
-            upstream_sig = '{}'.format(ports[0])
-            downstream_sig = '{}'.format(ports[0])
-            upstream_sig += '.{}'.format(ports[1])
-            downstream_sig += '.{}'.format(ports[1] if self.handler.is_nni_port(ports[1]) else '*')
-
-            upstream_sig += '.{}.{}'.format(outer, inner)
-            downstream_sig += '.{}.*'.format(outer)
-
-            if self._flow_direction in FlowEntry.downstream_flow_types:
-                self.signature = downstream_sig
-
-            elif self._flow_direction in FlowEntry.upstream_flow_types:
-                self.signature = upstream_sig
-                self.downstream_signature = downstream_sig
-
-            else:
-                log.error('unsupported-flow')
-                status = False
-
-            log.debug('flow-evc-decode', upstream_sig=self.signature, downstream_sig=self.downstream_signature)
-        return status
-
-    def _decode_traffic_selector(self, flow):
-        """
-        Extract EVC related traffic selection settings
-        """
-        self.in_port = fd.get_in_port(flow)
-
-        if self.in_port > OFPP_MAX:
-            log.warn('logical-input-ports-not-supported', in_port=self.in_port)
-            return False
-
-        for field in fd.get_ofb_fields(flow):
-            if field.type == IN_PORT:
-                if self._handler.is_nni_port(self.in_port) or self._handler.is_uni_port(self.in_port):
-                    self._logical_port = self.in_port
-
-            elif field.type == VLAN_VID:
-                if field.vlan_vid >= OFPVID_PRESENT + 4095:
-                    self.vlan_id = None             # pre-ONOS v1.13.5 or old EAPOL Rule
-                else:
-                    self.vlan_id = field.vlan_vid & 0xfff
-
-                log.debug('*** field.type == VLAN_VID', value=field.vlan_vid, vlan_id=self.vlan_id)
-
-            elif field.type == VLAN_PCP:
-                log.debug('*** field.type == VLAN_PCP', value=field.vlan_pcp)
-                self.pcp = field.vlan_pcp
-
-            elif field.type == ETH_TYPE:
-                log.debug('*** field.type == ETH_TYPE', value=field.eth_type)
-                self.eth_type = field.eth_type
-
-            elif field.type == IP_PROTO:
-                log.debug('*** 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', protocol=self.ip_protocol)
-                    return False
-
-            elif field.type == IPV4_DST:
-                log.debug('*** field.type == IPV4_DST', value=field.ipv4_dst)
-                self.ipv4_dst = field.ipv4_dst
-
-            elif field.type == UDP_DST:
-                log.debug('*** field.type == UDP_DST', value=field.udp_dst)
-                self.udp_dst = field.udp_dst
-
-            elif field.type == UDP_SRC:
-                log.debug('*** field.type == UDP_SRC', value=field.udp_src)
-                self.udp_src = field.udp_src
-
-            elif field.type == METADATA:
-                if self._handler.is_nni_port(self.in_port):
-                    # Downstream flow
-                    log.debug('*** field.type == METADATA', value=field.table_metadata)
-
-                    if field.table_metadata > 4095:
-                        # ONOS v1.13.5 or later. c-vid in upper 32-bits
-                        vid = field.table_metadata & 0x0FFF
-                        if vid > 0:
-                            self.inner_vid = vid        # CTag is never '0'
-
-                    elif field.table_metadata > 0:
-                        # Pre-ONOS v1.13.5 (vid without the 4096 offset)
-                        self.inner_vid = field.table_metadata
-
-                else:
-                    # Upstream flow
-                    pass   # Not used upstream at this time
-
-                log.debug('*** field.type == METADATA', value=field.table_metadata,
-                          inner_vid=self.inner_vid)
-            else:
-                log.warn('unsupported-selection-field', type=field.type)
-                self._status_message = 'Unsupported field.type={}'.format(field.type)
-                return False
-
-        return True
-
-    def _decode_traffic_treatment(self, flow):
-        # Loop through traffic treatment
-        for act in fd.get_actions(flow):
-            if act.type == fd.OUTPUT:
-                self.output = act.output.port
-
-            elif act.type == POP_VLAN:
-                log.debug('*** action.type == POP_VLAN')
-                self.pop_vlan = True
-
-            elif act.type == PUSH_VLAN:
-                log.debug('*** action.type == PUSH_VLAN', value=act.push)
-                tpid = act.push.ethertype
-                self.push_vlan_tpid = tpid
-
-            elif act.type == SET_FIELD:
-                log.debug('*** action.type == SET_FIELD', value=act.set_field.field)
-                assert (act.set_field.field.oxm_class == OFPXMC_OPENFLOW_BASIC)
-                field = act.set_field.field.ofb_field
-
-                if field.type == VLAN_VID:
-                    self.push_vlan_id = field.vlan_vid & 0xfff
-                else:
-                    log.debug('unsupported-set-field')
-            else:
-                log.warn('unsupported-action', action=act)
-                self._status_message = 'Unsupported action.type={}'.format(act.type)
-                return False
-
-        return True
-
-    def _decode_flow_direction(self):
-        # Determine direction of the flow
-        def port_type(port_number):
-            if port_number in self._handler.northbound_ports:
-                return FlowEntry.PortType.NNI
-
-            elif port_number in self._handler.southbound_ports:
-                return FlowEntry.PortType.PON
-
-            elif port_number <= OFPP_MAX:
-                return FlowEntry.PortType.UNI
-
-            elif port_number in {OFPP_CONTROLLER, 0xFFFFFFFD}:      # OFPP_CONTROLLER is wrong in proto-file
-                return FlowEntry.PortType.CONTROLLER
-
-            return FlowEntry.PortType.OTHER
-
-        flow_dir_map = {
-            (FlowEntry.PortType.UNI, FlowEntry.PortType.NNI):        FlowEntry.FlowDirection.UPSTREAM,
-            (FlowEntry.PortType.NNI, FlowEntry.PortType.UNI):        FlowEntry.FlowDirection.DOWNSTREAM,
-            (FlowEntry.PortType.UNI, FlowEntry.PortType.CONTROLLER): FlowEntry.FlowDirection.CONTROLLER_UNI,
-            (FlowEntry.PortType.NNI, FlowEntry.PortType.PON):        FlowEntry.FlowDirection.NNI_PON,
-            # The following are not yet supported
-            # (FlowEntry.PortType.NNI, FlowEntry.PortType.CONTROLLER): FlowEntry.FlowDirection.CONTROLLER_NNI,
-            # (FlowEntry.PortType.PON, FlowEntry.PortType.CONTROLLER): FlowEntry.FlowDirection.CONTROLLER_PON,
-            # (FlowEntry.PortType.NNI, FlowEntry.PortType.NNI):        FlowEntry.FlowDirection.NNI_NNI,
-            # (FlowEntry.PortType.UNI, FlowEntry.PortType.UNI):        FlowEntry.FlowDirection.UNI_UNI,
-        }
-        self._flow_direction = flow_dir_map.get((port_type(self.in_port), port_type(self.output)),
-                                                FlowEntry.FlowDirection.OTHER)
-        return self._flow_direction != FlowEntry.FlowDirection.OTHER
-
-    def _apply_downstream_mods(self):
-        # This is a downstream flow.  It could be any one of the following:
-        #
-        #   Legacy control VLAN:
-        #       This is the old VLAN 4000 that was used to attach EAPOL and other
-        #       controller flows to. Eventually these will change to CONTROLLER_UNI
-        #       flows.  For these, use the 'utility' VLAN instead so 4000 if available
-        #       for other uses (AT&T uses it for downstream multicast video).
-        #
-        #   Multicast VLAN:
-        #       This is downstream multicast data.
-        #       TODO: Test this to see if this needs to be in a separate NNI_PON mod-method
-        #
-        #   User Data flow:
-        #       This is for user data.  Eventually we may need to support ACLs?
-        #
-        # May be for to controller flow downstream (no ethType)
-        if self.vlan_id == FlowEntry.LEGACY_CONTROL_VLAN and self.eth_type is None and self.pcp == 0:
-            return False    # Do not install this flow.  Utility VLAN is in charge
-
-        elif self.flow_direction == FlowEntry.FlowDirection.NNI_PON and \
-                self.vlan_id == self.handler.utility_vlan:
-            # Utility VLAN downstream flow/EVC
-            self._is_acl_flow = True
-
-        elif self.vlan_id in self._handler.multicast_vlans:
-            #  multicast (ethType = IP)                         # TODO: May need to be an NNI_PON flow
-            self._is_multicast = True
-            self._is_acl_flow = True
+        elif self._flow_direction in FlowEntry.upstream_flow_types:
+            status = self._apply_upstream_mods()
 
         else:
-            # Currently do not support ACLs on user data flows downstream
-            assert not self._needs_acl_support    # User data, no special modifications needed at this time
+            # TODO: Need to code this - Perhaps this is an NNI_PON for Multicast support?
+            log.error('unsupported-flow-direction')
+            status = False
+
+        log.debug('flow-evc-decode', direction=self._flow_direction, is_acl=self._is_acl_flow,
+                  inner_vid=self.inner_vid, vlan_id=self.vlan_id, pop_vlan=self.pop_vlan,
+                  push_vid=self.push_vlan_id, status=status)
+
+    # Create a signature that will help locate related flow entries on a device.
+    if status:
+        # These are not exact, just ones that may be put together to make an EVC. The
+        # basic rules are:
+        #
+        # 1 - Port numbers in increasing order
+        ports = [self.in_port, self.output]
+        ports.sort()
+        assert len(ports) == 2, 'Invalid port count: {}'.format(len(ports))
+
+        # 3 - The outer VID
+        # 4 - The inner VID.  Wildcard if downstream
+        if self.push_vlan_id is None:
+            outer = self.vlan_id
+            inner = self.inner_vid
+        else:
+            outer = self.push_vlan_id
+            inner = self.vlan_id
+
+        upstream_sig = '{}'.format(ports[0])
+        downstream_sig = '{}'.format(ports[0])
+        upstream_sig += '.{}'.format(ports[1])
+        downstream_sig += '.{}'.format(ports[1] if self.handler.is_nni_port(ports[1]) else '*')
+
+        upstream_sig += '.{}.{}'.format(outer, inner)
+        downstream_sig += '.{}.*'.format(outer)
+
+        if self._flow_direction in FlowEntry.downstream_flow_types:
+            self.signature = downstream_sig
+
+        elif self._flow_direction in FlowEntry.upstream_flow_types:
+            self.signature = upstream_sig
+            self.downstream_signature = downstream_sig
+
+        else:
+            log.error('unsupported-flow')
+            status = False
+
+        log.debug('flow-evc-decode', upstream_sig=self.signature, downstream_sig=self.downstream_signature)
+    return status
+
+def _decode_traffic_selector(self, flow):
+    """
+    Extract EVC related traffic selection settings
+    """
+    self.in_port = fd.get_in_port(flow)
+
+    if self.in_port > OFPP_MAX:
+        log.warn('logical-input-ports-not-supported', in_port=self.in_port)
+        return False
+
+    for field in fd.get_ofb_fields(flow):
+        if field.type == IN_PORT:
+            if self._handler.is_nni_port(self.in_port) or self._handler.is_uni_port(self.in_port):
+                self._logical_port = self.in_port
+
+        elif field.type == VLAN_VID:
+            if field.vlan_vid >= OFPVID_PRESENT + 4095:
+                self.vlan_id = None             # pre-ONOS v1.13.5 or old EAPOL Rule
+            else:
+                self.vlan_id = field.vlan_vid & 0xfff
+
+            log.debug('*** field.type == VLAN_VID', value=field.vlan_vid, vlan_id=self.vlan_id)
+
+        elif field.type == VLAN_PCP:
+            log.debug('*** field.type == VLAN_PCP', value=field.vlan_pcp)
+            self.pcp = field.vlan_pcp
+
+        elif field.type == ETH_TYPE:
+            log.debug('*** field.type == ETH_TYPE', value=field.eth_type)
+            self.eth_type = field.eth_type
+
+        elif field.type == IP_PROTO:
+            log.debug('*** 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', protocol=self.ip_protocol)
+                return False
+
+        elif field.type == IPV4_DST:
+            log.debug('*** field.type == IPV4_DST', value=field.ipv4_dst)
+            self.ipv4_dst = field.ipv4_dst
+
+        elif field.type == UDP_DST:
+            log.debug('*** field.type == UDP_DST', value=field.udp_dst)
+            self.udp_dst = field.udp_dst
+
+        elif field.type == UDP_SRC:
+            log.debug('*** field.type == UDP_SRC', value=field.udp_src)
+            self.udp_src = field.udp_src
+
+        elif field.type == METADATA:
+            if self._handler.is_nni_port(self.in_port):
+                # Downstream flow
+                log.debug('*** field.type == METADATA', value=field.table_metadata)
+
+                if field.table_metadata > 4095:
+                    # ONOS v1.13.5 or later. c-vid in upper 32-bits
+                    vid = field.table_metadata & 0x0FFF
+                    if vid > 0:
+                        self.inner_vid = vid        # CTag is never '0'
+            
+                elif field.table_metadata > 0:
+                    # Pre-ONOS v1.13.5 (vid without the 4096 offset)
+                    self.inner_vid = field.table_metadata
+            
+            else:
+                # Upstream flow
+                pass   # Not used upstream at this time
+
+            log.debug('*** field.type == METADATA', value=field.table_metadata,
+                      inner_vid=self.inner_vid)
+        else:
+            log.warn('unsupported-selection-field', type=field.type)
+            self._status_message = 'Unsupported field.type={}'.format(field.type)
+            return False
+
+    return True
+
+def _decode_traffic_treatment(self, flow):
+    # Loop through traffic treatment
+    for act in fd.get_actions(flow):
+        if act.type == fd.OUTPUT:
+            self.output = act.output.port
+
+        elif act.type == POP_VLAN:
+            log.debug('*** action.type == POP_VLAN')
+            self.pop_vlan = True
+
+        elif act.type == PUSH_VLAN:
+            log.debug('*** action.type == PUSH_VLAN', value=act.push)
+            tpid = act.push.ethertype
+            self.push_vlan_tpid = tpid
+
+        elif act.type == SET_FIELD:
+            log.debug('*** action.type == SET_FIELD', value=act.set_field.field)
+            assert (act.set_field.field.oxm_class == OFPXMC_OPENFLOW_BASIC)
+            field = act.set_field.field.ofb_field
+
+            if field.type == VLAN_VID:
+                self.push_vlan_id = field.vlan_vid & 0xfff
+            else:
+                log.debug('unsupported-set-field')
+        else:
+            log.warn('unsupported-action', action=act)
+            self._status_message = 'Unsupported action.type={}'.format(act.type)
+            return False
+
+    return True
+
+def _decode_flow_direction(self):
+    # Determine direction of the flow
+    def port_type(port_number):
+        if port_number in self._handler.northbound_ports:
+            return FlowEntry.PortType.NNI
+
+        elif port_number in self._handler.southbound_ports:
+            return FlowEntry.PortType.PON
+
+        elif port_number <= OFPP_MAX:
+            return FlowEntry.PortType.UNI
+
+        elif port_number in {OFPP_CONTROLLER, 0xFFFFFFFD}:      # OFPP_CONTROLLER is wrong in proto-file
+            return FlowEntry.PortType.CONTROLLER
+
+        return FlowEntry.PortType.OTHER
+
+    flow_dir_map = {
+        (FlowEntry.PortType.UNI, FlowEntry.PortType.NNI):        FlowEntry.FlowDirection.UPSTREAM,
+        (FlowEntry.PortType.NNI, FlowEntry.PortType.UNI):        FlowEntry.FlowDirection.DOWNSTREAM,
+        (FlowEntry.PortType.UNI, FlowEntry.PortType.CONTROLLER): FlowEntry.FlowDirection.CONTROLLER_UNI,
+        (FlowEntry.PortType.NNI, FlowEntry.PortType.PON):        FlowEntry.FlowDirection.NNI_PON,
+        # The following are not yet supported
+        # (FlowEntry.PortType.NNI, FlowEntry.PortType.CONTROLLER): FlowEntry.FlowDirection.CONTROLLER_NNI,
+        # (FlowEntry.PortType.PON, FlowEntry.PortType.CONTROLLER): FlowEntry.FlowDirection.CONTROLLER_PON,
+        # (FlowEntry.PortType.NNI, FlowEntry.PortType.NNI):        FlowEntry.FlowDirection.NNI_NNI,
+        # (FlowEntry.PortType.UNI, FlowEntry.PortType.UNI):        FlowEntry.FlowDirection.UNI_UNI,
+    }
+    self._flow_direction = flow_dir_map.get((port_type(self.in_port), port_type(self.output)),
+                                            FlowEntry.FlowDirection.OTHER)
+    return self._flow_direction != FlowEntry.FlowDirection.OTHER
+
+def _apply_downstream_mods(self):
+    # This is a downstream flow.  It could be any one of the following:
+    #
+    #   Legacy control VLAN:
+    #       This is the old VLAN 4000 that was used to attach EAPOL and other
+    #       controller flows to. Eventually these will change to CONTROLLER_UNI
+    #       flows.  For these, use the 'utility' VLAN instead so 4000 if available
+    #       for other uses (AT&T uses it for downstream multicast video).
+    #
+    #   Multicast VLAN:
+    #       This is downstream multicast data.
+    #       TODO: Test this to see if this needs to be in a separate NNI_PON mod-method
+    #
+    #   User Data flow:
+    #       This is for user data.  Eventually we may need to support ACLs?
+    #
+    # May be for to controller flow downstream (no ethType)
+    if self.vlan_id == FlowEntry.LEGACY_CONTROL_VLAN and self.eth_type is None and self.pcp == 0:
+        return False    # Do not install this flow.  Utility VLAN is in charge
+
+    elif self.flow_direction == FlowEntry.FlowDirection.NNI_PON and \
+            self.vlan_id == self.handler.utility_vlan:
+        # Utility VLAN downstream flow/EVC
+        self._is_acl_flow = True
+
+    elif self.vlan_id in self.handler.multicast_vlans:
+        #  multicast (ethType = IP)                         # TODO: May need to be an NNI_PON flow
+        self._is_multicast = True
+        self._is_acl_flow = True
+
+    else:
+        # Currently do not support ACLs on user data flows downstream
+        assert not self._needs_acl_support    # User data, no special modifications needed at this time
+
+    return True
+
+def _apply_upstream_mods(self):
+    #
+    # This is an upstream flow.  It could be any of the following
+    #
+    #   ACL/Packet capture:
+    #       This is either a legacy (FlowDirection.UPSTREAM) or a new one
+    #       that specifies an output port of controller (FlowDirection.CONTROLLER_UNI).
+    #       Either way, these need to be placed on the Utility VLAN if the ONU attached
+    #       does not have a user-data flow (C-Tag).  If there is a C-Tag available,
+    #       then place it on that VLAN.
+    #
+    #       Once a user-data flow is established, move any of the ONUs ACL flows
+    #       over to that VLAN (this is handled elsewhere).
+    #
+    #   User Data flows:
+    #       No special modifications are needed
+    #
+    try:
+        # Do not handle PON level ACLs in this method
+        assert(self._flow_direction != FlowEntry.FlowDirection.CONTROLLER_PON)
+
+        # Is this a legacy (VLAN 4000) upstream to-controller flow
+        if self._needs_acl_support and FlowEntry.LEGACY_CONTROL_VLAN == self.push_vlan_id:
+            self._flow_direction = FlowEntry.FlowDirection.CONTROLLER_UNI
+            self._is_acl_flow = True
+            self.push_vlan_id = self.handler.utility_vlan
 
         return True
 
-    def _apply_upstream_mods(self):
-        #
-        # This is an upstream flow.  It could be any of the following
-        #
-        #   ACL/Packet capture:
-        #       This is either a legacy (FlowDirection.UPSTREAM) or a new one
-        #       that specifies an output port of controller (FlowDirection.CONTROLLER_UNI).
-        #       Either way, these need to be placed on the Utility VLAN if the ONU attached
-        #       does not have a user-data flow (C-Tag).  If there is a C-Tag available,
-        #       then place it on that VLAN.
-        #
-        #       Once a user-data flow is established, move any of the ONUs ACL flows
-        #       over to that VLAN (this is handled elsewhere).
-        #
-        #   User Data flows:
-        #       No special modifications are needed
-        #
-        try:
-            # Do not handle PON level ACLs in this method
-            assert(self._flow_direction != FlowEntry.FlowDirection.CONTROLLER_PON)
+    except Exception as e:
+        # TODO: Need to support flow retry if the ONU is not yet activated   !!!!
+        log.exception('tag-fixup', e=e)
+        return False
 
-            # Is this a legacy (VLAN 4000) upstream to-controller flow
-            if self._needs_acl_support and FlowEntry.LEGACY_CONTROL_VLAN == self.push_vlan_id:
-                self._flow_direction = FlowEntry.FlowDirection.CONTROLLER_UNI
-                self._is_acl_flow = True
-                self.push_vlan_id = self.handler.utility_vlan
+@staticmethod
+def drop_missing_flows(handler, valid_flow_ids):
+    dl = []
+    try:
+        flow_table = handler.upstream_flows
+        flows_to_drop = [flow for flow_id, flow in flow_table.items()
+                         if flow_id not in valid_flow_ids]
+        dl.extend([flow.remove() for flow in flows_to_drop])
 
-            return True
-
-        except Exception as e:
-            # TODO: Need to support flow retry if the ONU is not yet activated   !!!!
-            log.exception('tag-fixup', e=e)
-            return False
-
-    @staticmethod
-    def drop_missing_flows(handler, valid_flow_ids):
-        dl = []
-        try:
-            flow_table = handler.upstream_flows
-            flows_to_drop = [flow for flow_id, flow in flow_table.items()
-                             if flow_id not in valid_flow_ids]
+        for sig_table in handler.downstream_flows.itervalues():
+            flows_to_drop = [flow for flow_id, flow in sig_table.flows.items()
+                             if isinstance(flow, FlowEntry) and flow_id not in valid_flow_ids]
             dl.extend([flow.remove() for flow in flows_to_drop])
 
-            for sig_table in handler.downstream_flows.itervalues():
-                flows_to_drop = [flow for flow_id, flow in sig_table.flows.items()
-                                 if isinstance(flow, FlowEntry) and flow_id not in valid_flow_ids]
-                dl.extend([flow.remove() for flow in flows_to_drop])
+    except Exception as _e:
+        pass
 
-        except Exception as _e:
-            pass
+    return gatherResults(dl, consumeErrors=True) if len(dl) > 0 else returnValue('no-flows-to-drop')
 
-        return gatherResults(dl, consumeErrors=True) if len(dl) > 0 else returnValue('no-flows-to-drop')
+@inlineCallbacks
+def remove(self):
+    """
+    Remove this flow entry from the list of existing entries and drop EVC
+    if needed
+    """
+    # Remove from exiting table list
+    flow_id = self.flow_id
+    flow_table = None
 
-    @inlineCallbacks
-    def remove(self):
-        """
-        Remove this flow entry from the list of existing entries and drop EVC
-        if needed
-        """
-        # Remove from exiting table list
-        flow_id = self.flow_id
-        flow_table = None
+    if self.flow_direction in FlowEntry.upstream_flow_types:
+        flow_table = self._handler.upstream_flows
 
-        if self.flow_direction in FlowEntry.upstream_flow_types:
-            flow_table = self._handler.upstream_flows
+    elif self.flow_direction in FlowEntry.downstream_flow_types:
+        sig_table = self._handler.downstream_flows.get(self.signature)
+        flow_table = sig_table.flows if sig_table is not None else None
 
-        elif self.flow_direction in FlowEntry.downstream_flow_types:
-            sig_table = self._handler.downstream_flows.get(self.signature)
-            flow_table = sig_table.flows if sig_table is not None else None
+    if flow_table is None or flow_id not in flow_table.keys():
+        returnValue('NOP')
 
-        if flow_table is None or flow_id not in flow_table.keys():
-            returnValue('NOP')
+    # Remove from flow table and clean up flow table if empty
+    flow_table.remove(flow_id)
+    evc_map, self.evc_map = self.evc_map, None
+    evc = None
 
-        # Remove from flow table and clean up flow table if empty
-        flow_table.remove(flow_id)
-        evc_map, self.evc_map = self.evc_map, None
-        evc = None
+    if self.flow_direction in FlowEntry.downstream_flow_types:
+        sig_table = self._handler.downstream_flows.get(self.signature)
+        if len(flow_table) == 0:   # Only 'evc' entry present
+            evc = sig_table.evc
+        else:
+            assert sig_table.evc is not None, 'EVC flow re-assignment error'
 
-        if self.flow_direction in FlowEntry.downstream_flow_types:
-            sig_table = self._handler.downstream_flows.get(self.signature)
-            if len(flow_table) == 0:   # Only 'evc' entry present
-                evc = sig_table.evc
-            else:
-                assert sig_table.evc is not None, 'EVC flow re-assignment error'
+    # Remove flow from the hardware
+    try:
+        dl = []
+        if evc_map is not None:
+            dl.append(evc_map.delete(self))
 
-        # Remove flow from the hardware
-        try:
-            dl = []
-            if evc_map is not None:
-                dl.append(evc_map.delete(self))
-
-            if evc is not None:
-                dl.append(evc.delete())
-
-            yield gatherResults(dl, consumeErrors=True)
-
-        except Exception as e:
-            log.exception('removal', e=e)
-
-        if self.flow_direction in FlowEntry.downstream_flow_types:
-            # If this flow owns the EVC, assign it to a remaining flow
-            sig_table = self._handler.downstream_flows.get(self.signature)
-            flow_evc = sig_table.evc
-
-            if flow_evc is not None and flow_evc.flow_entry is not None and flow_id == flow_evc.flow_entry.flow_id:
-                flow_evc.flow_entry = next((_flow for _flow in flow_table.itervalues()
-                                           if isinstance(_flow, FlowEntry)
-                                           and _flow.flow_id != flow_id), None)
-
-        # If evc was deleted, remove the signature table since now flows exist with
-        # that signature
         if evc is not None:
-            self._handler.downstream_flows.remove(self.signature)
+            dl.append(evc.delete())
 
-        self.evc = None
-        returnValue('Done')
+        yield gatherResults(dl, consumeErrors=True)
 
-    @staticmethod
-    def find_evc_map_flows(onu):
-        """
-        For a given OLT, find all the EVC Maps for a specific ONU
-        :param onu: (Onu) onu
-        :return: (list) of matching flows
-        """
-        # EVCs are only in the downstream table, EVC Maps are in upstream
-        onu_ports = onu.uni_ports
+    except Exception as e:
+        log.exception('removal', e=e)
 
-        all_flow_entries = onu.olt.upstream_flows
-        evc_maps = [flow_entry.evc_map for flow_entry in all_flow_entries.itervalues()
-                    if flow_entry.in_port in onu_ports
-                    and flow_entry.evc_map is not None
-                    and flow_entry.evc_map.valid]
+    if self.flow_direction in FlowEntry.downstream_flow_types:
+        # If this flow owns the EVC, assign it to a remaining flow
+        sig_table = self._handler.downstream_flows.get(self.signature)
+        flow_evc = sig_table.evc
 
-        return evc_maps
+        if flow_evc is not None and flow_evc.flow_entry is not None and flow_id == flow_evc.flow_entry.flow_id:
+            flow_evc.flow_entry = next((_flow for _flow in flow_table.itervalues()
+                                       if isinstance(_flow, FlowEntry)
+                                       and _flow.flow_id != flow_id), None)
 
-    @staticmethod
-    def sync_flows_by_onu(onu, reflow=False):
-        """
-        Check status of all flows on a per-ONU basis. Called when values
-        within the ONU are modified that may affect traffic.
+    # If evc was deleted, remove the signature table since now flows exist with
+    # that signature
+    if evc is not None:
+        self._handler.downstream_flows.remove(self.signature)
 
-        :param onu: (Onu) ONU to examine
-        :param reflow: (boolean) Flag, if True, requests that the flow be sent to
-                                 hardware even if the values in hardware are
-                                 consistent with the current flow settings
-        """
-        evc_maps = FlowEntry.find_evc_map_flows(onu)
-        evcs = {}
+    self.evc = None
+    returnValue('Done')
 
-        for evc_map in evc_maps:
-            if reflow or evc_map.reflow_needed():
-                evc_map.needs_update = False
+@staticmethod
+def find_evc_map_flows(onu):
+    """
+    For a given OLT, find all the EVC Maps for a specific ONU
+    :param onu: (Onu) onu
+    :return: (list) of matching flows
+    """
+    # EVCs are only in the downstream table, EVC Maps are in upstream
+    onu_ports = onu.uni_ports
 
-            if not evc_map.installed:
-                evc = evc_map.evc
-                if evc is not None:
-                    evcs[evc.name] = evc
+    all_flow_entries = onu.olt.upstream_flows
+    evc_maps = [flow_entry.evc_map for flow_entry in all_flow_entries.itervalues()
+                if flow_entry.in_port in onu_ports
+                and flow_entry.evc_map is not None
+                and flow_entry.evc_map.valid]
 
-        for evc in evcs.itervalues():
-            evc.installed = False
-            evc.schedule_install(delay=2)
+    return evc_maps
 
-    ######################################################
-    # Bulk operations
+@staticmethod
+def sync_flows_by_onu(onu, reflow=False):
+    """
+    Check status of all flows on a per-ONU basis. Called when values
+    within the ONU are modified that may affect traffic.
 
-    @staticmethod
-    def clear_all(handler):
-        """
-        Remove all flows for the device.
+    :param onu: (Onu) ONU to examine
+    :param reflow: (boolean) Flag, if True, requests that the flow be sent to
+                             hardware even if the values in hardware are
+                             consistent with the current flow settings
+    """
+    evc_maps = FlowEntry.find_evc_map_flows(onu)
+    evcs = {}
 
-        :param handler: voltha adapter device handler
-        """
-        handler.downstream_flows.clear_all()
-        handler.upstream_flows.clear_all()
+    for evc_map in evc_maps:
+        if reflow or evc_map.reflow_needed():
+            evc_map.needs_update = False
 
-    @staticmethod
-    def get_packetout_info(handler, logical_port):
-        """
-        Find parameters needed to send packet out successfully to the OLT.
+        if not evc_map.installed:
+            evc = evc_map.evc
+            if evc is not None:
+                evcs[evc.name] = evc
 
-        :param handler: voltha adapter device handler
-        :param logical_port: (int) logical port number for packet to go out.
+    for evc in evcs.itervalues():
+        evc.installed = False
+        evc.schedule_install(delay=2)
 
-        :return: physical port number, ctag, stag, evcmap name
-        """
-        from ..onu import Onu
+######################################################
+# Bulk operations
 
-        for flow_entry in handler.upstream_flows.itervalues():
-            log.debug('get-packetout-info', flow_entry=flow_entry)
+@staticmethod
+def clear_all(handler):
+    """
+    Remove all flows for the device.
 
-            # match logical port
-            if flow_entry.evc_map is not None and flow_entry.evc_map.valid and \
-               flow_entry.logical_port == logical_port:
-                evc_map = flow_entry.evc_map
-                gem_ids_and_vid = evc_map.gem_ids_and_vid
+    :param handler: voltha adapter device handler
+    """
+    handler.downstream_flows.clear()
+    handler.upstream_flows.clear()
 
-                # must have valid gem id
-                if len(gem_ids_and_vid) > 0:
-                    for onu_id, gem_ids_with_vid in gem_ids_and_vid.iteritems():
-                        log.debug('get-packetout-info', onu_id=onu_id, 
-                                  gem_ids_with_vid=gem_ids_with_vid)
-                        if len(gem_ids_with_vid) > 0:
-                            gem_ids = gem_ids_with_vid[0]
-                            ctag = gem_ids_with_vid[1]
-                            gem_id = gem_ids[0]     # TODO: always grab first in list
-                            return flow_entry.in_port, ctag, Onu.gem_id_to_gvid(gem_id), \
-                                evc_map.get_evcmap_name(onu_id, gem_id)
-        return None, None, None, None
+@staticmethod
+def get_packetout_info(handler, logical_port):
+    """
+    Find parameters needed to send packet out successfully to the OLT.
+
+    :param handler: voltha adapter device handler
+    :param logical_port: (int) logical port number for packet to go out.
+
+    :return: physical port number, ctag, stag, evcmap name
+    """
+    from ..onu import Onu
+
+    for flow_entry in handler.upstream_flows.itervalues():
+        log.debug('get-packetout-info', flow_entry=flow_entry)
+
+        # match logical port
+        if flow_entry.evc_map is not None and flow_entry.evc_map.valid and \
+           flow_entry.logical_port == logical_port:
+            evc_map = flow_entry.evc_map
+            gem_ids_and_vid = evc_map.gem_ids_and_vid
+
+            # must have valid gem id
+            if len(gem_ids_and_vid) > 0:
+                for onu_id, gem_ids_with_vid in gem_ids_and_vid.iteritems():
+                    log.debug('get-packetout-info', onu_id=onu_id, 
+                              gem_ids_with_vid=gem_ids_with_vid)
+                    if len(gem_ids_with_vid) > 0:
+                        gem_ids = gem_ids_with_vid[0]
+                        ctag = gem_ids_with_vid[1]
+                        gem_id = gem_ids[0]     # TODO: always grab first in list
+                        return flow_entry.in_port, ctag, Onu.gem_id_to_gvid(gem_id), \
+                            evc_map.get_evcmap_name(onu_id, gem_id)
+    return None, None, None, None
diff --git a/voltha/adapters/adtran_olt/flow/flow_tables.py b/voltha/adapters/adtran_olt/flow/flow_tables.py
index 48e2e7e..034eb4b 100644
--- a/voltha/adapters/adtran_olt/flow/flow_tables.py
+++ b/voltha/adapters/adtran_olt/flow/flow_tables.py
@@ -13,62 +13,71 @@
 # limitations under the License.
 #
 from flow_entry import FlowEntry
+from collections import MutableMapping
 from evc import EVC
+import six
 
 
-class DeviceFlows(object):
-    """ Tracks existing flows on the device """
+class _Storage(MutableMapping):
+    def __init__(self, *args, **kwargs):
+        self._store = dict()   # Key = (str)Flow ID, Value = FlowEntry
+        self.update(dict(*args, **kwargs))  # use the free update to set keys
 
-    def __init__(self):
-        self._flow_table = dict()   # Key = (str)Flow ID, Value = FlowEntry
+    def _keytransform(self, key):
+        raise NotImplementedError
 
-    def __getitem__(self, item):
-        flow_id = item.flow_id if isinstance(item, FlowEntry) else item
-        return self._flow_table[flow_id]
+    def __getitem__(self, key):
+        return self._store[self._keytransform(key)]
+
+    def __setitem__(self, key, flow):
+        self._store[self._keytransform(key)] = flow
+
+    def __delitem__(self, key):
+        del self._store[self._keytransform(key)]
 
     def __iter__(self):
-        for _flow_id, _flow in self._flow_table.items():
-            yield _flow_id, _flow
-
-    def itervalues(self):
-        for _flow in self._flow_table.values():
-            yield _flow
-
-    def iterkeys(self):
-        for _id in self._flow_table.keys():
-            yield _id
-
-    def items(self):
-        return self._flow_table.items()
-
-    def values(self):
-        return self._flow_table.values()
-
-    def keys(self):
-        return self._flow_table.keys()
+        return iter(self._store)
 
     def __len__(self):
-        return len(self._flow_table)
+        return len(self._store)
+
+
+class DeviceFlows(_Storage):
+    """ Tracks existing flows on the device """
+    def _keytransform(self, key):
+        key = key.flow_id if isinstance(key, FlowEntry) else key
+        assert isinstance(key, six.integer_types), "Flow key should be int"
+        return key
+
+    def __setitem__(self, key, flow):
+        assert isinstance(flow, FlowEntry)
+        assert key == flow.flow_id
+        return super(DeviceFlows, self).__setitem__(key, flow)
 
     def add(self, flow):
+        """
+        Non-standard dict function that adds and returns the added element
+        If the element with this key already exists, no state is modified
+        :param FlowEntry flow: element to add
+        :return: returns the added element
+        :rtype: FlowEntry
+        """
         assert isinstance(flow, FlowEntry)
-        if flow.flow_id not in self._flow_table:
-            self._flow_table[flow.flow_id] = flow
+        if flow.flow_id not in self:
+            self[flow.flow_id] = flow
         return flow
 
-    def get(self, item):
-        flow_id = item.flow_id if isinstance(item, FlowEntry) else item
-        return self._flow_table.get(flow_id)
-
     def remove(self, item):
-        flow_id = item.flow_id if isinstance(item, FlowEntry) else item
-        return self._flow_table.pop(flow_id, None)
-
-    def clear_all(self):
-        self._flow_table = dict()
+        """
+        Non-standard dict function that removes and returns an element
+        :param Union[int, FlowEntry] item: identifier for which element to remove
+        :return: flow entry or None
+        :rtype: Optional[FlowEntry]
+        """
+        return self.pop(item, None)
 
 
-class DownstreamFlows(object):
+class DownstreamFlows(_Storage):
     """
     Tracks existing flows that are downstream (NNI as source port)
 
@@ -86,62 +95,40 @@
 
     TODO: Drop device ID from signatures once flow tables are unique to a device handler
     """
-    def __init__(self):
-        self._signature_table = dict()  # Key = (str)Downstream signature
-                                        #  |
-                                        #  +-> downstream-signature
-                                        #      |
-                                        #      +-> 'evc' -> EVC
-                                        #      |
-                                        #      +-> flow-ids -> flow-entries...
+    # Key = (str)Downstream signature
+    #  |
+    #  +-> downstream-signature
+    #      |
+    #      +-> 'evc' -> EVC
+    #      |
+    #      +-> flow-ids -> flow-entries...
 
-    def __getitem__(self, signature):
-        assert isinstance(signature, str)
-        return self._signature_table[signature]
+    def _keytransform(self, key):
+        key = key.signature if isinstance(key, DownstreamFlows.SignatureTableEntry) else key
+        assert isinstance(key, six.string_types)
+        return key
 
-    def __iter__(self):
-        for _flow_id, _flow in self._signature_table.items():
-            yield _flow_id, _flow
-
-    def itervalues(self):
-        for _flow in self._signature_table.values():
-            yield _flow
-
-    def iterkeys(self):
-        for _id in self._signature_table.keys():
-            yield _id
-
-    def items(self):
-        return self._signature_table.items()
-
-    def values(self):
-        return self._signature_table.values()
-
-    def keys(self):
-        return self._signature_table.keys()
-
-    def __len__(self):
-        return len(self._signature_table)
-
-    def get(self, signature):
-        assert isinstance(signature, str)
-        return self._signature_table.get(signature)
+    def __setitem__(self, key, item):
+        assert isinstance(item, DownstreamFlows.SignatureTableEntry)
+        assert key == item.signature
+        return super(DownstreamFlows, self).__setitem__(key, item)
 
     def add(self, signature):
-        assert isinstance(signature, str)
         """
         Can be called by upstream flow to reserve a slot
         """
-        if signature not in self._signature_table:
-            self._signature_table[signature] = DownstreamFlows.SignatureTableEntry(signature)
-        return self._signature_table[signature]
+        if signature not in self:
+            self[signature] = DownstreamFlows.SignatureTableEntry(signature)
+        return self[signature]
 
     def remove(self, signature):
-        assert isinstance(signature, str)
-        return self._signature_table.pop(signature)
-
-    def clear_all(self):
-        self._signature_table = dict()
+        """
+        Non-standard dict function that removes and returns an element
+        :param Union[str] signature: identifier for which element to remove
+        :return: Signature Table or None
+        :rtype: Optional[DownstreamFlows.SignatureTableEntry]
+        """
+        return self.pop(signature, None)
 
     class SignatureTableEntry(object):
         def __init__(self, signature):
@@ -150,12 +137,16 @@
             self._flow_table = DeviceFlows()
 
         @property
+        def signature(self):
+            return self._signature
+
+        @property
         def evc(self):
             return self._evc
 
         @evc.setter
         def evc(self, evc):
-            assert isinstance(evc, (EVC, type(None)))
+            assert isinstance(evc, EVC) or evc is None
             self._evc = evc
 
         @property
diff --git a/voltha/adapters/adtran_olt/flow/utility_evc.py b/voltha/adapters/adtran_olt/flow/utility_evc.py
index a028f96..81e09ca 100644
--- a/voltha/adapters/adtran_olt/flow/utility_evc.py
+++ b/voltha/adapters/adtran_olt/flow/utility_evc.py
@@ -155,4 +155,4 @@
         :return: (deferred)
         """
         _utility_evcs.clear()
-        EVC.remove_all(client, regex_)
\ No newline at end of file
+        EVC.remove_all(client, regex_)
diff --git a/voltha/adapters/adtran_olt/net/__init__.py b/voltha/adapters/adtran_olt/net/__init__.py
index b0fb0b2..18d64b2 100644
--- a/voltha/adapters/adtran_olt/net/__init__.py
+++ b/voltha/adapters/adtran_olt/net/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2017-present Open Networking Foundation
+# Copyright 2017-present Adtran, Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/voltha/adapters/adtran_olt/net/adtran_netconf.py b/voltha/adapters/adtran_olt/net/adtran_netconf.py
index 4e39a6a..69c1ab6 100644
--- a/voltha/adapters/adtran_olt/net/adtran_netconf.py
+++ b/voltha/adapters/adtran_olt/net/adtran_netconf.py
@@ -39,6 +39,18 @@
     """.format(adtran_module_url('adtran-physical-entities'))
 
 
+def _raises_rpc_error(message=""):
+    def raises_rpc_error(func):
+        def wrap_func(*args, **kwargs):
+            try:
+                return func(*args, **kwargs)
+            except RPCError as e:
+                log.exception(message, e=e)
+                raise
+        return wrap_func
+    return raises_rpc_error
+
+
 class AdtranNetconfClient(object):
     """
     Performs NETCONF requests
@@ -158,11 +170,7 @@
 
         :return: (deferred) Deferred request that wraps the GetReply class
         """
-        if not self._session:
-            raise NotImplemented('No SSH Session')
-
-        if not self._session.connected:
-            self._reconnect()
+        self._check_session()
 
         return threads.deferToThread(self._do_get_config, source)
 
@@ -185,14 +193,11 @@
         """
         log.debug('get', filter=payload)
 
-        if not self._session:
-            raise NotImplemented('No SSH Session')
-
-        if not self._session.connected:
-            self._reconnect()
+        self._check_session()
 
         return threads.deferToThread(self._do_get, payload)
 
+    @_raises_rpc_error('get')
     def _do_get(self, payload):
         """
         Get the requested data from the server
@@ -200,15 +205,10 @@
         :param payload: Payload/filter
         :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', e=e)
-            raise
+        log.debug('get', payload=payload)
+        response = self._session.get(payload)
+        # To get XML, use response.xml
+        log.debug('response', response=response)
 
         return response
 
@@ -220,21 +220,17 @@
         log.info('lock', source=source, timeout=lock_timeout)
 
         if not self._session or not self._session.connected:
-            raise NotImplemented('TODO: Support auto-connect if needed')
+            raise NotImplementedError('TODO: Support auto-connect if needed')
 
         return threads.deferToThread(self._do_lock, source, lock_timeout)
 
+    @_raises_rpc_error('lock')
     def _do_lock(self, source, lock_timeout):
         """
         Lock the configuration system
         """
-        try:
-            response = self._session.lock(source, timeout=lock_timeout)
-            # To get XML, use response.xml
-
-        except RPCError as e:
-            log.exception('lock', e=e)
-            raise
+        response = self._session.lock(source, timeout=lock_timeout)
+        # To get XML, use response.xml
 
         return response
 
@@ -248,21 +244,16 @@
         log.info('unlock', source=source)
 
         if not self._session or not self._session.connected:
-            raise NotImplemented('TODO: Support auto-connect if needed')
+            raise NotImplementedError('TODO: Support auto-connect if needed')
 
         return threads.deferToThread(self._do_unlock, source)
 
+    @_raises_rpc_error('unlock')
     def _do_unlock(self, source):
         """
         Lock the configuration system
         """
-        try:
-            response = self._session.unlock(source)
-            # To get XML, use response.xml
-
-        except RPCError as e:
-            log.exception('unlock', e=e)
-            raise
+        response = self._session.unlock(source)
 
         return response
 
@@ -290,7 +281,7 @@
         :return: (deferred) for RpcReply
         """
         if not self._session:
-            raise NotImplemented('No SSH Session')
+            raise NotImplementedError('No SSH Session')
 
         if not self._session.connected:
             try:
@@ -309,8 +300,7 @@
                       default_operation=default_operation)
 
             rpc_reply = yield threads.deferToThread(self._do_edit_config, target,
-                                                    config, default_operation,
-                                                    test_option, error_option)
+                                                    config)
         except Exception as e:
             if ignore_delete_error and 'operation="delete"' in config.lower():
                 returnValue('ignoring-delete-error')
@@ -319,8 +309,7 @@
 
         returnValue(rpc_reply)
 
-    def _do_edit_config(self, target, config, default_operation, test_option, error_option,
-                        ignore_delete_error=False):
+    def _do_edit_config(self, target, config, ignore_delete_error=False):
         """
         Perform actual edit-config operation
         """
@@ -353,11 +342,7 @@
         """
         log.debug('rpc', rpc=rpc_string)
 
-        if not self._session:
-            raise NotImplemented('No SSH Session')
-
-        if not self._session.connected:
-            self._reconnect()
+        self._check_session()
 
         return threads.deferToThread(self._do_rpc, rpc_string)
 
@@ -371,3 +356,9 @@
             raise
 
         return response
+
+    def _check_session(self):
+        if not self._session:
+            raise NotImplementedError('No SSH Session')
+        if not self._session.connected:
+            self._reconnect()
diff --git a/voltha/adapters/adtran_olt/net/adtran_rest.py b/voltha/adapters/adtran_olt/net/adtran_rest.py
index 9020e82..97308d3 100644
--- a/voltha/adapters/adtran_olt/net/adtran_rest.py
+++ b/voltha/adapters/adtran_olt/net/adtran_rest.py
@@ -67,15 +67,15 @@
     for _method in _valid_methods:
         assert _method in _valid_results  # Make sure we have a results entry for each supported method
 
-    def __init__(self, host_ip, port, username='', password='', timeout=10):
+    def __init__(self, host_ip, port, username='', password='', timeout=10.0):
         """
         REST Client initialization
 
-        :param host_ip: (string) IP Address of Adtran Device
-        :param port: (int) Port number
-        :param username: (string) Username for credentials
-        :param password: (string) Password for credentials
-        :param timeout: (int) Number of seconds to wait for a response before timing out
+        :param str host_ip: IP Address of Adtran Device
+        :param int port: Port number
+        :param str username: Username for credentials
+        :param str password: Password for credentials
+        :param float timeout: Number of seconds to wait for a response before timing out
         """
         self._ip = host_ip
         self._port = port
@@ -88,20 +88,23 @@
 
     @inlineCallbacks
     def request(self, method, uri, data=None, name='', timeout=None, is_retry=False,
-                suppress_error=False):
+                suppress_error=False, **kwargs):
         """
         Send a REST request to the Adtran device
 
-        :param method: (string) HTTP method
-        :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.
-        :param suppress_error: (boolean) If true, do not output ERROR message on REST request failure
-        :return: (dict) On success with the proper results
+        :param str method: HTTP method
+        :param str uri: fully URL to perform method on
+        :param Optional[str] data: optional data for the request body
+        :param str name: optional name of the request, useful for logging purposes
+        :param Optional[float] timeout: Number of seconds to wait for a response before timing out
+        :param bool is_retry: 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.
+        :param bool suppress_error: If true, do not output ERROR message on REST request failure
+        :keyword dict json: json body passed as a dictionary and used in the absence of data
+
+        :return: On success with the proper results
+        :rtype dict
         """
         log.debug('request', method=method, uri=uri, data=data, retry=is_retry)
 
@@ -114,6 +117,10 @@
         response = None
         timeout = timeout or self._timeout
 
+        json_data = kwargs.get('json')
+        if data is None and json_data:
+            data = json.dumps(json_data)
+
         try:
             if method.upper() == 'GET':
                 response = yield treq.get(url,
@@ -137,13 +144,11 @@
                                              auth=(self._username, self._password),
                                              timeout=timeout,
                                              headers=self.REST_DELETE_REQUEST_HEADER)
-            else:
-                raise NotImplementedError("REST method '{}' is not supported".format(method))
 
         except NotImplementedError:
             raise
 
-        except (ConnectionDone, ConnectionLost) as e:
+        except (ConnectionDone, ConnectionLost):
             if is_retry:
                 raise
             returnValue(self.request(method, uri, data=data, name=name,
diff --git a/voltha/adapters/adtran_olt/pytest.ini b/voltha/adapters/adtran_olt/pytest.ini
new file mode 100644
index 0000000..79ffd10
--- /dev/null
+++ b/voltha/adapters/adtran_olt/pytest.ini
@@ -0,0 +1,23 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
+
+[pytest]
+addopts = --cov=. --cov-config=.coveragerc --cov-report html --cov-report term-missing
+          --doctest-modules -vv --junit-xml junit-coverage.xml
+doctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS
+norecursedirs=
+
+filterwarnings =
+    ignore::DeprecationWarning
+    ignore::PendingDeprecationWarning
diff --git a/voltha/adapters/adtran_olt/resources/adtran_olt_resource_manager.py b/voltha/adapters/adtran_olt/resources/adtran_olt_resource_manager.py
index 074c07e..93dfd8a 100644
--- a/voltha/adapters/adtran_olt/resources/adtran_olt_resource_manager.py
+++ b/voltha/adapters/adtran_olt/resources/adtran_olt_resource_manager.py
@@ -70,7 +70,7 @@
 
     def __del__(self):
         self.log.info("clearing-device-resource-pool")
-        for key, resource_mgr in self.resource_mgrs.iteritems():
+        for key, resource_mgr in self.resource_managers.iteritems():
             resource_mgr.clear_device_resource_pool()
 
     def get_onu_id(self, pon_intf_id):
@@ -107,7 +107,7 @@
                                                           PONResourceManager.ALLOC_ID,
                                                           onu_id=onu_id,
                                                           num_of_id=1)
-        if alloc_id_list and len(alloc_id_list) == 0:
+        if alloc_id_list is None or len(alloc_id_list) == 0:
             self.log.error("no-alloc-id-available")
             return None
 
@@ -285,4 +285,4 @@
     def update_flow_id_info_for_uni(self, pon_intf_id, onu_id, uni_id, flow_id, flow_data):
         pon_intf_onu_id = (pon_intf_id, onu_id, uni_id)
         return self.resource_managers[pon_intf_id].update_flow_id_info_for_onu(
-            pon_intf_onu_id, flow_id, flow_data)
\ No newline at end of file
+            pon_intf_onu_id, flow_id, flow_data)
diff --git a/voltha/adapters/adtran_olt/resources/adtran_resource_manager.py b/voltha/adapters/adtran_olt/resources/adtran_resource_manager.py
index 965e6ca..e49e8aa 100644
--- a/voltha/adapters/adtran_olt/resources/adtran_resource_manager.py
+++ b/voltha/adapters/adtran_olt/resources/adtran_resource_manager.py
@@ -217,8 +217,11 @@
                          else False
         """
         status = False
-
-        path = self._get_path(pon_intf_id, resource_type)
+        try:
+	    path = self._get_path(pon_intf_id, resource_type)
+        except KeyError:
+            path = None
+        
         if path is None:
             return status
 
diff --git a/voltha/adapters/adtran_olt/test.mk b/voltha/adapters/adtran_olt/test.mk
new file mode 100644
index 0000000..da6b625
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test.mk
@@ -0,0 +1,72 @@
+THIS_MAKEFILE      := $(abspath $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)))
+WORKING_DIR        := $(dir $(THIS_MAKEFILE) )
+ADAPTER_NAME       := $(notdir $(patsubst %/,%,$(WORKING_DIR)))
+ADAPTERS_DIR       := $(dir $(patsubst %/,%,$(WORKING_DIR)))
+VOLTHA_DIR         := $(dir $(patsubst %/,%,$(ADAPTERS_DIR)))
+export VOLTHA_BASE := $(VOLTHA_DIR)../
+GIT_DIR           := $(dir $(patsubst %/,%,$(VOLTHA_DIR))).git
+
+OPENOLT_DIR        := $(ADAPTERS_DIR)openolt
+OPENOLT_PROTO      := $(shell find $(OPENOLT_DIR)/protos/ -name '*.proto')
+OPENOLT_PB2        := $(patsubst %.proto,%_pb2.py,$(OPENOLT_PROTO))
+
+VOLTHA_PROTO       := $(shell find $(VOLTHA_DIR)protos -name '*.proto')
+VOLTHA_PB2         := $(patsubst %.proto,%_pb2.py,$(VOLTHA_PROTO))
+
+VENVDIR             =$(VOLTHA_BASE)venv-$(shell uname -s | tr '[:upper:]' '[:lower:]')
+TESTDIR             =$(WORKING_DIR)test
+IN_VENV            :=. '$(VENVDIR)/bin/activate';
+TEST_REQ_INSTALLED := $(VENVDIR)/.$(ADAPTER_NAME)-test
+
+RUN_PYTEST=$(IN_VENV) PYTHONPATH=$(VOLTHA_BASE):$(VOLTHA_DIR)protos/third_party py.test -vvlx
+
+.PHONY: test
+test: requirements hooks
+	@rm -rf $(TESTDIR)/__pycache__
+	@cd $(WORKING_DIR); $(RUN_PYTEST) $(TESTDIR); coverage xml
+
+.PHONY: clean
+clean:
+	@-rm -rf .coverage
+	@-rm -rf htmlcov
+	@-rm -rf *coverage.xml
+	@-rm -rf .pytest_cache
+	@-find $(WORKING_DIR) -type f -name '*.pyc' -delete
+	@-find $(VOLTHA_DIR)protos -type f -name '*_pb2.py' -delete
+	@-find $(OPENOLT_DIR)/protos -type f -name '*_pb2.py' -delete
+
+.PHONY: lint
+lint: requirements
+	@-$(IN_VENV) pylint `pwd`
+
+.PHONY: create-venv
+create-venv: $(VENVDIR)/.built
+
+
+$(VENVDIR)/.built:
+	cd $(VOLTHA_BASE); make venv
+
+$(OPENOLT_PB2): %_pb2.py : %.proto
+	@echo !-- Making $(@) because $< changed ---
+	@cd $(OPENOLT_DIR); $(IN_VENV) $(MAKE)
+
+$(VOLTHA_PB2): %_pb2.py : %.proto
+	@echo !-- Making $(@) because $< changed ---
+	@cd $(VOLTHA_DIR)/protos; $(IN_VENV) $(MAKE) third_party build
+
+$(TEST_REQ_INSTALLED): $(WORKING_DIR)test_requirements.txt \
+                       $(VOLTHA_BASE)requirements.txt
+	@$(IN_VENV) pip install --upgrade -r $(WORKING_DIR)test_requirements.txt
+	@ virtualenv -p python2 --relocatable ${VENVDIR}
+	uname -s > ${@};
+
+.PHONY: requirements
+requirements: create-venv $(OPENOLT_PB2) $(VOLTHA_PB2) $(TEST_REQ_INSTALLED)
+
+.PHONY: hooks
+hooks: $(GIT_DIR)/hooks/commit-msg
+	@echo "Commit hooks installed"
+
+$(GIT_DIR)/hooks/commit-msg:
+	@curl https://gerrit.opencord.org/tools/hooks/commit-msg > $(GIT_DIR)/hooks/commit-msg
+	@chmod u+x $(GIT_DIR)/hooks/commit-msg
diff --git a/voltha/adapters/adtran_olt/test/__init__.py b/voltha/adapters/adtran_olt/test/__init__.py
new file mode 100644
index 0000000..18d64b2
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
diff --git a/voltha/adapters/adtran_olt/test/codec/__init__.py b/voltha/adapters/adtran_olt/test/codec/__init__.py
new file mode 100644
index 0000000..18d64b2
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/codec/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
diff --git a/voltha/adapters/adtran_olt/test/codec/resources/__init__.py b/voltha/adapters/adtran_olt/test/codec/resources/__init__.py
new file mode 100644
index 0000000..1e5268d
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/codec/resources/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
\ No newline at end of file
diff --git a/voltha/adapters/adtran_olt/test/codec/resources/physical_entities_state_xml.py b/voltha/adapters/adtran_olt/test/codec/resources/physical_entities_state_xml.py
new file mode 100644
index 0000000..c4050f8
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/codec/resources/physical_entities_state_xml.py
@@ -0,0 +1,46 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
+
+test_xml = """
+  <data>
+    <physical-entities-state xmlns="http://www.adtran.com/ns/yang/adtran-physical-entities">
+      <physical-entity>
+        <a-string>something</a-string>
+      </physical-entity>
+    </physical-entities-state>
+  </data>
+"""
+
+physical_entities_output = """
+  <data>
+    <physical-entities-state xmlns="http://www.adtran.com/ns/yang/adtran-physical-entities">
+      <physical-entity>
+        <name>temperature 0/1</name>
+        <availability xmlns="http://www.adtran.com/ns/yang/adtran-physical-entity-availability">
+          <availability-status/>
+        </availability>
+        <classification xmlns:adtn-phys-sens="http://www.adtran.com/ns/yang/adtran-physical-sensors">adtn-phys-sens:temperature-sensor-celsius</classification>
+        <is-field-replaceable>false</is-field-replaceable>
+      </physical-entity>
+      <physical-entity>
+        <name>transceiver 0/1</name>
+        <availability xmlns="http://www.adtran.com/ns/yang/adtran-physical-entity-availability">
+          <availability-status/>
+        </availability>
+        <classification xmlns:adtn-phys-mod-trans="http://www.adtran.com/ns/yang/adtran-physical-module-transceivers">adtn-phys-mod-trans:transceiver</classification>
+        <is-field-replaceable>false</is-field-replaceable>
+      </physical-entity>
+    </physical-entities-state>
+  </data>
+"""
\ No newline at end of file
diff --git a/voltha/adapters/adtran_olt/test/codec/resources/sample_json.py b/voltha/adapters/adtran_olt/test/codec/resources/sample_json.py
new file mode 100644
index 0000000..f9eec68
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/codec/resources/sample_json.py
@@ -0,0 +1,232 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
+
+
+olt_state_json = {
+    "software-version": "ngpon2_agent-13.0.32-1.657.815547",
+    "pon": [{
+        "pon-id": 0,
+        "downstream-channel-id": 15,
+        "onu": [{
+            "onu-id": 0,
+            "reported-password": "redacted",
+            "rssi": -207,
+            "equalization-delay": 620952,
+            "fiber-length": 47,
+            "op-code": 0,
+            "response-code": 0
+        }],
+        "ont-los": [],
+        "gem": [{
+            "port-id": 2176,
+            "tx-packets": 65405,
+            "rx-packets": 13859,
+            "tx-bytes": "5420931",
+            "rx-bytes": "3242784",
+            "of-port-name": "pon0_128",
+            "onu-id": 0,
+            "alloc-id": 1024
+        }, {
+            "port-id": 2308,
+            "tx-packets": 763461880,
+            "rx-packets": 1619060062,
+            "tx-bytes": "141303946790728",
+            "rx-bytes": "145149180973448",
+            "of-port-name": "pon0_4",
+            "onu-id": 0,
+            "alloc-id": 1024
+        }, {
+            "port-id": 2309,
+            "tx-packets": 18,
+            "rx-packets": 9272150,
+            "tx-bytes": "1152",
+            "rx-bytes": "593417600",
+            "of-port-name": "pon0_5",
+            "onu-id": 0,
+            "alloc-id": 1024
+        }],
+        "rx-packets": 1625773517,
+        "rx-bytes": "145149613233620",
+        "tx-packets": 761098346,
+        "tx-bytes": "141303797318481",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "gAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 1,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 2,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 3,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 4,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 5,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 6,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 7,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 8,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 9,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 10,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 11,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 12,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 13,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 14,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }, {
+        "pon-id": 15,
+        "downstream-channel-id": 255,
+        "ont-los": [],
+        "rx-packets": 0,
+        "rx-bytes": "0",
+        "tx-packets": 0,
+        "tx-bytes": "0",
+        "tx-bip-errors": 0,
+        "wm-tuned-out-onus": "AAAAAAAAAAAAAAAAAAAAAA==",
+        "ra-complete-onus": "AAAAAAAAAAAAAAAAAAAAAA=="
+    }]
+}
diff --git a/voltha/adapters/adtran_olt/test/codec/test_ietf_interfaces.py b/voltha/adapters/adtran_olt/test/codec/test_ietf_interfaces.py
new file mode 100644
index 0000000..446b563
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/codec/test_ietf_interfaces.py
@@ -0,0 +1,77 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 voltha.adapters.adtran_olt.codec.ietf_interfaces import (
+    IetfInterfacesConfig, IetfInterfacesState
+)
+from mock import MagicMock
+from pytest_twisted import inlineCallbacks
+from xmltodict import parse
+
+
+def test_create_config():
+    IetfInterfacesConfig(None)
+
+
+@inlineCallbacks
+def test_get_config():
+    session = MagicMock()
+    ifc = IetfInterfacesConfig(session)
+    session.get.return_value = 'test value'
+    cfg = yield ifc.get_config()
+    assert 'test value' == cfg
+    assert ('running',) == session.get.call_args[0]
+    xml = parse(session.get.call_args[1]['filter'])
+    contents = {
+        'filter': {
+            '@xmlns': 'urn:ietf:params:xml:ns:netconf:base:1.0',
+            'interfaces': {
+                '@xmlns': 'urn:ietf:params:xml:ns:yang:ietf-interfaces',
+                'interface': None
+            }
+        }
+    }
+    assert contents == xml
+
+
+def test_create_state():
+    IetfInterfacesState(None)
+
+
+@inlineCallbacks
+def test_get_state():
+    session = MagicMock()
+    ifc = IetfInterfacesState(session)
+    session.get.return_value = 'test value'
+    state = yield ifc.get_state()
+    assert 'test value' == state
+    xml = parse(session.get.call_args[0][0])
+    contents = {
+        'filter': {
+            '@xmlns': 'urn:ietf:params:xml:ns:netconf:base:1.0',
+            'interfaces-state': {
+                '@xmlns': 'urn:ietf:params:xml:ns:yang:ietf-interfaces',
+                'interface': {
+                    'name': None,
+                    'type': None,
+                    'admin-status': None,
+                    'oper-status': None,
+                    'last-change': None,
+                    'phys-address': None,
+                    'speed': None
+                }
+            }
+        }
+    }
+    assert contents == xml
diff --git a/voltha/adapters/adtran_olt/test/codec/test_olt_config.py b/voltha/adapters/adtran_olt/test/codec/test_olt_config.py
new file mode 100644
index 0000000..49deb29
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/codec/test_olt_config.py
@@ -0,0 +1,387 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 voltha.adapters.adtran_olt.codec.olt_config import OltConfig
+
+import pytest
+
+real_json = [{
+    "pon-id": 0,
+    "enabled": True,
+    "downstream-fec-enable": True,
+    "upstream-fec-enable": True,
+    "onus": {
+        "onu": [{
+            "onu-id": 0,
+            "serial-number": "QURUThYmBZk=",
+            "enable": True,
+            "protection-requested": False,
+            "t-conts": {
+                "t-cont": [{
+                    "alloc-id": 1024,
+                    "traffic-descriptor": {
+                        "fixed-bandwidth": "0",
+                        "assured-bandwidth": "0",
+                        "maximum-bandwidth": "8500000000"
+                    },
+                    # I don't actually see this in the model but I am passing in the expected values.
+                    "best-effort": {
+                        "bandwidth": 12345,
+                        "priority": 0,
+                        "weight": 20
+                    }
+                }]
+            },
+            "gem-ports": {
+                "gem-port": [{
+                    "port-id": 2176,
+                    "encryption": False,
+                    "alloc-id": 1024
+                }, {
+                    "port-id": 2308,
+                    "encryption": False,
+                    "alloc-id": 1024
+                }, {
+                    "port-id": 2309,
+                    "encryption": False,
+                    "alloc-id": 1024
+                }]
+            }
+        }]
+}
+}, {
+    "pon-id": 1,
+    "enabled": False
+}, {
+    "pon-id": 2,
+    "enabled": False
+}, {
+    "pon-id": 3,
+    "enabled": False
+}, {
+    "pon-id": 4,
+    "enabled": False
+}, {
+    "pon-id": 5,
+    "enabled": False
+}, {
+    "pon-id": 6,
+    "enabled": False
+}, {
+    "pon-id": 7,
+    "enabled": False
+}, {
+    "pon-id": 8,
+    "enabled": False
+}, {
+    "pon-id": 9,
+    "enabled": False
+}, {
+    "pon-id": 10,
+    "enabled": False
+}, {
+    "pon-id": 11,
+    "enabled": False
+}, {
+    "pon-id": 12,
+    "enabled": False
+}, {
+    "pon-id": 13,
+    "enabled": False
+}, {
+    "pon-id": 14,
+    "enabled": False
+}, {
+    "pon-id": 15,
+    "enabled": False
+}]
+
+tcont_config = real_json[0]["onus"]["onu"][0]["t-conts"]["t-cont"][0]
+
+
+@pytest.fixture()
+def traffic_desc_object():
+    traffic_desc_config = tcont_config["traffic-descriptor"]
+    return OltConfig.Pon.Onu.TCont.TrafficDescriptor(traffic_desc_config)
+
+
+@pytest.fixture()
+def bad_traffic_desc_object():
+    bad_traffic_desc = {
+        "fixed-bandwidth": "not-an-int",
+        "assured-bandwidth": SyntaxError(),
+        "maximum-bandwidth": []
+    }
+    return OltConfig.Pon.Onu.TCont.TrafficDescriptor(bad_traffic_desc)
+
+
+@pytest.fixture()
+def best_effort_object():
+    best_effort_config = tcont_config["best-effort"]
+    return OltConfig.Pon.Onu.TCont.BestEffort(best_effort_config)
+
+
+@pytest.fixture()
+def gemport_object():
+    gem_port_config = real_json[0]["onus"]["onu"][0]["gem-ports"]["gem-port"][0]
+    return OltConfig.Pon.Onu.GemPort(gem_port_config)
+
+
+@pytest.fixture()
+def onu_object():
+    onu_config = real_json[0]["onus"]["onu"][0]
+    return OltConfig.Pon.Onu(onu_config)
+
+
+@pytest.fixture()
+def pon_object():
+    pon_config = real_json[0]
+    return OltConfig.Pon(pon_config)
+
+
+@pytest.fixture()
+def olt_object():
+    olt_config = {"pon": real_json}
+    return OltConfig(olt_config)
+
+
+def test_tcont_to_string():
+    test_config = OltConfig.Pon.Onu.TCont(tcont_config)
+    assert str(test_config) == "OltConfig.Pon.Onu.TCont: alloc-id: 1024"
+
+
+def test_tcont_decode():
+    test_config = real_json[0]["onus"]["onu"][0]["t-conts"]
+    decoded_output = OltConfig.Pon.Onu.TCont.decode(test_config)
+    assert tcont_config == decoded_output[1024]._packet
+
+
+def test_tcont_decode_no_tcont():
+    decoded_output = OltConfig.Pon.Onu.TCont.decode(None)
+    assert decoded_output == {}
+
+
+def test_tcont_traffic_descriptor():
+    test_config = real_json[0]["onus"]["onu"][0]["t-conts"]
+    decoded_output = OltConfig.Pon.Onu.TCont.decode(test_config)[1024].traffic_descriptor
+    assert "OltConfig.Pon.Onu.TCont.TrafficDescriptor: 0/0/8500000000" == str(decoded_output)
+
+
+def test_tcont_traffic_descriptor_exists():
+    test_config = real_json[0]["onus"]["onu"][0]["t-conts"]
+    tcont_config = OltConfig.Pon.Onu.TCont.decode(test_config)[1024]
+    tcont_config._traffic_descriptor = "exists"
+    assert tcont_config.traffic_descriptor == "exists"
+
+
+def test_traffic_descriptor_fixed_bw(traffic_desc_object):
+    assert traffic_desc_object.fixed_bandwidth == 0
+
+
+def test_traffic_descriptor_fixed_bw_exception(bad_traffic_desc_object):
+    assert bad_traffic_desc_object.fixed_bandwidth == 0
+
+
+def test_traffic_descriptor_assured_bw(traffic_desc_object):
+    assert traffic_desc_object.assured_bandwidth == 0
+
+
+def test_traffic_descriptor_assured_bw_exception(bad_traffic_desc_object):
+    assert bad_traffic_desc_object.assured_bandwidth == 0
+
+
+def test_traffic_descriptor_max_bw(traffic_desc_object):
+    assert traffic_desc_object.maximum_bandwidth == 8500000000
+
+
+def test_traffic_descriptor_max_bw_exception(bad_traffic_desc_object):
+    assert bad_traffic_desc_object.maximum_bandwidth == 0
+
+
+def test_traffic_descriptor_additional_bw_eligibility(traffic_desc_object):
+    assert traffic_desc_object.additional_bandwidth_eligibility == "none"
+
+
+def test_tcont_best_effort():
+    test_config = real_json[0]["onus"]["onu"][0]["t-conts"]
+    decoded_output = OltConfig.Pon.Onu.TCont.decode(test_config)[1024].best_effort
+    assert "OltConfig.Pon.Onu.TCont.BestEffort: 12345" == str(decoded_output)
+
+
+def test_tcont_best_effort_exists():
+    test_config = real_json[0]["onus"]["onu"][0]["t-conts"]
+    tcont_config = OltConfig.Pon.Onu.TCont.decode(test_config)[1024]
+    tcont_config._best_effort = "exists"
+    assert tcont_config.best_effort == "exists"
+
+
+def test_best_effort_bandwidth(best_effort_object):
+    assert best_effort_object.bandwidth == 12345
+
+
+def test_best_effort_priority(best_effort_object):
+    assert best_effort_object.priority == 0
+
+
+def test_best_effort_weight(best_effort_object):
+    assert best_effort_object.weight == 20
+
+
+def test_gem_port_decode_no_gemport(gemport_object):
+    assert gemport_object.decode(None) == {}
+
+
+def test_gem_port_to_string(gemport_object):
+    assert str(gemport_object) == "OltConfig.Pon.Onu.GemPort: port-id: 2176/1024"
+
+
+def test_gem_port_port_id(gemport_object):
+    assert gemport_object.port_id == 2176
+
+
+def test_gem_port_gem_id(gemport_object):
+    assert gemport_object.port_id == 2176
+
+
+def test_gem_port_alloc_id(gemport_object):
+    assert gemport_object.alloc_id == 1024
+
+
+def test_gem_port_omci_transport(gemport_object):
+    assert not gemport_object.omci_transport
+
+
+def test_gem_port_encryption(gemport_object):
+    assert not gemport_object.encryption
+
+
+def test_onu_to_string(onu_object):
+    assert str(onu_object) == "OltConfig.Pon.Onu: onu-id: 0"
+
+
+def test_onu_onu_id(onu_object):
+    assert onu_object.onu_id == 0
+
+
+def test_onu_serial_number_64(onu_object):
+    assert onu_object.serial_number_64 == "QURUThYmBZk="
+
+
+def test_onu_password(onu_object):
+    assert onu_object.password == "0"
+
+
+def test_onu_enable(onu_object):
+    assert onu_object.enable
+
+
+def test_onu_gem_ports(onu_object):
+    assert str(onu_object.gem_ports[2176]) == "OltConfig.Pon.Onu.GemPort: port-id: 2176/1024"
+
+
+def test_onu_tconts(onu_object):
+    assert str(onu_object.tconts[1024]) == "OltConfig.Pon.Onu.TCont: alloc-id: 1024"
+
+
+def test_onu_gem_ports_dict(onu_object):
+    assert str(onu_object.gem_ports_dict[2176]) == "OltConfig.Pon.Onu.GemPort: port-id: 2176/1024"
+
+
+def test_onu_gem_ports_dict_existing(onu_object):
+    onu_object._gem_ports_dict = "existing"
+    assert onu_object.gem_ports_dict == "existing"
+
+
+def test_onu_tconts_dict(onu_object):
+    assert str(onu_object.tconts_dict[1024]) == "OltConfig.Pon.Onu.TCont: alloc-id: 1024"
+
+
+def test_onu_tconts_dict_existing(onu_object):
+    onu_object._tconts_dict = "existing"
+    assert onu_object.tconts_dict == "existing"
+
+
+def test_onu_decode_onus():
+    assert str(OltConfig.Pon.Onu.decode(real_json[0]["onus"])[0]) == "OltConfig.Pon.Onu: onu-id: 0"
+
+
+def test_onu_decode_onus_no_onus():
+    assert OltConfig.Pon.Onu.decode(None) == {}
+
+
+def test_onu_decode_onu_list():
+    assert str(OltConfig.Pon.Onu.decode(real_json[0]["onus"]["onu"])[0]) == "OltConfig.Pon.Onu: onu-id: 0"
+
+
+def test_onu_decode_bad_dict():
+    test_json = [{}]
+    assert OltConfig.Pon.Onu.decode(test_json) == {}
+
+
+def test_pon_to_string(pon_object):
+    assert str(pon_object) == "OltConfig.Pon: pon-id: 0"
+
+
+def test_pon_pon_id(pon_object):
+    assert pon_object.pon_id == 0
+
+
+def test_pon_enabled(pon_object):
+    assert pon_object.enabled
+
+
+def test_pon_downstream_fec_enable(pon_object):
+    assert pon_object.downstream_fec_enable
+
+
+def test_pon_upstream_fec_enable(pon_object):
+    assert pon_object.upstream_fec_enable
+
+
+def test_pon_deployment_range(pon_object):
+    assert pon_object.deployment_range == 25000
+
+
+def test_pon_onus(pon_object):
+    assert str(pon_object.onus[0]) == "OltConfig.Pon.Onu: onu-id: 0"
+
+
+def test_pon_onus_existing(pon_object):
+    pon_object._onus = "existing"
+    assert pon_object.onus == "existing"
+
+
+def test_pon_decode_no_pons(pon_object):
+    assert pon_object.decode(None) == {}
+
+
+def test_olt_to_string(olt_object):
+    assert str(olt_object) == "OltConfig: "
+
+
+def test_olt_olt_id(olt_object):
+    assert olt_object.olt_id == ""
+
+
+def test_olt_debug_output(olt_object):
+    assert olt_object.debug_output == "warning"
+
+
+def test_olt_pons(olt_object):
+    assert str(olt_object.pons[0]) == "OltConfig.Pon: pon-id: 0"
+
+
+def test_olt_existing_pons(olt_object):
+    olt_object._pons = "existing"
+    assert olt_object.pons == "existing"
\ No newline at end of file
diff --git a/voltha/adapters/adtran_olt/test/codec/test_olt_state.py b/voltha/adapters/adtran_olt/test/codec/test_olt_state.py
new file mode 100644
index 0000000..4479cd5
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/codec/test_olt_state.py
@@ -0,0 +1,147 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 resources.sample_json import olt_state_json
+from voltha.adapters.adtran_olt.codec.olt_state import OltState
+
+import pytest
+
+
+@pytest.fixture()
+def olt_state_object():
+    return OltState(olt_state_json)
+
+
+@pytest.fixture()
+def pon_object():
+    return OltState.Pon(olt_state_json["pon"][0])
+
+
+@pytest.fixture()
+def onu_object():
+    return OltState.Pon.Onu(olt_state_json["pon"][0]["onu"][0])
+
+
+@pytest.fixture()
+def gem_object():
+    return OltState.Pon.Gem(olt_state_json["pon"][0]["gem"][0])
+
+
+def test_olt_to_string(olt_state_object):
+    assert str(olt_state_object) == "OltState: ngpon2_agent-13.0.32-1.657.815547"
+
+
+def test_olt_state_software_version(olt_state_object):
+    assert olt_state_object.software_version == "ngpon2_agent-13.0.32-1.657.815547"
+
+
+def test_olt_state_pons(olt_state_object):
+    assert str(olt_state_object.pons[0]) == "OltState.Pon: pon-id: 0"
+
+
+def test_olt_state_len(olt_state_object):
+    assert len(olt_state_object) == 16
+
+
+def test_olt_state_get_item(olt_state_object):
+    assert str(olt_state_object[1]) == "OltState.Pon: pon-id: 1"
+
+
+def test_olt_state_get_item_not_int(olt_state_object):
+    with pytest.raises(TypeError):
+        olt_state_object["something"]
+
+
+def test_olt_state_get_item_out_of_bounds(olt_state_object):
+    with pytest.raises(KeyError):
+        olt_state_object[16]
+
+
+def test_olt_state_iter(olt_state_object):
+    with pytest.raises(NotImplementedError):
+        for _ in olt_state_object:
+            pass
+
+
+def test_olt_state_contains(olt_state_object):
+    assert 5 in olt_state_object
+
+
+def test_olt_state_contains_does_not_contain(olt_state_object):
+    assert not 16 in olt_state_object
+
+
+def test_olt_state_contains_not_int(olt_state_object):
+    with pytest.raises(TypeError):
+        "something" in olt_state_object
+
+
+def test_pon_to_string(pon_object):
+    assert str(pon_object) == "OltState.Pon: pon-id: 0"
+
+
+def test_pon_properties(pon_object):
+    assert pon_object.pon_id == 0
+    assert pon_object.downstream_wavelength == 0
+    assert pon_object.upstream_wavelength == 0
+    assert pon_object.downstream_channel_id == 15
+    assert pon_object.rx_packets == 1625773517
+    assert pon_object.tx_packets == 761098346
+    assert pon_object.rx_bytes == 145149613233620
+    assert pon_object.tx_bytes == 141303797318481
+    assert pon_object.tx_bip_errors == 0
+    assert pon_object.ont_los == []
+    assert pon_object.discovered_onu == frozenset()
+    assert pon_object.wm_tuned_out_onus == "AAAAAAAAAAAAAAAAAAAAAA=="
+
+
+def test_pon_gems(pon_object):
+    assert str(pon_object.gems[2176]) == "OltState.Pon.Gem: onu-id: 0, gem-id: 2176"
+
+
+def test_pon_gems_existing(pon_object):
+    pon_object._gems = "existing"
+    assert pon_object.gems == "existing"
+
+
+def test_pon_onus(pon_object):
+    assert str(pon_object.onus[0]) == "OltState.Pon.Onu: onu-id: 0"
+
+
+def test_pon_onus_existing(pon_object):
+    pon_object._onus = "existing"
+    assert pon_object.onus == "existing"
+
+
+def test_onu_properties(onu_object):
+    assert onu_object.onu_id == 0
+    assert onu_object.oper_status == "unknown"
+    assert onu_object.reported_password == "redacted"
+    assert onu_object.rssi == -207
+    assert onu_object.equalization_delay == 620952
+    assert onu_object.fiber_length == 47
+
+
+def test_gem_properties(gem_object):
+    assert gem_object.onu_id == 0
+    assert gem_object.alloc_id == 1024
+    assert gem_object.gem_id == 2176
+    assert gem_object.tx_packets == 65405
+    assert gem_object.tx_bytes == 5420931
+    assert gem_object.rx_packets == 13859
+    assert gem_object.rx_bytes == 3242784
+
+
+
+
diff --git a/voltha/adapters/adtran_olt/test/codec/test_physical_entities_state.py b/voltha/adapters/adtran_olt/test/codec/test_physical_entities_state.py
new file mode 100644
index 0000000..db76f50
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/codec/test_physical_entities_state.py
@@ -0,0 +1,91 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mock
+import pytest
+import pytest_twisted
+
+from xmltodict import OrderedDict
+from resources.physical_entities_state_xml import test_xml, physical_entities_output
+
+from voltha.adapters.adtran_olt.codec.physical_entities_state import PhysicalEntitiesState
+
+
+class MockRPCReply(object):
+    def __init__(self, data_xml):
+        self.data_xml = data_xml
+
+
+expected_ordered_dict_output = [
+    OrderedDict(
+        [(u'name', u'temperature 0/1'),
+         (u'availability', OrderedDict(
+             [(u'@xmlns', u'http://www.adtran.com/ns/yang/adtran-physical-entity-availability'),
+              (u'availability-status', None)]
+         )),
+         (u'classification', OrderedDict(
+             [(u'@xmlns:adtn-phys-sens', u'http://www.adtran.com/ns/yang/adtran-physical-sensors'),
+              ('#text', u'adtn-phys-sens:temperature-sensor-celsius')])),
+              (u'is-field-replaceable', u'false')
+         ]
+    )
+]
+
+
+@pytest.fixture()
+def mock_session():
+    return mock.MagicMock()
+
+
+@pytest.fixture()
+def pes_object(mock_session):
+    return PhysicalEntitiesState(mock_session)
+
+
+@pytest_twisted.inlineCallbacks
+def test_get_state(mock_session, pes_object):
+    mock_session.get.return_value = "<some>xml</some>"
+    output = yield pes_object.get_state()
+    assert output == "<some>xml</some>"
+
+
+def test_physical_entities_no_reply_data(pes_object):
+    assert pes_object.physical_entities is None
+
+
+def test_physical_entities(pes_object):
+    pes_object._rpc_reply = MockRPCReply(test_xml)
+    assert pes_object.physical_entities == OrderedDict([('a-string', 'something')])
+
+
+def test_get_physical_entities_no_classification(pes_object):
+    pes_object._rpc_reply = MockRPCReply(test_xml)
+    assert pes_object.get_physical_entities() == OrderedDict([('a-string', 'something')])
+
+
+def test_get_physical_entities_no_matching_classification(pes_object):
+    pes_object._rpc_reply = MockRPCReply(test_xml)
+    assert pes_object.get_physical_entities("test-classification") == []
+
+
+def test_get_physical_entities(pes_object):
+    pes_object._rpc_reply = MockRPCReply(physical_entities_output)
+    output = pes_object.get_physical_entities("adtn-phys-sens:temperature-sensor-celsius")
+    assert output == expected_ordered_dict_output
+
+
+def test_get_physical_entities_with_list(pes_object):
+    pes_object._rpc_reply = MockRPCReply(physical_entities_output)
+    output = pes_object.get_physical_entities(["adtn-phys-sens:temperature-sensor-celsius", "another_classification"])
+    assert output == expected_ordered_dict_output
diff --git a/voltha/adapters/adtran_olt/test/flow/__init__.py b/voltha/adapters/adtran_olt/test/flow/__init__.py
new file mode 100644
index 0000000..18d64b2
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/flow/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
diff --git a/voltha/adapters/adtran_olt/test/flow/test_evc.py b/voltha/adapters/adtran_olt/test/flow/test_evc.py
new file mode 100644
index 0000000..bf2c092
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/flow/test_evc.py
@@ -0,0 +1,198 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 collections import namedtuple
+import pytest_twisted
+import pytest
+from voltha.adapters.adtran_olt.flow.evc import EVC
+from mock import MagicMock, patch
+from twisted.internet import reactor, defer
+
+
+@pytest.fixture()
+def flow():
+    Flow = namedtuple('Flow', 'flow_id')
+    return Flow(1)
+
+
+@pytest.fixture()
+def evc_log():
+    with patch('voltha.adapters.adtran_olt.flow.evc.log') as log:
+        yield log
+
+
+@pytest.fixture()
+def vanilla_evc(flow, evc_log):
+    yield EVC(flow)
+
+
+def test_evc_repr(vanilla_evc):
+    assert str(vanilla_evc) == "EVC-VOLTHA-1: MEN: [], S-Tag: None"
+
+
+def test_evc_stpid(vanilla_evc):
+    vanilla_evc.stpid = None
+    vanilla_evc.stpid = 0x8100
+    with pytest.raises(AssertionError):
+        vanilla_evc.stpid = 0x9100
+    with pytest.raises(AssertionError):
+        vanilla_evc.stpid = None
+
+
+def test_evc_contains_evc_maps(vanilla_evc):
+    EvcMap = namedtuple('EvcMap', 'name')
+    testMap = EvcMap('test-evc-map')
+    assert len(vanilla_evc.evc_maps) is 0
+    assert len(vanilla_evc.evc_map_names) is 0
+
+    vanilla_evc.add_evc_map(testMap)
+    assert len(vanilla_evc.evc_maps) is 1
+    assert len(vanilla_evc.evc_map_names) is 1
+
+    vanilla_evc._evc_maps = None
+    vanilla_evc.add_evc_map(testMap)
+    vanilla_evc.add_evc_map(testMap)
+    assert len(vanilla_evc.evc_maps) is 1
+    assert len(vanilla_evc.evc_map_names) is 1
+
+    vanilla_evc.remove_evc_map(testMap)
+    vanilla_evc.remove_evc_map(EvcMap('evc-map-not-in-there'))
+    assert len(vanilla_evc.evc_maps) is 0
+    assert len(vanilla_evc.evc_map_names) is 0
+
+
+@pytest.mark.parametrize('falsey', (
+    [], {}, False, 0, None
+))
+def test_set_installed(falsey, vanilla_evc):
+    with pytest.raises(AssertionError):
+        vanilla_evc.installed = 'abc'
+    vanilla_evc.installed = falsey
+    assert vanilla_evc.installed is False
+
+
+def test_status_prop(vanilla_evc):
+    assert None is vanilla_evc.status
+    vanilla_evc.status = 'why is this settable?'
+    assert None is not vanilla_evc.status
+
+
+def test_switch_method_prop(vanilla_evc):
+    assert None is vanilla_evc.switching_method
+
+
+def test_men_2_uni_manip_prop(vanilla_evc):
+    assert None is vanilla_evc.men_to_uni_tag_manipulation
+
+
+def test_flow_prop(vanilla_evc, flow):
+    assert flow is vanilla_evc.flow_entry
+    vanilla_evc.flow_entry = 'New Value'
+    assert 'New Value' == vanilla_evc.flow_entry
+
+
+@pytest.mark.parametrize('value, expected', [
+    (None, '<single-tag-switched/>'),
+    (EVC.SwitchingMethod.SINGLE_TAGGED, '<single-tag-switched/>'),
+    (EVC.SwitchingMethod.DOUBLE_TAGGED, '<double-tag-switched/>'),
+    (EVC.SwitchingMethod.MAC_SWITCHED, '<mac-switched/>'),
+    (EVC.SwitchingMethod.DOUBLE_TAGGED_MAC_SWITCHED, '<double-tag-mac-switched/>'),
+    ('invalid', ValueError)
+])
+def test_evc_switching_method_xml(value, expected):
+    if isinstance(expected, str):
+        assert EVC.SwitchingMethod.xml(value) == expected
+    else:
+        with pytest.raises(expected):
+            EVC.SwitchingMethod.xml(value)
+
+
+@pytest.mark.parametrize('value, expected', [
+    (None, '<symmetric/>'),
+    (EVC.Men2UniManipulation.SYMMETRIC, '<symmetric/>'),
+    (EVC.Men2UniManipulation.POP_OUT_TAG_ONLY, '<pop-outer-tag-only/>'),
+    ('invalid', ValueError)
+])
+def test_evc_men_2_uni_manip(value, expected):
+    if isinstance(expected, str):
+        xml = '<men-to-uni-tag-manipulation>%s</men-to-uni-tag-manipulation>' % expected
+        assert EVC.Men2UniManipulation.xml(value) == xml
+    else:
+        with pytest.raises(expected):
+            EVC.Men2UniManipulation.xml(value)
+
+
+@pytest_twisted.inlineCallbacks
+def test_evc_do_simple_install():
+    flow = MagicMock()
+    flow.flow_id = 1
+    flow.vlan_id = 2
+    flow.handler.get_port_name = lambda _: 'nni'
+    evc = EVC(flow)
+
+    # TEST Pre-Conditions
+    assert flow.handler.netconf_client.edit_config.call_args is None
+    assert not evc.installed
+    d = evc._do_install()
+
+    def callback(result):
+        assert result is True
+        assert evc.installed
+
+        xml = """
+<evcs xmlns="http://www.adtran.com/ns/yang/adtran-evcs">
+<evc>
+<name>VOLTHA-1</name>
+<enabled>true</enabled>
+<stag>2</stag>
+<stag-tpid>33024</stag-tpid>
+<men-ports>nni</men-ports>
+</evc>
+</evcs>""".replace('\n', '')
+        flow.handler.netconf_client.edit_config.assert_called_with(xml)
+    d.addCallback(callback)
+    yield d
+
+
+@pytest.mark.parametrize('evcs', [
+    ['VOLTHA-1'], ['VOLTHA-1', 'VOLTHA-2']
+])
+@pytest_twisted.inlineCallbacks
+def test_evc_do_remove(evcs):
+    def get_evc_response():
+        d = defer.Deferred()
+        Reply = namedtuple('Reply', ['ok', 'data_xml'])
+        reactor.callLater(0.1, d.callback, Reply(True, (
+            '<data><evcs>' +
+            ''.join(('<evc><name>%s</name></evc>' % n) for n in evcs) +
+            '</evcs></data>')))
+        return d
+
+    client = MagicMock()
+    client.get.return_value = get_evc_response()
+    result = yield EVC.remove_all(client)
+
+    assert result is None
+    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>'''
+    ).replace('\n', '')
+    delete_xml = (
+        '''<evcs xmlns="http://www.adtran.com/ns/yang/adtran-evcs" xc:operation="delete">''' +
+        ''.join(('''<evc><name>%s</name></evc>''' % n) for n in evcs) +
+        '''</evcs>'''
+    ).replace('\n', '')
+    client.get.assert_called_with(get_xml)
+    client.edit_config.assert_called_with(delete_xml)
diff --git a/voltha/adapters/adtran_olt/test/flow/test_evc_map.py b/voltha/adapters/adtran_olt/test/flow/test_evc_map.py
new file mode 100644
index 0000000..d1bb531
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/flow/test_evc_map.py
@@ -0,0 +1,81 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 voltha.adapters.adtran_olt.flow.evc_map import EVCMap
+from mock import MagicMock
+import pytest
+
+
+
+## This section test the proberties of the class EVCMap
+
+def test_EVCMap_properties():
+    flow = MagicMock()
+    flow.logical_port = 100
+    flow.flow_id = 200
+    testmap = EVCMap(flow,300,False)
+    assert testmap.valid == False
+    assert testmap.installed == False
+    assert testmap.name == 'VOLTHA-100-200'
+    assert testmap.evc == None
+    assert testmap._needs_acl_support == False
+    assert testmap.pon_id == None
+    assert testmap.onu_id == None
+    assert testmap.gem_ids_and_vid == {}
+
+
+    # TODO: needs_update property could use refactoring
+    assert testmap.needs_update == False
+
+    # setting the private _needs_update variable != falsey
+    testmap._needs_update = 'update_true'
+    assert testmap.needs_update == 'update_true'
+
+    # testing that the setter only operates on Falsey arg
+    testmap.needs_update = ''
+    assert testmap.needs_update == False
+
+    # testing that it does not allow Truthy things
+    with pytest.raises(AssertionError):
+        testmap.needs_update = 1
+
+
+##  This section to test static methods of the class EVCMap
+
+def test_create_ingress_map():
+    flow = MagicMock()
+    flow.logical_port = 101
+    flow.flow_id = 201
+    evc = MagicMock()
+    dry_run = False
+    emap = EVCMap.create_ingress_map(flow, evc, dry_run)
+    assert isinstance(emap, EVCMap)
+    evc.add_evc_map.assert_called_once_with(emap)
+    assert emap.evc == evc
+
+
+def test_create_egress_map():
+    flow = MagicMock()
+    flow.logical_port = 102
+    flow.flow_id = 202
+    evc = MagicMock()
+    dry_run = False
+    imap = EVCMap.create_egress_map(flow, evc, dry_run)
+    assert isinstance(imap, EVCMap)
+    evc.add_evc_map.assert_called_once_with(imap)
+    assert imap.evc == evc
+
+
+
diff --git a/voltha/adapters/adtran_olt/test/flow/test_flow_entry.py b/voltha/adapters/adtran_olt/test/flow/test_flow_entry.py
new file mode 100644
index 0000000..b307a09
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/flow/test_flow_entry.py
@@ -0,0 +1,156 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+from collections import namedtuple
+from voltha.adapters.adtran_olt.adtran_device_handler import DEFAULT_MULTICAST_VLAN
+from voltha.adapters.adtran_olt.adtran_olt_handler import *
+from voltha.adapters.adtran_olt.flow.flow_entry import *
+from voltha.adapters.adtran_olt.flow.flow_tables import DownstreamFlows, DeviceFlows
+from voltha.core.flow_decomposer import mk_flow_stat
+import mock
+
+
+@pytest.fixture()
+def downstream():
+    yield mk_flow_stat(
+        priority=40000,
+        match_fields=[
+            in_port(1),
+            vlan_vid(ofp.OFPVID_PRESENT | 4),
+            vlan_pcp(7),
+            metadata(666)
+        ],
+        actions=[
+            pop_vlan(),
+            output(5)
+        ]
+    )
+
+
+@pytest.fixture()
+def flow_handler():
+    handler = mock.MagicMock(spec=AdtranOltHandler)
+    handler.device_id = 9876543210
+    handler.multicast_vlans = [DEFAULT_MULTICAST_VLAN]
+    handler.northbound_ports = {1}
+    handler.southbound_ports = {}
+    handler.utility_vlan = 0
+    handler.downstream_flows = DownstreamFlows()
+    handler.upstream_flows = DeviceFlows()
+    handler.is_nni_port = lambda n: n in handler.northbound_ports
+    handler.is_pon_port = lambda n: not handler.is_nni_port(n)
+    handler.get_port_name = lambda n: "mock-{} 0/{}".format(
+        "uni" if n not in handler.northbound_ports else "nni",
+        n)
+    yield handler
+
+
+@pytest.mark.parametrize('decoder', [
+    '_decode',
+    '_decode_flow_direction',
+    '_decode_traffic_treatment',
+    '_decode_traffic_selector'
+])
+def test_create_fails_decode(flow_handler, decoder, downstream):
+    with mock.patch(
+            'voltha.adapters.adtran_olt.flow.flow_entry.FlowEntry.%s' % decoder,
+            return_value=False) as mock_decode:
+        assert (None, None) == FlowEntry.create(downstream, flow_handler)
+    mock_decode.assert_called_once()
+
+
+def test_empty_flow_signature(flow_handler):
+    Flow = namedtuple('Flow', 'id')
+    flow = FlowEntry(Flow(1), flow_handler)
+    with pytest.raises(AssertionError):
+        _ = flow.signature
+    flow.in_port, flow.output = 1, 10
+    assert flow.signature is None
+
+
+@pytest.mark.parametrize('direction, expected', [
+    ('downstream', '1.*.2048.*'),
+    ('upstream', '1.10.2048.2')
+])
+def test_flow_signature(flow_handler, direction, expected):
+    Flow = namedtuple('Flow', 'id')
+    flow = FlowEntry(Flow(1), flow_handler)
+    flow.in_port, flow.output = 1, 10
+    flow.vlan_id, flow.inner_vid = 2048, 2
+    if direction == 'downstream':
+        flow._flow_direction = FlowEntry.FlowDirection.DOWNSTREAM
+    else:
+        flow._flow_direction = FlowEntry.FlowDirection.UPSTREAM
+    assert expected == flow.signature
+    assert expected == flow.signature
+
+
+@pytest.mark.parametrize('direction', [
+    'downstream', 'upstream', 'multicast'
+])
+def test_create_failures(flow_handler, direction):
+    def mock_decode(self, _):
+        expected['flow_entry'] = self
+        self.in_port, self.output = 1, 2
+        self.vlan_id, self.inner_vid = 3, 4
+        if direction == 'upstream':
+            self._flow_direction = FlowEntry.FlowDirection.UPSTREAM
+            flow_handler.upstream_flows.add(self)
+        elif direction == 'downstream':
+            self._flow_direction = FlowEntry.FlowDirection.DOWNSTREAM
+            flow_handler.downstream_flows.add(self.signature).flows.add(self)
+        elif direction == 'multicast':
+            self._flow_direction = FlowEntry.FlowDirection.DOWNSTREAM
+            self._is_multicast = True
+            sig_table = flow_handler.downstream_flows.add(self.signature)
+            expected['evc'] = sig_table.evc = EVC(self)
+        return True
+
+    expected = {'flow_entry': None, 'evc': None}
+    Flow = namedtuple('Flow', 'id')
+    with mock.patch(
+            'voltha.adapters.adtran_olt.flow.flow_entry.FlowEntry._decode',
+            mock_decode):
+        flow_entry, evc = FlowEntry.create(Flow(5), flow_handler)
+    assert flow_entry is expected['flow_entry']
+    assert evc is expected['evc']
+
+
+def test_create(flow_handler, caplog, downstream):
+    ds_entry, ds_evc = FlowEntry.create(downstream, flow_handler)
+    assert ds_entry is not None, "Entry wasn't created"
+    assert ds_evc is None, "EVC not labeled"
+
+    upstream = mk_flow_stat(priority=40000,
+                            match_fields=[
+                                in_port(5),
+                                vlan_vid(ofp.OFPVID_PRESENT | 666),
+                                vlan_pcp(7)
+                            ],
+                            actions=[
+                                push_vlan(0x8100),
+                                set_field(vlan_vid(ofp.OFPVID_PRESENT | 4)),
+                                set_field(vlan_pcp(7)),
+                                output(1)
+                            ])
+    us_entry, us_evc = FlowEntry.create(upstream, flow_handler)
+    assert us_entry is not None, "Entry wasn't created"
+    assert us_evc is not None, "EVC not labeled"
+    us_evc._do_install()
+    assert us_evc._installed, "EVC wasn't installed"
+    edit_configs = flow_handler.netconf_client.edit_config.call_args_list
+    assert len(edit_configs) == 1, "EVC-MAP edit config"
+    for call in edit_configs:
+        log.info("Netconf Calls: {}".format(call))
diff --git a/voltha/adapters/adtran_olt/test/flow/test_flow_tables.py b/voltha/adapters/adtran_olt/test/flow/test_flow_tables.py
new file mode 100644
index 0000000..d2f737e
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/flow/test_flow_tables.py
@@ -0,0 +1,90 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+from collections import namedtuple
+from voltha.adapters.adtran_olt.flow.flow_entry import FlowEntry
+from voltha.adapters.adtran_olt.flow.flow_tables import DeviceFlows, DownstreamFlows
+Flow = namedtuple('Flow', 'id')
+Handler = namedtuple('Handler', 'device_id')
+
+
+def test_device_flows_init():
+    DeviceFlows()
+
+
+def test_storage_class():
+    d = DeviceFlows()
+    d._keytransform = super(type(d), d)._keytransform
+    with pytest.raises(NotImplementedError):
+        d.get(1)
+
+
+def test_device_flows_good_access():
+    d = DeviceFlows()
+    a_flow_entry = FlowEntry(Flow(123), Handler('dev'))
+    if 123 not in d:
+        d[123] = a_flow_entry
+    assert 123 in d
+    assert d.get(123, None) is a_flow_entry
+    assert len(d) is 1
+    assert d.remove(123) is a_flow_entry
+    assert d.get(123, None) is None
+    d.add(a_flow_entry)
+    del d[123]
+
+
+def test_device_flows_add():
+    d = DeviceFlows()
+    a_flow_entry = FlowEntry(Flow(1), Handler('dev'))
+    assert a_flow_entry is d.add(a_flow_entry)
+    assert a_flow_entry is d.add(a_flow_entry)
+
+
+def test_device_flows_bad_access_init():
+    with pytest.raises(AssertionError):
+        DeviceFlows(a=1, b=2)
+
+
+def test_device_flows_bad_access_key():
+    d = DeviceFlows()
+    with pytest.raises(AssertionError):
+        d['abc'] = 123
+    with pytest.raises(KeyError):
+        del d[123]
+
+
+def test_device_flows_bad_access_value():
+    d = DeviceFlows()
+    with pytest.raises(AssertionError):
+        d[123] = 'abc'
+
+
+def test_downstream_flows_init():
+    DownstreamFlows()
+
+
+def test_downstream_flows_good_access():
+    d = DownstreamFlows()
+    key = 'test-sig'
+    a_test_sig = d.add(key)
+    if key not in d:
+        d[key] = a_test_sig
+    assert d.get(key, None) is a_test_sig
+    assert len(d) is 1
+    assert d.remove(key) is a_test_sig
+    assert d.get(key, None) is None
+    d.add(a_test_sig)
+    del d[key]
+
diff --git a/voltha/adapters/adtran_olt/test/net/__init__.py b/voltha/adapters/adtran_olt/test/net/__init__.py
new file mode 100644
index 0000000..18d64b2
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/net/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
diff --git a/voltha/adapters/adtran_olt/net/mock_netconf_client.py b/voltha/adapters/adtran_olt/test/net/mock_netconf_client.py
similarity index 79%
rename from voltha/adapters/adtran_olt/net/mock_netconf_client.py
rename to voltha/adapters/adtran_olt/test/net/mock_netconf_client.py
index 59410a5..a2b18a8 100644
--- a/voltha/adapters/adtran_olt/net/mock_netconf_client.py
+++ b/voltha/adapters/adtran_olt/test/net/mock_netconf_client.py
@@ -15,9 +15,10 @@
 import structlog
 import random
 import time
-from adtran_netconf import AdtranNetconfClient
+from voltha.adapters.adtran_olt.net.adtran_netconf import AdtranNetconfClient
 from common.utils.asleep import asleep
 from ncclient.operations.rpc import RPCReply, RPCError
+from ncclient.operations.retrieve import GetReply
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 log = structlog.get_logger()
@@ -28,6 +29,7 @@
       '<data/>' + \
       '</rpc-reply>'
 
+
 class MockNetconfClient(AdtranNetconfClient):
     """
     Performs NETCONF requests
@@ -69,7 +71,7 @@
 
         :return: (deferred) Deferred request
         """
-        yield asleep(random.uniform(0.1, 5.0))   # Simulate NETCONF request delay
+        yield asleep(random.uniform(0.01, 0.05))   # Simulate NETCONF request delay
         self._connected = True
         self._locked = {}
         returnValue(True)
@@ -80,7 +82,7 @@
         Close the connection to the NETCONF server
         :return:  (deferred) Deferred request
         """
-        yield asleep(random.uniform(0.1, 0.5))   # Simulate NETCONF request delay
+        yield asleep(random.uniform(0.01, 0.05))   # Simulate NETCONF request delay
         self._connected = False
         self._locked = {}
         returnValue(True)
@@ -93,11 +95,11 @@
         :param source: (string) Configuration source, 'running', 'candidate', ...
         :return: (deferred) Deferred request that wraps the GetReply class
         """
-        yield asleep(random.uniform(0.1, 4.0))   # Simulate NETCONF request delay
+        yield asleep(random.uniform(0.01, 0.04))   # Simulate NETCONF request delay
 
         # TODO: Customize if needed...
         xml = _dummy_xml
-        returnValue(RPCReply(xml))
+        returnValue(GetReply(xml))
 
     @inlineCallbacks
     def get(self, payload):
@@ -105,13 +107,13 @@
         Get the requested data from the server
 
         :param payload: Payload/filter
-        :return: (defeered) for GetReply
+        :return: (deferred) for GetReply
         """
-        yield asleep(random.uniform(0.1, 3.0))   # Simulate NETCONF request delay
+        yield asleep(random.uniform(0.01, 0.03))   # Simulate NETCONF request delay
 
         # TODO: Customize if needed...
         xml = _dummy_xml
-        returnValue(RPCReply(xml))
+        returnValue(GetReply(xml))
 
     @inlineCallbacks
     def lock(self, source, lock_timeout):
@@ -134,7 +136,7 @@
             yield asleep(0.1)
 
         if time.time() < expire_time:
-            yield asleep(random.uniform(0.1, 0.5))   # Simulate NETCONF request delay
+            yield asleep(random.uniform(0.01, 0.05))   # Simulate NETCONF request delay
             self._locked[source] = expire_time
 
         returnValue(RPCReply(_dummy_xml) if expire_time > time.time() else RPCError('TODO'))
@@ -151,14 +153,14 @@
             self._locked[source] = None
 
         if self._locked[source] is not None:
-            yield asleep(random.uniform(0.1, 0.5))   # Simulate NETCONF request delay
+            yield asleep(random.uniform(0.01, 0.05))   # Simulate NETCONF request delay
 
         self._locked[source] = None
         returnValue(RPCReply(_dummy_xml))
 
     @inlineCallbacks
     def edit_config(self, config, target='running', default_operation='merge',
-                    test_option=None, error_option=None):
+                    test_option=None, error_option=None, ignore_delete_error=False):
         """
         Loads all or part of the specified config to the target configuration datastore with the ability to lock
         the datastore during the edit.
@@ -170,13 +172,18 @@
         :param test_option if specified must be one of { 'test_then_set', 'set' }
         :param error_option if specified must be one of { 'stop-on-error', 'continue-on-error', 'rollback-on-error' }
                             The 'rollback-on-error' error_option depends on the :rollback-on-error capability.
-
-        :return: (defeered) for RpcReply
+        :param ignore_delete_error: (bool) For some startup deletes/clean-ups, we do a
+                                    delete high up in the config to get whole lists. If
+                                    these lists are empty, this helps suppress any error
+                                    message from NETConf on failure to delete an empty list
+        :return: (deferred) for RpcReply
         """
         try:
-            yield asleep(random.uniform(0.1, 2.0))  # Simulate NETCONF request delay
+            yield asleep(random.uniform(0.01, 0.02))  # Simulate NETCONF request delay
 
         except Exception as e:
+            if ignore_delete_error and 'operation="delete"' in config.lower():
+                returnValue('ignoring-delete-error')
             log.exception('edit_config', e=e)
             raise
 
@@ -191,7 +198,7 @@
         :param rpc_string: (string) RPC request
         :return: (defeered) for GetReply
         """
-        yield asleep(random.uniform(0.1, 2.0))   # Simulate NETCONF request delay
+        yield asleep(random.uniform(0.01, 0.02))   # Simulate NETCONF request delay
 
         # TODO: Customize if needed...
         xml = _dummy_xml
diff --git a/voltha/adapters/adtran_olt/test/net/test_adtran_netconf.py b/voltha/adapters/adtran_olt/test/net/test_adtran_netconf.py
new file mode 100644
index 0000000..0615b7d
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/net/test_adtran_netconf.py
@@ -0,0 +1,402 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mock
+import pytest
+import pytest_twisted
+
+from ncclient.transport.errors import SSHError
+from ncclient.operations import RPCError
+from voltha.adapters.adtran_olt.net import adtran_netconf
+
+
+
+@pytest.fixture()
+def test_client():
+    return adtran_netconf.AdtranNetconfClient("1.2.3.4", 830, "username", "password")
+
+
+@pytest.fixture(autouse=True)
+def mock_manager():
+    old_manager = adtran_netconf.manager
+    adtran_netconf.manager = mock.MagicMock()
+    yield adtran_netconf.manager
+    adtran_netconf.manager = old_manager
+
+
+@pytest.fixture(autouse=True)
+def mock_logger():
+    with mock.patch("voltha.adapters.adtran_olt.net.adtran_netconf.log") as temp_mock:
+        yield temp_mock
+
+
+def test_adtran_module_url():
+    assert adtran_netconf.adtran_module_url("adtran-physical-entities") == "http://www.adtran.com/ns/yang/adtran-physical-entities"
+
+
+def test_phys_entities_rpc():
+    expected_out = """
+    <filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+      <physical-entities-state xmlns="http://www.adtran.com/ns/yang/adtran-physical-entities">
+        <physical-entity/>
+      </physical-entities-state>
+    </filter>
+    """
+    assert adtran_netconf.phys_entities_rpc() == expected_out
+
+
+def test_adtran_netconf_client_to_string(test_client):
+    assert str(test_client) == "AdtranNetconfClient username@1.2.3.4:830"
+
+
+def test_do_connect(test_client, mock_manager):
+    mock_manager.connect.return_value = "This is good"
+    assert "This is good" == test_client._do_connect(10)
+    mock_manager.connect.assert_called_once_with(host="1.2.3.4",
+                                                 port=830,
+                                                 username="username",
+                                                 password="password",
+                                                 allow_agent=False,
+                                                 look_for_keys=False,
+                                                 hostkey_verify=False,
+                                                 timeout=10)
+
+
+def test_capabilities(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.server_capabilities = "Here's what I do...."
+    assert "Here's what I do...." == test_client.capabilities
+
+
+def test_connected(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.connected = True
+    assert test_client.connected
+
+
+def test_do_connect_with_ssh_error(test_client, mock_manager):
+    mock_manager.connect.side_effect = SSHError()
+    with pytest.raises(SSHError):
+        test_client._do_connect(10)
+
+
+def test_do_connect_with_literally_any_exception(test_client, mock_manager):
+    mock_manager.connect.side_effect = SyntaxError()
+    with pytest.raises(SyntaxError):
+        test_client._do_connect(10)
+
+
+def test_do_connect_reset_log_level(test_client, mock_manager, mock_logger):
+    mock_logger.isEnabledFor.return_value = True
+    test_client._do_connect(10)
+    mock_logger.setLevel.assert_called_once_with("INFO")
+
+
+def test_do_connect_dont_reset_log_level(test_client, mock_manager, mock_logger):
+    mock_logger.isEnabledFor.return_value = False
+    test_client._do_connect(10)
+    assert mock_logger.setLevel.call_count == 0
+
+
+@pytest_twisted.inlineCallbacks
+def test_connect(test_client, mock_manager):
+    mock_manager.connect.return_value = "This is good"
+    output = yield test_client.connect(10)
+    assert "This is good" == output
+
+
+def test_do_close(test_client, mock_manager):
+    mock_session = mock.MagicMock()
+    test_client._do_close(mock_session)
+    mock_session.close_session.assert_called_once_with()
+
+
+@pytest_twisted.inlineCallbacks
+def test_close(test_client):
+    mock_session = mock.MagicMock()
+    test_client._session = mock_session
+    mock_session.connected = True
+    yield test_client.close()
+    mock_session.close_session.assert_called_once_with()
+
+
+@pytest_twisted.inlineCallbacks
+def test_close_not_connected(test_client):
+    mock_session = mock.MagicMock()
+    test_client._session = mock_session
+    mock_session.connected = False
+    output = yield test_client.close()
+    assert output
+
+
+@pytest_twisted.inlineCallbacks
+def test_reconnect(test_client):
+    with mock.patch.object(test_client, "connect") as mock_connect:
+        with mock.patch.object(test_client, "close") as mock_close:
+            yield test_client._reconnect()
+            mock_connect.assert_called_once()
+            mock_close.assert_called_once()
+
+
+@pytest_twisted.inlineCallbacks
+def test_reconnect_ignore_errors(test_client):
+    with mock.patch.object(test_client, "connect") as mock_connect:
+        with mock.patch.object(test_client, "close") as mock_close:
+            mock_connect.side_effect = SyntaxError()
+            mock_close.side_effect = SyntaxError()
+            yield test_client._reconnect()
+            mock_connect.assert_called_once()
+            mock_close.assert_called_once()
+
+
+def test_do_get_config(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._do_get_config("running")
+    test_client._session.get_config.assert_called_once_with("running")
+
+
+@pytest_twisted.inlineCallbacks
+def test_get_config(test_client):
+    test_client._session = mock.MagicMock()
+    yield test_client.get_config()
+    test_client._session.get_config.assert_called_once_with("running")
+
+
+@pytest_twisted.inlineCallbacks
+def test_get_config_with_no_session(test_client):
+    test_client._session = None
+    with pytest.raises(NotImplementedError):
+        yield test_client.get_config()
+
+
+@pytest_twisted.inlineCallbacks
+def test_get_config_session_not_connected(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.connected = False
+    with mock.patch.object(test_client, "_reconnect") as mock_reconnect:
+        yield test_client.get_config()
+        mock_reconnect.assert_called_once()
+
+
+def test_do_get(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.get.return_value = "<some>xml</some>"
+    assert test_client._do_get("<get>xml</get>") == "<some>xml</some>"
+    test_client._session.get.assert_called_once_with("<get>xml</get>")
+
+
+def test_do_get_rpc_error(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.get.side_effect = RPCError(mock.MagicMock())
+    with pytest.raises(RPCError):
+        test_client._do_get("<get>xml</get>")
+
+
+@pytest_twisted.inlineCallbacks
+def test_get(test_client):
+    test_client._session = mock.MagicMock()
+    yield test_client.get("<get>xml</get>")
+    test_client._session.get.assert_called_once_with("<get>xml</get>")
+
+
+@pytest_twisted.inlineCallbacks
+def test_get_with_no_session(test_client):
+    test_client._session = None
+    with pytest.raises(NotImplementedError):
+        yield test_client.get("<get>xml</get>")
+
+
+@pytest_twisted.inlineCallbacks
+def test_get_session_not_connected(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.connected = False
+    with mock.patch.object(test_client, "_reconnect") as mock_reconnect:
+        yield test_client.get("<get>xml</get>")
+        mock_reconnect.assert_called_once()
+
+
+def test_do_lock(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.lock.return_value = "<ok>"
+    assert test_client._do_lock("running", 10) == "<ok>"
+    test_client._session.lock.assert_called_once_with("running", timeout=10)
+
+
+def test_do_lock_rpc_error(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.lock.side_effect = RPCError(mock.MagicMock())
+    with pytest.raises(RPCError):
+        test_client._do_lock("running", 10)
+
+
+@pytest_twisted.inlineCallbacks
+def test_lock(test_client):
+    test_client._session = mock.MagicMock()
+    yield test_client.lock("running", 10)
+    test_client._session.lock.assert_called_once_with("running", timeout=10)
+
+
+@pytest_twisted.inlineCallbacks
+def test_lock_with_no_session(test_client):
+    test_client._session = None
+    with pytest.raises(NotImplementedError):
+        yield test_client.lock("running", 10)
+
+
+def test_do_unlock(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.unlock.return_value = "<ok>"
+    assert test_client._do_unlock("running") == "<ok>"
+    test_client._session.unlock.assert_called_once_with("running")
+
+
+def test_do_unlock_rpc_error(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.unlock.side_effect = RPCError(mock.MagicMock())
+    with pytest.raises(RPCError):
+        test_client._do_unlock("running")
+
+
+@pytest_twisted.inlineCallbacks
+def test_unlock(test_client):
+    test_client._session = mock.MagicMock()
+    yield test_client.unlock("running")
+    test_client._session.unlock.assert_called_once_with("running")
+
+
+@pytest_twisted.inlineCallbacks
+def test_unlock_with_no_session(test_client):
+    test_client._session = None
+    with pytest.raises(NotImplementedError):
+        yield test_client.unlock("running")
+
+
+def test_do_edit_config(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.edit_config.return_value = "<ok>"
+    assert test_client._do_edit_config("running", "<some>config</some>") == "<ok>"
+    test_client._session.edit_config.assert_called_once_with(target="running", config="<some>config</some>")
+
+
+def test_do_edit_config_rpc_error_and_ignore_delete_error(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.edit_config.side_effect = RPCError(mock.MagicMock())
+    with pytest.raises(RPCError):
+        test_client._do_edit_config("running", 'operation="delete"', ignore_delete_error=True)
+
+
+def test_do_edit_config_rpc_error(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.edit_config.side_effect = RPCError(mock.MagicMock())
+    with pytest.raises(RPCError):
+        test_client._do_edit_config("running", "")
+
+
+@pytest_twisted.inlineCallbacks
+def test_edit_config_with_no_session(test_client):
+    test_client._session = None
+    with pytest.raises(NotImplementedError):
+        yield test_client.edit_config("<some>config</some>")
+
+
+@pytest_twisted.inlineCallbacks
+def test_edit_config_session_not_connected(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.connected = False
+    with mock.patch.object(test_client, "_reconnect") as mock_reconnect:
+        yield test_client.edit_config("<some>config</some>")
+        mock_reconnect.assert_called_once()
+
+
+@pytest_twisted.inlineCallbacks
+def test_edit_config_session_reconnect_causes_exception(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.connected = False
+    with mock.patch.object(test_client, "_reconnect") as mock_reconnect:
+        mock_reconnect.side_effect = SyntaxError()
+        yield test_client.edit_config("<some>config</some>")
+        mock_reconnect.assert_called_once()
+
+
+@pytest_twisted.inlineCallbacks
+def test_edit_config_with_config_at_start_of_xml(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.edit_config.return_value = "<ok>"
+    yield test_client.edit_config("<config")
+    test_client._session.edit_config.assert_called_once_with(target="running", config="<config")
+
+
+@pytest_twisted.inlineCallbacks
+def test_edit_config_without_config_at_start_of_xml(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.edit_config.return_value = "<ok>"
+    yield test_client.edit_config("")
+    test_client._session.edit_config.assert_called_once_with(
+        target="running",
+        config='<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"></config>')
+
+
+@pytest_twisted.inlineCallbacks
+def test_edit_config_with_any_exception(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.edit_config.side_effect = SyntaxError()
+    with pytest.raises(SyntaxError):
+        yield test_client.edit_config("<config")
+
+
+@pytest_twisted.inlineCallbacks
+def test_edit_config_with_any_exception_ignore_errors(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.edit_config.side_effect = SyntaxError()
+    output = yield test_client.edit_config('operation="delete"', ignore_delete_error=True)
+    assert output == 'ignoring-delete-error'
+
+
+def test_do_rpc(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.dispatch.return_value = "<ok>"
+    with mock.patch("voltha.adapters.adtran_olt.net.adtran_netconf.etree") as mock_etree:
+        assert test_client._do_rpc("<rpc>xml</rpc>") == "<ok>"
+        mock_etree.fromstring.assert_called_once_with("<rpc>xml</rpc>")
+
+
+def test_do_rpc_with_rpc_error(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.dispatch.side_effect = RPCError(mock.MagicMock())
+    with pytest.raises(RPCError):
+        test_client._do_rpc("<rpc>xml</rpc>")
+
+
+@pytest_twisted.inlineCallbacks
+def test_rpc(test_client):
+    test_client._session = mock.MagicMock()
+    with mock.patch("voltha.adapters.adtran_olt.net.adtran_netconf.etree") as mock_etree:
+        yield test_client.rpc("<rpc>xml</rpc>")
+        mock_etree.fromstring.assert_called_once_with("<rpc>xml</rpc>")
+
+
+@pytest_twisted.inlineCallbacks
+def test_rpc_with_no_session(test_client):
+    test_client._session = None
+    with pytest.raises(NotImplementedError):
+        yield test_client.rpc("<rpc>xml</rpc>")
+
+
+@pytest_twisted.inlineCallbacks
+def test_rpc_session_reconnect(test_client):
+    test_client._session = mock.MagicMock()
+    test_client._session.connected = False
+    with mock.patch.object(test_client, "_reconnect") as mock_reconnect:
+        yield test_client.rpc("<rpc>xml</rpc>")
+        mock_reconnect.assert_called_once()
diff --git a/voltha/adapters/adtran_olt/test/net/test_adtran_rest.py b/voltha/adapters/adtran_olt/test/net/test_adtran_rest.py
new file mode 100644
index 0000000..e3c9cb9
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/net/test_adtran_rest.py
@@ -0,0 +1,185 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+import pytest_twisted
+import json
+import mock
+
+from voltha.adapters.adtran_olt.net.adtran_rest import AdtranRestClient, RestInvalidResponseCode
+from twisted.internet.error import ConnectionClosed, ConnectionDone, ConnectionLost
+
+
+GET_LIKE_ARGS = {
+    "auth": ("user", "password"),
+    "timeout": 10,
+    "headers": {
+       "User-Agent": "Adtran RESTConf",
+       "Accept": ["application/json"]
+    }
+}
+
+SOME_JSON = json.dumps({"some": "json"})
+
+POST_LIKE_ARGS = {
+    "data": SOME_JSON,
+    "auth": ("user", "password"),
+    "timeout": 10,
+    "headers": {
+       "User-Agent": "Adtran RESTConf",
+       "Content-Type": "application/json",
+       "Accept": ["application/json"]
+    }
+}
+
+
+class MockResponse(object):
+    def __init__(self, code):
+        self.code = code
+        self.headers = mock.MagicMock()
+        self.content = mock.MagicMock()
+
+
+@pytest.fixture()
+def test_client():
+    return AdtranRestClient("1.2.3.4", "80", "user", "password")
+
+
+@pytest.fixture(autouse=True)
+def mock_treq():
+    with mock.patch("voltha.adapters.adtran_olt.net.adtran_rest.treq") as mock_obj:
+        yield mock_obj
+
+
+def test_adtran_rest_str(test_client):
+    assert str(test_client) == "AdtranRestClient user@1.2.3.4:80"
+
+
+def test_get_request(test_client, mock_treq):
+    test_client.request("GET", "/test/uri")
+    mock_treq.get.assert_called_once_with("http://1.2.3.4:80/test/uri", **GET_LIKE_ARGS)
+
+
+def test_post_request(test_client, mock_treq):
+    test_client.request("POST", "/test/uri", SOME_JSON)
+    mock_treq.post.assert_called_once_with("http://1.2.3.4:80/test/uri", **POST_LIKE_ARGS)
+
+
+def test_post_json_request(test_client, mock_treq):
+    test_client.request("POST", "/test/uri", json={"some": "json"})
+    mock_treq.post.assert_called_once_with("http://1.2.3.4:80/test/uri", **POST_LIKE_ARGS)
+
+
+def test_patch_request(test_client, mock_treq):
+    test_client.request("PATCH", "/test/uri", SOME_JSON)
+    mock_treq.patch.assert_called_once_with("http://1.2.3.4:80/test/uri", **POST_LIKE_ARGS)
+
+
+def test_delete_request(test_client, mock_treq):
+    test_client.request("DELETE", "/test/uri", SOME_JSON)
+    mock_treq.delete.assert_called_once_with("http://1.2.3.4:80/test/uri", **GET_LIKE_ARGS)
+
+
+@pytest_twisted.inlineCallbacks
+def test_bad_http_method(test_client):
+    with pytest.raises(NotImplementedError):
+        yield test_client.request("UPDATE", "/test/uri", SOME_JSON, is_retry=True)
+
+
+@pytest_twisted.inlineCallbacks
+def test_method_not_implemented(test_client, mock_treq):
+    mock_treq.post.side_effect = NotImplementedError()
+    with pytest.raises(NotImplementedError):
+        yield test_client.request("POST", "/test/uri", SOME_JSON, is_retry=True)
+
+
+@pytest_twisted.inlineCallbacks
+def test_connection_closed(test_client, mock_treq):
+    mock_treq.post.side_effect = ConnectionClosed()
+    output = yield test_client.request("POST", "/test/uri", SOME_JSON)
+    assert output == ConnectionClosed
+
+
+@pytest_twisted.inlineCallbacks
+def test_connection_lost(test_client, mock_treq):
+    mock_treq.post.side_effect = ConnectionLost()
+    with pytest.raises(ConnectionLost):
+        yield test_client.request("POST", "/test/uri", SOME_JSON, is_retry=True)
+
+
+@pytest_twisted.inlineCallbacks
+def test_connection_done(test_client, mock_treq):
+    mock_treq.post.side_effect = ConnectionDone()
+    with pytest.raises(ConnectionDone):
+        yield test_client.request("POST", "/test/uri", SOME_JSON, is_retry=True)
+
+
+@pytest_twisted.inlineCallbacks
+def test_literally_any_other_exception(test_client, mock_treq):
+    mock_treq.post.side_effect = SyntaxError()
+    with pytest.raises(SyntaxError):
+        yield test_client.request("POST", "/test/uri", SOME_JSON, is_retry=True)
+
+
+@pytest_twisted.inlineCallbacks
+def test_204(test_client, mock_treq):
+    mock_treq.post.side_effect = [MockResponse(204)]
+    output = yield test_client.request("POST", "/test/uri", SOME_JSON)
+    assert output is None
+
+
+@pytest_twisted.inlineCallbacks
+def test_404_on_delete(test_client, mock_treq):
+    mock_treq.delete.side_effect = [MockResponse(404)]
+    output = yield test_client.request("DELETE", "/test/uri", SOME_JSON)
+    assert output is None
+
+
+@pytest_twisted.inlineCallbacks
+def test_404_on_post(test_client, mock_treq):
+    mock_treq.post.side_effect = [MockResponse(404)]
+    with pytest.raises(RestInvalidResponseCode,
+                       message="REST POST '' request to 'http://1.2.3.4:80/test/uri' failed with status code 404"):
+        yield test_client.request("POST", "/test/uri", SOME_JSON)
+
+
+@pytest_twisted.inlineCallbacks
+def test_no_headers(test_client, mock_treq):
+    mock_resp = MockResponse(200)
+    mock_treq.post.side_effect = [mock_resp]
+    mock_resp.headers.hasHeader.return_value = False
+    with pytest.raises(Exception):
+        yield test_client.request("POST", "/test/uri", SOME_JSON)
+
+
+@pytest_twisted.inlineCallbacks
+def test_good_request(test_client, mock_treq):
+    mock_resp = MockResponse(200)
+    mock_treq.post.side_effect = [mock_resp]
+    mock_resp.headers.hasHeader.return_value = True
+    mock_resp.headers.getRawHeaders.return_value = ['application/json']
+    mock_resp.content.return_value = """{"other": "json"}"""
+    output = yield test_client.request("POST", "/test/uri", SOME_JSON)
+    assert output == {"other": "json"}
+
+
+@pytest_twisted.inlineCallbacks
+def test_bad_json(test_client, mock_treq):
+    mock_resp = MockResponse(200)
+    mock_treq.post.side_effect = [mock_resp]
+    mock_resp.headers.hasHeader.return_value = True
+    mock_resp.headers.getRawHeaders.return_value = ['application/json']
+    mock_resp.content.return_value = """{"other": "json}"""
+    with pytest.raises(ValueError):
+        yield test_client.request("POST", "/test/uri", SOME_JSON)
diff --git a/voltha/adapters/adtran_olt/test/net/test_adtran_zmq.py b/voltha/adapters/adtran_olt/test/net/test_adtran_zmq.py
new file mode 100644
index 0000000..8d9cfe9
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/net/test_adtran_zmq.py
@@ -0,0 +1,619 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 unittest import TestCase
+from mock import patch, MagicMock
+from voltha.adapters.adtran_olt.net.adtran_zmq import (
+    AdtranZmqClient, ZmqPairConnection, TwistedZmqAuthenticator, LocalAuthenticationThread)
+import zmq
+from zmq import constants
+
+
+class TestAdtranZmqClient_send(TestCase):
+    """
+    This class contains all methods to unit test AdtranZmqClient.send()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.ZmqPairConnection', autospec=True):
+            # Create AdtranZmqClient instance for test
+            self.adtran_zmq_client = AdtranZmqClient('1.2.3.4', lambda x: x, 5657)
+
+    # Test send() with normal data
+    def test_send_normal_data(self):
+        self.adtran_zmq_client.send("data")
+        self.adtran_zmq_client._socket.send.assert_called_once_with('data')
+
+    # Test send() with bad data (force exception)
+    def test_send_bad_data(self):
+        # Cause exception to occur
+        self.adtran_zmq_client._socket.send.side_effect = ValueError
+        # _socket.send in AdtranZmqClient.send() already mocked out via mock_zmq_pair_connection
+        self.adtran_zmq_client.send("cause exception")
+        self.adtran_zmq_client._socket.send.assert_called_once_with('cause exception')
+        self.adtran_zmq_client.log.exception.assert_called_once()
+
+
+class TestAdtranZmqClient_shutdown(TestCase):
+    """
+    This class contains all methods to unit test AdtranZmqClient.shutdown()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.ZmqPairConnection', autospec=True):
+            # Create AdtranZmqClient instance for test
+            self.adtran_zmq_client = AdtranZmqClient('1.2.3.4', lambda x: x, 5657)
+
+    # Test shutdown() and verifying that the socket call to shutdown() is made
+    def test_shutdown(self):
+        # _socket.shutdown in AdtranZmqClient.shutdown() already mocked out via mock_zmq_pair_connection
+        self.adtran_zmq_client.shutdown()
+        self.adtran_zmq_client._socket.shutdown.assert_called_once()
+
+
+class TestAdtranZmqClient_socket(TestCase):
+    """
+    This class contains all methods to unit test AdtranZmqClient.socket()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.ZmqPairConnection', autospec=True) as mk_zmq_pair_conn:
+            # Save mock instance ID for comparison later
+            self.zmq_pair_conn_mock_id = mk_zmq_pair_conn.return_value
+            # Create AdtranZmqClient instance for test
+            self.adtran_zmq_client = AdtranZmqClient('1.2.3.4', lambda x: x, 5657)
+
+    # Test socket() and verifying that the property and the attribute are correct
+    def test_socket(self):
+        # socket() is a property (getter) for the _socket attribute
+        # test that socket() and _socket equal the same thing
+        self.assertEqual(self.adtran_zmq_client.socket, self.zmq_pair_conn_mock_id)
+
+
+class TestAdtranZmqClient_rx_nop(TestCase):
+    """
+    This class contains all methods to unit test AdtranZmqClient.rx_nop()
+    """
+    # Test rx_nop() -- nothing to test, just creating code coverage
+    def test_rx_nop(self):
+        # rx_nop() is a static method
+        AdtranZmqClient.rx_nop(None)
+
+
+@patch('voltha.adapters.adtran_olt.net.adtran_zmq.TwistedZmqAuthenticator')
+class TestAdtranZmqClient_setup_plain_security(TestCase):
+    """
+    This class contains all methods to unit test AdtranZmqClient.setup_plain_security()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.ZmqPairConnection', autospec=True):
+            # Create AdtranZmqClient instance for test
+            self.adtran_zmq_client = AdtranZmqClient('1.2.3.4', lambda x: x, 5657)
+
+    # Test setup_plain_security() including verifying calls to addCallbacks()
+    # Omitting coverage for the methods inside of setup_plain_security() for now
+    def test_setup_plain_security(self, mk_twisted_zmq_authenticator):
+        deferred = self.adtran_zmq_client.setup_plain_security('user', 'pswd')
+        self.adtran_zmq_client.auth.start.assert_called_once()
+        # Test that addCallbacks was called twice
+        self.assertEqual(deferred.addCallbacks.call_count, 2)
+        # Test that both params used in each call is a function
+        # First, the call to d.addCallbacks(configure_plain, config_failure)
+        self.assertTrue(callable(deferred.addCallbacks.call_args_list[0][0][0]))
+        self.assertTrue(callable(deferred.addCallbacks.call_args_list[0][0][1]))
+        # Second, the call to d.addCallbacks(add_endoints, endpoint_failure)
+        self.assertTrue(callable(deferred.addCallbacks.call_args_list[1][0][0]))
+        self.assertTrue(callable(deferred.addCallbacks.call_args_list[1][0][1]))
+
+
+class TestAdtranZmqClient_setup_curve_security(TestCase):
+    """
+    This class contains all methods to unit test AdtranZmqClient.setup_curve_security()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.ZmqPairConnection', autospec=True):
+            # Create AdtranZmqClient instance for test
+            self.adtran_zmq_client = AdtranZmqClient('1.2.3.4', lambda x: x, 5657)
+
+    # Test setup_curve_security() -- not much to test, just creating line coverage
+    def test_setup_curve_security(self):
+        with self.assertRaises(NotImplementedError):
+            self.adtran_zmq_client.setup_curve_security()
+
+
+class TestZmqPairConnection_messageReceived(TestCase):
+    """
+    This class contains all methods to unit test ZmqPairConnection.messageReceived()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.ZmqConnection.__init__') as mock_init:
+            # Create ZmqPairConnection instance for test
+            mock_init.return_value = None
+            self.zmq_pair_connection = ZmqPairConnection(None)
+
+    # Test messageReceived() -- not much to test, just creating line coverage
+    def test_messageReceived(self):
+        self.zmq_pair_connection.onReceive = MagicMock()
+        self.zmq_pair_connection.messageReceived('message')
+
+
+class TestZmqPairConnection_onReceive(TestCase):
+    """
+    This class contains all methods to unit test ZmqPairConnection.onReceive()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.ZmqConnection.__init__') as mock_init:
+            # Create ZmqPairConnection instance for test
+            mock_init.return_value = None
+            self.zmq_pair_connection = ZmqPairConnection(None)
+
+    # Test onReceive() -- not much to test, just creating line coverage
+    def test_messageReceived(self):
+        with self.assertRaises(NotImplementedError):
+            self.zmq_pair_connection.onReceive('message')
+
+
+@patch('twisted.internet.reactor.callLater')
+class TestZmqPairConnection_send(TestCase):
+    """
+    This class contains all methods to unit test ZmqPairConnection.send()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.ZmqConnection.__init__') as mock_init:
+            # Create ZmqPairConnection instance for test
+            mock_init.return_value = None
+            self.zmq_pair_connection = ZmqPairConnection(None)
+            self.zmq_pair_connection.read_scheduled = None
+            self.zmq_pair_connection.socket = MagicMock()
+            self.zmq_pair_connection.doRead = MagicMock()
+
+    # Test send() for single-part message
+    def test_send_single_part_msg(self, mk_callLater):
+        self.zmq_pair_connection.send('message')
+        self.zmq_pair_connection.socket.send.assert_called_once_with('message', constants.NOBLOCK)
+
+    # Test send() for multi-part message
+    def test_send_multi_part_msg(self, mk_callLater):
+        self.zmq_pair_connection.send(['message1', 'message2', 'message3'])
+        self.zmq_pair_connection.socket.send_multipart.assert_called_once_with(['message1', 'message2', 'message3'],
+                                                                               flags=constants.NOBLOCK)
+
+
+class TestTwistedZmqAuthenticator_allow(TestCase):
+    """
+    This class contains all methods to unit test TwistedZmqAuthenticator.allow()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'):
+            # Create TwistedZmqAuthenticator instance for test
+            self.twisted_zmq_authenticator = TwistedZmqAuthenticator()
+            self.twisted_zmq_authenticator.pipe = MagicMock()
+
+    # Test allow() for successfully sending an ALLOW message with no IP addresses specified
+    def test_allow_success_no_ip(self):
+        self.twisted_zmq_authenticator.allow()
+        self.twisted_zmq_authenticator.pipe.send.assert_called_once_with([b'ALLOW'])
+
+    # Test allow() for successfully sending an ALLOW message to allow one IP address
+    def test_allow_success_one_ip(self):
+        self.twisted_zmq_authenticator.allow('1.2.3.4')
+        self.twisted_zmq_authenticator.pipe.send.assert_called_once_with([b'ALLOW', b'1.2.3.4'])
+
+    # Test allow() for successfully sending an ALLOW message to allow multiple IP addresses
+    def test_allow_success_mult_ips(self):
+        self.twisted_zmq_authenticator.allow('1.2.3.4', '5.6.7.8')
+        self.twisted_zmq_authenticator.pipe.send.assert_called_once_with([b'ALLOW', b'1.2.3.4', b'5.6.7.8'])
+
+    # Test allow() for sending an ALLOW message that results in an exception
+    def test_allow_failure(self):
+        self.twisted_zmq_authenticator.allow(1234)
+        self.twisted_zmq_authenticator.pipe.send.assert_not_called()
+
+    def tearDown(self):
+        self.twisted_zmq_authenticator.pipe = None
+
+
+class TestTwistedZmqAuthenticator_deny(TestCase):
+    """
+    This class contains all methods to unit test TwistedZmqAuthenticator.deny()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'):
+            # Create TwistedZmqAuthenticator instance for test
+            self.twisted_zmq_authenticator = TwistedZmqAuthenticator()
+            self.twisted_zmq_authenticator.pipe = MagicMock()
+
+    # Test deny() for successfully sending a DENY message with no IP addresses specified
+    def test_deny_success_no_ip(self):
+        self.twisted_zmq_authenticator.deny()
+        self.twisted_zmq_authenticator.pipe.send.assert_called_once_with([b'DENY'])
+
+    # Test deny() for successfully sending a DENY message to deny one IP address
+    def test_deny_success_one_ip(self):
+        self.twisted_zmq_authenticator.deny('1.2.3.4')
+        self.twisted_zmq_authenticator.pipe.send.assert_called_once_with([b'DENY', b'1.2.3.4'])
+
+    # Test deny() for successfully sending a DENY message to deny multiple IP addresses
+    def test_deny_success_mult_ips(self):
+        self.twisted_zmq_authenticator.deny('1.2.3.4', '5.6.7.8')
+        self.twisted_zmq_authenticator.pipe.send.assert_called_once_with([b'DENY', b'1.2.3.4', b'5.6.7.8'])
+
+    # Test deny() for sending a DENY message that results in an exception
+    def test_deny_failure(self):
+        self.twisted_zmq_authenticator.deny(1234, 5678)
+        self.twisted_zmq_authenticator.pipe.send.assert_not_called()
+
+    def tearDown(self):
+        self.twisted_zmq_authenticator.pipe = None
+
+
+@patch('voltha.adapters.adtran_olt.net.adtran_zmq.jsonapi.dumps')
+class TestTwistedZmqAuthenticator_configure_plain(TestCase):
+    """
+    This class contains all methods to unit test TwistedZmqAuthenticator.configure_plain()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'):
+            # Create TwistedZmqAuthenticator instance for test
+            self.twisted_zmq_authenticator = TwistedZmqAuthenticator()
+            self.twisted_zmq_authenticator.pipe = MagicMock()
+
+    # Test configure_plain() for successful plain security configuration with basic password
+    def test_configure_plain_success_with_pswd(self, mk_dumps):
+        mk_dumps.return_value = '{"passwords": ["topsecret"]}'
+        self.twisted_zmq_authenticator.configure_plain(passwords={'passwords': ['topsecret']})
+        self.twisted_zmq_authenticator.pipe.send.assert_called_once_with([b'PLAIN', b'*',
+                                                                          b'{"passwords": ["topsecret"]}'])
+
+    # Test configure_plain() for successful plain security configuration with no password
+    def test_configure_plain_success_without_pswd(self, mk_dumps):
+        mk_dumps.return_value = '{}'
+        # 'passwords' parameter defaults to None
+        self.twisted_zmq_authenticator.configure_plain()
+        self.twisted_zmq_authenticator.pipe.send.assert_called_once_with([b'PLAIN', b'*',
+                                                                          b'{}'])
+
+    # Test configure_plain() for failed call to send() with TypeError return
+    def test_configure_plain_failure(self, mk_dumps):
+        self.twisted_zmq_authenticator.pipe.send.side_effect = TypeError
+        self.twisted_zmq_authenticator.configure_plain()
+        self.twisted_zmq_authenticator.log.exception.assert_called_once()
+
+    def tearDown(self):
+        self.twisted_zmq_authenticator.pipe = None
+
+
+class TestTwistedZmqAuthenticator_configure_curve(TestCase):
+    """
+    This class contains all methods to unit test TwistedZmqAuthenticator.configure_curve()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'):
+            # Create TwistedZmqAuthenticator instance for test
+            self.twisted_zmq_authenticator = TwistedZmqAuthenticator()
+            self.twisted_zmq_authenticator.pipe = MagicMock()
+
+    # Test configure_curve() for successful curve security configuration
+    def test_configure_curve_success(self):
+        self.twisted_zmq_authenticator.configure_curve('x', 'anywhere')
+        self.twisted_zmq_authenticator.pipe.send.assert_called_once_with([b'CURVE', b'x', b'anywhere'])
+
+    # Test configure_curve() for failed call to send() with TypeError return
+    def test_configure_curve_failure(self):
+        self.twisted_zmq_authenticator.pipe.send.side_effect = TypeError
+        self.twisted_zmq_authenticator.configure_curve()
+        self.twisted_zmq_authenticator.log.exception.assert_called_once()
+
+    def tearDown(self):
+        self.twisted_zmq_authenticator.pipe = None
+
+
+@patch('voltha.adapters.adtran_olt.net.adtran_zmq.threads.deferToThread')
+@patch('voltha.adapters.adtran_olt.net.adtran_zmq.LocalAuthenticationThread', autospec=True)
+@patch('voltha.adapters.adtran_olt.net.adtran_zmq.ZmqPairConnection', autospec=True)
+class TestTwistedZmqAuthenticator_start(TestCase):
+    """
+    This class contains all methods to unit test TwistedZmqAuthenticator.start()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'):
+            # Create TwistedZmqAuthenticator instance for test
+            self.twisted_zmq_authenticator = TwistedZmqAuthenticator()
+
+    # Test start() for successful execution
+    def test_start_success(self, mk_zmq_pair_conn, mk_local_auth_thread, mk_defer):
+        _ = self.twisted_zmq_authenticator.start()
+        self.assertEqual(self.twisted_zmq_authenticator.pipe.onReceive, AdtranZmqClient.rx_nop)
+
+    # Test start() for failure due to artificial exception
+    def test_start_failure(self, mk_zmq_pair_conn, mk_local_auth_thread, mk_defer):
+        mk_defer.side_effect = TypeError
+        self.twisted_zmq_authenticator.start()
+        self.twisted_zmq_authenticator.log.exception.assert_called_once()
+
+
+@patch('voltha.adapters.adtran_olt.net.adtran_zmq.sys')
+class TestTwistedZmqAuthenticator_do_thread_start(TestCase):
+    """
+    This class contains all methods to unit test TwistedZmqAuthenticator._do_thread_start()
+    """
+    # Test _do_thread_start() for successful execution with non-default timeout=20
+    def test_do_thread_start_success(self, mk_sys):
+        mk_sys.version_info = (2, 7)
+        mk_thread = MagicMock()
+        mk_thread.started.wait.return_value = True
+        TwistedZmqAuthenticator._do_thread_start(mk_thread, 20)
+        mk_thread.start.assert_called_once_with()
+        mk_thread.started.wait.assert_called_once_with(timeout=20)
+
+    # Test _do_thread_start() for successful execution running python v2.6 with default timeout=10
+    def test_do_thread_start_success_v26(self, mk_sys):
+        mk_sys.version_info = (2, 6)
+        mk_thread = MagicMock()
+        TwistedZmqAuthenticator._do_thread_start(mk_thread)
+        mk_thread.start.assert_called_once_with()
+        mk_thread.started.wait.assert_called_once_with(timeout=10)
+
+    # Test _do_thread_start() for failed execution due to thread.started.wait() returning False
+    def test_do_thread_start_failure(self, mk_sys):
+        mk_sys.version_info = (2, 7)
+        mk_thread = MagicMock()
+        mk_thread.started.wait.return_value = False
+        with self.assertRaises(RuntimeError):
+            TwistedZmqAuthenticator._do_thread_start(mk_thread)
+        mk_thread.start.assert_called_once_with()
+        mk_thread.started.wait.assert_called_once_with(timeout=10)
+
+
+@patch('voltha.adapters.adtran_olt.net.adtran_zmq.succeed')
+@patch('voltha.adapters.adtran_olt.net.adtran_zmq.threads.deferToThread')
+class TestTwistedZmqAuthenticator_stop(TestCase):
+    """
+    This class contains all methods to unit test TwistedZmqAuthenticator.stop()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'):
+            # Create TwistedZmqAuthenticator instance for test
+            self.twisted_zmq_authenticator = TwistedZmqAuthenticator()
+
+    # Test stop() for successful execution where pipe exists and needs to be closed properly
+    def test_stop_pipe_exists(self, mk_defer, mk_succeed):
+        # Create mocks and save a reference for later because source code clears the pipe/thread attributes
+        self.mk_pipe = self.twisted_zmq_authenticator.pipe = MagicMock()
+        self.mk_thread = self.twisted_zmq_authenticator.thread = MagicMock()
+        self.twisted_zmq_authenticator.thread.is_alive.return_value = True
+        _ = self.twisted_zmq_authenticator.stop()
+        self.mk_pipe.send.assert_called_once_with(b'TERMINATE')
+        self.mk_pipe.shutdown.assert_called_once_with()
+        self.mk_thread.is_alive.assert_called_once_with()
+        mk_defer.assert_called_once_with(TwistedZmqAuthenticator._do_thread_join, self.mk_thread)
+
+    # Test stop() for successful execution where pipe doesn't exist
+    def test_stop_pipe_doesnt_exist(self, mk_defer, mk_succeed):
+        self.twisted_zmq_authenticator.pipe = None
+        _ = self.twisted_zmq_authenticator.stop()
+        self.assertEqual(self.twisted_zmq_authenticator.pipe, None)
+        self.assertEqual(self.twisted_zmq_authenticator.thread, None)
+
+
+class TestTwistedZmqAuthenticator_do_thread_join(TestCase):
+    """
+    This class contains all methods to unit test TwistedZmqAuthenticator._do_thread_join()
+    """
+    # Test _do_thread_join() for successful execution
+    def test_do_thread_join_default_timeout(self):
+        mk_thread = MagicMock()
+        TwistedZmqAuthenticator._do_thread_join(mk_thread)
+        mk_thread.join.assert_called_once_with(1)
+
+    # Test _do_thread_join() for successful execution
+    def test_do_thread_join_timeout_10(self):
+        mk_thread = MagicMock()
+        TwistedZmqAuthenticator._do_thread_join(mk_thread, 10)
+        mk_thread.join.assert_called_once_with(10)
+
+
+class TestTwistedZmqAuthenticator_is_alive(TestCase):
+    """
+    This class contains all methods to unit test TwistedZmqAuthenticator.is_alive()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq_factory'):
+            # Create TwistedZmqAuthenticator instance for test
+            self.twisted_zmq_authenticator = TwistedZmqAuthenticator()
+
+    # Test is_alive() to return True
+    def test_is_alive_true(self):
+        self.twisted_zmq_authenticator.thread = MagicMock()
+        self.twisted_zmq_authenticator.thread.is_alive.return_value = True
+        response = self.twisted_zmq_authenticator.is_alive()
+        self.assertTrue(response)
+
+    # Test is_alive() to return False
+    def test_is_alive_false(self):
+        self.twisted_zmq_authenticator.thread = MagicMock()
+        self.twisted_zmq_authenticator.thread.is_alive.return_value = False
+        response = self.twisted_zmq_authenticator.is_alive()
+        self.assertFalse(response)
+
+
+@patch('voltha.adapters.adtran_olt.net.adtran_zmq.LocalAuthenticationThread._handle_zap')
+@patch('voltha.adapters.adtran_olt.net.adtran_zmq.LocalAuthenticationThread._handle_pipe')
+@patch('voltha.adapters.adtran_olt.net.adtran_zmq.zmq.Poller', autospec=True)
+class TestLocalAuthenticationThread_run(TestCase):
+    """
+    This class contains all methods to unit test LocalAuthenticationThread.run()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.Event', autospec=True):
+            ctxt_mock = MagicMock()
+            auth_mock = MagicMock()
+            # Save mock instance ID's for comparison later
+            self.ctxt_socket_instance = ctxt_mock.socket.return_value
+            self.auth_zap_socket_instance = auth_mock.zap_socket
+            # Create LocalAuthenticationThread instance for test
+            self.local_auth_thread = LocalAuthenticationThread(ctxt_mock, None, authenticator=auth_mock)
+
+    # Test run() for running the loop once and terminating due to simulated 'TERMINATE' msg from main thread socket
+    def test_run_auth_loop_once(self, mk_poller, mk_handle_pipe, mk_handle_zap):
+        instance = mk_poller.return_value
+        instance.poll.return_value = [(self.ctxt_socket_instance, zmq.POLLIN)]
+        mk_handle_pipe.return_value = True
+        self.local_auth_thread.run()
+        self.local_auth_thread.authenticator.start.assert_called_once_with()
+        self.local_auth_thread.started.set.assert_called_once_with()
+        instance.register.assert_any_call(self.ctxt_socket_instance, zmq.POLLIN)
+        instance.register.assert_any_call(self.auth_zap_socket_instance, zmq.POLLIN)
+        mk_handle_pipe.assert_called_once_with()
+        self.local_auth_thread.pipe.close.assert_called_once_with()
+        self.local_auth_thread.authenticator.stop.assert_called_once_with()
+
+    # Test run() for running the loop once and terminating due to exception when handling zap socket
+    def test_run_auth_loop_handle_zap_exception(self, mk_poller, mk_handle_pipe, mk_handle_zap):
+        instance = mk_poller.return_value
+        instance.poll.return_value = [(self.ctxt_socket_instance, zmq.POLLIN),
+                                      (self.auth_zap_socket_instance, zmq.POLLIN)]
+        mk_handle_pipe.return_value = False
+        mk_handle_zap.side_effect = AssertionError
+        self.local_auth_thread.run()
+        mk_handle_zap.assert_called_once_with()
+        self.local_auth_thread.log.exception.assert_called_once()
+
+    # Test run() for failed call to poller.poll()
+    def test_run_bad_poll_response(self, mk_poller, mk_handle_pipe, mk_handle_zap):
+        instance = mk_poller.return_value
+        instance.poll.side_effect = zmq.ZMQError
+        self.local_auth_thread.run()
+        mk_handle_pipe.assert_not_called()
+        mk_handle_zap.assert_not_called()
+        self.local_auth_thread.pipe.close.assert_called_once_with()
+        self.local_auth_thread.authenticator.stop.assert_called_once_with()
+
+
+class TestLocalAuthenticationThread_handle_zap(TestCase):
+    """
+    This class contains all methods to unit test LocalAuthenticationThread._handle_zap()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.Event', autospec=True):
+            # Create LocalAuthenticationThread instance for test
+            self.local_auth_thread = LocalAuthenticationThread(MagicMock(), None, authenticator=MagicMock())
+
+    # Test _handle_zap() for handling a valid message returned from recv_multipart()
+    def test_handle_zap_valid_msg(self):
+        self.local_auth_thread.authenticator.zap_socket.recv_multipart.return_value = 'message'
+        self.local_auth_thread._handle_zap()
+        self.local_auth_thread.authenticator.handle_zap_message.assert_called_once_with('message')
+
+    # Test _handle_zap() for handling no message returned from recv_multipart()
+    def test_handle_zap_no_msg(self):
+        self.local_auth_thread.authenticator.zap_socket.recv_multipart.return_value = None
+        self.local_auth_thread._handle_zap()
+        self.local_auth_thread.authenticator.handle_zap_message.assert_not_called()
+
+
+class TestLocalAuthenticationThread_handle_pipe(TestCase):
+    """
+    This class contains all methods to unit test LocalAuthenticationThread._handle_pipe()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.structlog.get_logger'), \
+                patch('voltha.adapters.adtran_olt.net.adtran_zmq.Event', autospec=True):
+            # Create LocalAuthenticationThread instance for test
+            self.local_auth_thread = LocalAuthenticationThread(MagicMock(), None, authenticator=MagicMock())
+
+    # Test _handle_pipe() for handling no message returned from recv_multipart()
+    def test_handle_pipe_no_msg(self):
+        self.local_auth_thread.pipe.recv_multipart.return_value = None
+        terminate = self.local_auth_thread._handle_pipe()
+        self.assertEqual(terminate, True)
+
+    # Test _handle_pipe() for handling ALLOW message with one IP address
+    def test_handle_pipe_allow_one_ip(self):
+        self.local_auth_thread.pipe.recv_multipart.return_value = [b'ALLOW', b'1.2.3.4']
+        terminate = self.local_auth_thread._handle_pipe()
+        self.assertEqual(terminate, False)
+        self.local_auth_thread.authenticator.allow.assert_called_once_with(u'1.2.3.4')
+        self.local_auth_thread.log.exception.assert_not_called()
+
+    # Test _handle_pipe() for handling ALLOW message and failing due to exception in allow()
+    def test_handle_pipe_allow_failure(self):
+        self.local_auth_thread.pipe.recv_multipart.return_value = [b'ALLOW', b'1.2.3.4']
+        self.local_auth_thread.authenticator.allow.side_effect = ValueError
+        terminate = self.local_auth_thread._handle_pipe()
+        self.assertEqual(terminate, False)
+        self.local_auth_thread.authenticator.allow.assert_called_once_with(u'1.2.3.4')
+        self.local_auth_thread.log.exception.assert_called_once()
+
+    # Test _handle_pipe() for handling DENY message with two IP addresses
+    def test_handle_pipe_deny_two_ips(self):
+        self.local_auth_thread.pipe.recv_multipart.return_value = [b'DENY', b'1.2.3.4', b'5.6.7.8']
+        terminate = self.local_auth_thread._handle_pipe()
+        self.assertEqual(terminate, False)
+        self.local_auth_thread.authenticator.deny.assert_called_once_with(u'1.2.3.4', u'5.6.7.8')
+        self.local_auth_thread.log.exception.assert_not_called()
+
+    # Test _handle_pipe() for handling DENY message and failing due to exception in deny()
+    def test_handle_pipe_deny_failure(self):
+        self.local_auth_thread.pipe.recv_multipart.return_value = [b'DENY', b'1.2.3.4', b'5.6.7.8']
+        self.local_auth_thread.authenticator.deny.side_effect = ValueError
+        terminate = self.local_auth_thread._handle_pipe()
+        self.assertEqual(terminate, False)
+        self.local_auth_thread.authenticator.deny.assert_called_once_with(u'1.2.3.4', u'5.6.7.8')
+        self.local_auth_thread.log.exception.assert_called_once()
+
+    # Test _handle_pipe() for handling PLAIN message
+    @patch('voltha.adapters.adtran_olt.net.adtran_zmq.jsonapi.loads')
+    def test_handle_pipe_plain(self, mk_jsonapi_loads):
+        self.local_auth_thread.pipe.recv_multipart.return_value = [b'PLAIN', b'*', b'password']
+        mk_jsonapi_loads.return_value = u'password'
+        terminate = self.local_auth_thread._handle_pipe()
+        self.assertEqual(terminate, False)
+        self.local_auth_thread.authenticator.configure_plain.assert_called_once_with(u'*', u'password')
+
+    # Test _handle_pipe() for handling CURVE message
+    def test_handle_pipe_curve(self):
+        self.local_auth_thread.pipe.recv_multipart.return_value = [b'CURVE', b'x', b'anywhere']
+        terminate = self.local_auth_thread._handle_pipe()
+        self.assertEqual(terminate, False)
+        self.local_auth_thread.authenticator.configure_curve.assert_called_once_with(u'x', u'anywhere')
+
+    # Test _handle_pipe() for handling TERMINATE message
+    def test_handle_pipe_terminate(self):
+        self.local_auth_thread.pipe.recv_multipart.return_value = [b'TERMINATE']
+        terminate = self.local_auth_thread._handle_pipe()
+        self.assertEqual(terminate, True)
+
+    # Test _handle_pipe() for handling invalid message
+    def test_handle_pipe_invalid(self):
+        self.local_auth_thread.pipe.recv_multipart.return_value = [b'xINVALIDx']
+        terminate = self.local_auth_thread._handle_pipe()
+        self.assertEqual(terminate, False)
+        self.local_auth_thread.log.error.assert_called_once()
diff --git a/voltha/adapters/adtran_olt/test/net/test_pio_zmq.py b/voltha/adapters/adtran_olt/test/net/test_pio_zmq.py
new file mode 100644
index 0000000..dc55107
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/net/test_pio_zmq.py
@@ -0,0 +1,300 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 unittest import TestCase
+from mock import MagicMock, patch
+import pytest
+from voltha.adapters.adtran_olt.net.pio_zmq import PioClient
+
+
+@patch('json.loads', autospec=True, spec_set=True)
+class TestPioZmqGetUrlType(TestCase):
+    """
+    This class contains all methods to unit test get_url_type()
+    """
+    # Helper method to run the test and do the checks for each test method
+    def assert_url_type(self, packet, expected_url_type, mock_json_loads):
+        url_type = self.pio_client.get_url_type(packet)
+        mock_json_loads.assert_called_once_with(packet)
+        self.assertEqual(url_type, expected_url_type)
+
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.AdtranZmqClient.__init__', autospec=True):
+            # Create PioClient instance for test
+            self.pio_client = PioClient(None, None, None)
+
+    # Test the creation of the PioClient instance
+    def test_create_pio_client_instance(self, mock_json_loads):
+        self.assertGreaterEqual(self.pio_client._seq_number, 1)
+        self.assertLessEqual(self.pio_client._seq_number, 2**32)
+
+    # Test get_url_type() for valid PACKET_IN url
+    def test_get_url_type_packet_in(self, mock_json_loads):
+        # Create serialized json string
+        packet = '{"url": "adtran-olt-of-control/packet-in", "buffer-id": 1, "total-len": 4, \
+                   "evc-map-name": "evc-map-name", "exception-type": "", "port-number": 1, \
+                   "message-contents": "ASNFZw==n"}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'url': 'adtran-olt-of-control/packet-in',
+                                        'buffer-id': 1,
+                                        'total-len': 4,
+                                        'evc-map-name': 'evc-map-name',
+                                        'exception-type': '',
+                                        'port-number': 1,
+                                        'message-contents': 'ASNFZw==n'}
+        self.assert_url_type(packet, PioClient.UrlType.PACKET_IN, mock_json_loads)
+
+    # Test get_url_type() for valid PACKET_OUT url
+    def test_get_url_type_packet_out(self, mock_json_loads):
+        # Create serialized json string
+        packet = '{"url": "adtran-olt-of-control/packet-out", "buffer-id": 1, "total-len": 4, \
+                   "evc-map-name": "evc-map-name", "exception-type": "", "port-number": 1, \
+                   "message-contents": "ASNFZw==n"}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'url': 'adtran-olt-of-control/packet-out',
+                                        'buffer-id': 1,
+                                        'total-len': 4,
+                                        'evc-map-name': 'evc-map-name',
+                                        'exception-type': '',
+                                        'port-number': 1,
+                                        'message-contents': 'ASNFZw==n'}
+        self.assert_url_type(packet, PioClient.UrlType.PACKET_OUT, mock_json_loads)
+
+    # Test get_url_type() for valid EVCMAPS_RESPONSE url
+    def test_get_url_type_evc_map_response(self, mock_json_loads):
+        # Create serialized json string
+        packet = '{"url": "adtran-olt-of-control/evc-map-response", "buffer-id": 1, "total-len": 4, \
+                   "evc-map-name": "evc-map-name", "exception-type": "", "port-number": 1, \
+                   "message-contents": "ASNFZw==n"}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'url': 'adtran-olt-of-control/evc-map-response',
+                                        'buffer-id': 1,
+                                        'total-len': 4,
+                                        'evc-map-name': 'evc-map-name',
+                                        'exception-type': '',
+                                        'port-number': 1,
+                                        'message-contents': 'ASNFZw==n'}
+        self.assert_url_type(packet, PioClient.UrlType.EVCMAPS_RESPONSE, mock_json_loads)
+
+    # Test get_url_type() for valid EVCMAPS_REQUEST url
+    def test_get_url_type_evc_map_request(self, mock_json_loads):
+        # Create serialized json string
+        packet = '{"url": "adtran-olt-of-control/evc-map-request", "buffer-id": 1, "total-len": 4, \
+                   "evc-map-name": "evc-map-name", "exception-type": "", "port-number": 1, \
+                   "message-contents": "ASNFZw==n"}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'url': 'adtran-olt-of-control/evc-map-request',
+                                        'buffer-id': 1,
+                                        'total-len': 4,
+                                        'evc-map-name': 'evc-map-name',
+                                        'exception-type': '',
+                                        'port-number': 1,
+                                        'message-contents': 'ASNFZw==n'}
+        self.assert_url_type(packet, PioClient.UrlType.EVCMAPS_REQUEST, mock_json_loads)
+
+    # Test get_url_type() for unknown url type
+    def test_get_url_type_unknown_url_type(self, mock_json_loads):
+        # Create serialized json string
+        packet = '{"url": "adtran-olt-of-control/unknown", "buffer-id": 1, "total-len": 4, \
+                   "evc-map-name": "evc-map-name", "exception-type": "", "port-number": 1, \
+                   "message-contents": "ASNFZw==n"}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'url': 'adtran-olt-of-control/unknown',
+                                        'buffer-id': 1,
+                                        'total-len': 4,
+                                        'evc-map-name': 'evc-map-name',
+                                        'exception-type': '',
+                                        'port-number': 1,
+                                        'message-contents': 'ASNFZw==n'}
+        self.assert_url_type(packet, PioClient.UrlType.UNKNOWN, mock_json_loads)
+
+    # Test get_url_type() for invalid json message (url field missing)
+    def test_get_url_type_invalid_json(self, mock_json_loads):
+        # Create serialized json string
+        packet = '{"invalid": "meaningless"}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'invalid': 'meaningless'}
+        self.assert_url_type(packet, PioClient.UrlType.UNKNOWN, mock_json_loads)
+
+
+@patch('scapy.layers.l2.Ether', autospec=True, spec_set=True)
+@patch('json.loads', autospec=True, spec_set=True)
+class TestPioZmqDecodePacket(TestCase):
+    """
+    This class contains all methods to unit test decode_packet()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.AdtranZmqClient.__init__', autospec=True) as mock_init:
+            # Create PonClient instance for test
+            self.pio_client = PioClient(None, None, None)
+            # Probably shouldn't be doing a test in setUp(), but it seemed like the thing to do
+            mock_init.assert_called_once_with(self.pio_client, None, None, None)
+        self.pio_client.log = MagicMock()
+
+    # Test decode_packet() for good decode with valid json message
+    def test_decode_packet_valid_decode(self, mock_json_loads, mock_ether_class):
+        # Create serialized json string
+        packet = '{"url": "adtran-olt-of-control/packet-in", "total-len": 4, "evc-map-name": "evc-map-name", \
+                   "port-number": 1, "message-contents": "ASNFZw==n"}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'url': 'adtran-olt-of-control/packet-in',
+                                        'total-len': 4,
+                                        'evc-map-name': 'evc-map-name',
+                                        'port-number': 1,
+                                        'message-contents': 'ASNFZw==n'}
+        port_num, evc_map_name, _ = self.pio_client.decode_packet(packet)
+        mock_json_loads.assert_called_once_with(packet)
+        self.assertTrue(self.pio_client.log.debug.called)
+        self.assertFalse(self.pio_client.log.exception.called)
+        self.assertEqual(port_num, 1)
+        self.assertEqual(evc_map_name, 'evc-map-name')
+
+    # Test decode_packet() for missing json field
+    def test_decode_packet_json_field_missing(self, mock_json_loads, mock_ether_class):
+        # Create serialized json string
+        packet = '{"url": "adtran-olt-of-control/packet-in", "total-len": 4, "evc-map-name": "evc-map-name", \
+                   "port-number": 1}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'url': 'adtran-olt-of-control/packet-in',
+                                        'total-len': 4,
+                                        'evc-map-name': 'evc-map-name',
+                                        'port-number': 1}
+        with pytest.raises(AssertionError, message="Expecting AssertionError"):
+            _, _, _ = self.pio_client.decode_packet(packet)
+        # All checks must be outside of the context manager scope in order to be executed
+        self.assertTrue(self.pio_client.log.exception.called)
+
+    # Test decode_packet() for 'total-len' value not matching actual length of 'message-contents'
+    def test_decode_packet_json_message_length_mismatch(self, mock_json_loads, mock_ether_class):
+        # Create serialized json string
+        packet = '{"url": "adtran-olt-of-control/packet-in", "total-len": 4, "evc-map-name": "evc-map-name", \
+                   "port-number": 1, "message-contents": ""}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'url': 'adtran-olt-of-control/packet-in',
+                                        'total-len': 4,
+                                        'evc-map-name': 'evc-map-name',
+                                        'port-number': 1,
+                                        'message-contents': ''}
+        with pytest.raises(AssertionError, message="Expecting AssertionError"):
+            _, _, _ = self.pio_client.decode_packet(packet)
+        # All checks must be outside of the context manager scope in order to be executed
+        self.assertTrue(self.pio_client.log.exception.called)
+
+
+class TestPioZmqSequenceNumber(TestCase):
+    """
+    This class contains all methods to unit test sequence_number()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.AdtranZmqClient.__init__', autospec=True) as mock_init:
+            # Create PonClient instance for test
+            self.pio_client = PioClient(None, None, None)
+            # Probably shouldn't be doing a test in setUp(), but it seemed like the thing to do
+            mock_init.assert_called_once_with(self.pio_client, None, None, None)
+
+    # Test sequence_number() for normal +1 increment
+    def test_sequence_number_normal_increment(self):
+        self.pio_client._seq_number = 1
+        seq_num = self.pio_client.sequence_number
+        self.assertEqual(seq_num, 2)
+
+    # Test sequence_number() for 2^32 overflow back to 0
+    def test_sequence_number_overflow_reset(self):
+        self.pio_client._seq_number = 2**32
+        seq_num = self.pio_client.sequence_number
+        self.assertEqual(seq_num, 0)
+
+
+@patch('json.dumps', autospec=True, spec_set=True)
+class TestPioZmqEncodePacket(TestCase):
+    """
+    This class contains all methods to unit test encode_packet()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.AdtranZmqClient.__init__', autospec=True) as mock_init:
+            # Create PonClient instance for test
+            self.pio_client = PioClient(None, None, None)
+            # Probably shouldn't be doing a test in setUp(), but it seemed like the thing to do
+            mock_init.assert_called_once_with(self.pio_client, None, None, None)
+
+    # Test encode_packet() -- nothing to test, just gaining code coverage
+    def test_encode_packet(self, mock_json_dumps):
+        self.pio_client.encode_packet(1, '01234567', "evc-map-name")
+
+
+@patch('json.dumps', autospec=True, spec_set=True)
+class TestPioZmqQueryRequestPacket(TestCase):
+    """
+    This class contains all methods to unit test query_request_packet()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.AdtranZmqClient.__init__', autospec=True) as mock_init:
+            # Create PonClient instance for test
+            self.pio_client = PioClient(None, None, None)
+            # Probably shouldn't be doing a test in setUp(), but it seemed like the thing to do
+            mock_init.assert_called_once_with(self.pio_client, None, None, None)
+
+    # Test query_request_packet() -- nothing to test, just gaining code coverage
+    def test_query_request_packet(self, mock_json_dumps):
+        self.pio_client.query_request_packet()
+
+
+@patch('json.loads', autospec=True, spec_set=True)
+class TestPioZmqDecodeQueryResponsePacket(TestCase):
+    """
+    This class contains all methods to unit test decode_query_response_packet()
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.AdtranZmqClient.__init__', autospec=True) as mock_init:
+            # Create PonClient instance for test
+            self.pio_client = PioClient(None, None, None)
+            # Probably shouldn't be doing a test in setUp(), but it seemed like the thing to do
+            mock_init.assert_called_once_with(self.pio_client, None, None, None)
+        self.pio_client.log = MagicMock()
+
+    # Test decode_query_response_packet() for decoding a json message with valid evc-map list
+    def test_decode_query_response_packet_valid_decode(self, mock_json_loads):
+        # Create serialized json string
+        packet = '{"url": "adtran-olt-of-control/evc-map-response"}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'url': 'adtran-olt-of-control/evc-map-response',
+                                        'evc-map-list': ['evc-map-0123456789.0.0.2176']}
+        maps = self.pio_client.decode_query_response_packet(packet)
+        mock_json_loads.assert_called_once_with(packet)
+        self.assertTrue(self.pio_client.log.debug.called)
+        self.assertEqual(maps[0], 'evc-map-0123456789.0.0.2176')
+
+    # Test decode_query_response_packet() for decoding a json message with wrong url type
+    def test_decode_query_response_packet_wrong_url_type(self, mock_json_loads):
+        # Create serialized json string
+        packet = '{"url": "adtran-olt-of-control/packet-in"}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'url': 'adtran-olt-of-control/packet-in',
+                                        'evc-map-list': ['evc-map-0123456789.0.0.2176']}
+        maps = self.pio_client.decode_query_response_packet(packet)
+        mock_json_loads.assert_called_once_with(packet)
+        self.assertTrue(self.pio_client.log.debug.called)
+        self.assertEqual(maps, [])
+
+    # Test decode_query_response_packet() for decoding a json message with empty evc-map list
+    def test_decode_query_response_packet_no_evc_maps(self, mock_json_loads):
+        # Create serialized json string
+        packet = '{"url": "adtran-olt-of-control/evc-map-response"}'
+        # Create dict that would be returned by json.loads(packet)
+        mock_json_loads.return_value = {'url': 'adtran-olt-of-control/evc-map-response',
+                                        'evc-map-list': None}
+        maps = self.pio_client.decode_query_response_packet(packet)
+        mock_json_loads.assert_called_once_with(packet)
+        self.assertTrue(self.pio_client.log.debug.called)
+        self.assertEqual(maps, [])
diff --git a/voltha/adapters/adtran_olt/test/net/test_pon_zmq.py b/voltha/adapters/adtran_olt/test/net/test_pon_zmq.py
new file mode 100644
index 0000000..fa9f93b
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/net/test_pon_zmq.py
@@ -0,0 +1,83 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 unittest import TestCase
+from mock import patch
+from voltha.adapters.adtran_olt.net.pon_zmq import PonClient
+import json
+
+
+# Mock PonClient's __init__ constructor method
+def adtran_zmq_client_init(self, ip_address, rx_callback, port):
+    # Create instance vars that otherwise would have been created by the super() constructor call
+    self.log = None
+    self.zmq_endpoint = None
+    self._socket = None
+    self.auth = None
+
+
+class TestPonZmq(TestCase):
+    """
+    This class contains all methods to unit test pon_zmq.py
+    """
+    def setUp(self):
+        with patch('voltha.adapters.adtran_olt.net.adtran_zmq.AdtranZmqClient.__init__', adtran_zmq_client_init):
+            # Create PonClient instance for test
+            self.pon_client = PonClient(None, None, None)
+
+    def test_create_pon_client_instance(self):
+        self.assertIsNone(self.pon_client.log)
+        self.assertIsNone(self.pon_client.zmq_endpoint)
+        self.assertIsNone(self.pon_client._socket)
+        self.assertIsNone(self.pon_client.auth)
+
+    def test_encode_sample_omci_packet_pon0_onu0(self):
+        packet = self.pon_client.encode_omci_packet('01234567', 0, 0)
+        self.assertIs(type(packet), str)
+        msg = json.loads(packet)
+        self.assertEqual(msg['operation'], 'NOTIFY')
+        self.assertEqual(msg['pon-id'], 0)
+        self.assertEqual(msg['onu-id'], 0)
+
+    def test_encode_sample_omci_packet_pon15_onu127(self):
+        packet = self.pon_client.encode_omci_packet('76543210', 15, 127)
+        self.assertIs(type(packet), str)
+        msg = json.loads(packet)
+        self.assertEqual(msg['operation'], 'NOTIFY')
+        self.assertEqual(msg['pon-id'], 15)
+        self.assertEqual(msg['onu-id'], 127)
+
+    def test_decode_sample_omci_packet_pon0_onu0(self):
+        msg = '01234567'
+        test_packet = json.dumps({"operation": "NOTIFY",
+                                  "url": "adtran-olt-pon-control/omci-message",
+                                  "pon-id": 0,
+                                  "onu-id": 0,
+                                  "message-contents": msg.decode("hex").encode("base64")})
+        pon_id, onu_id, msg_data, is_omci = self.pon_client.decode_packet(test_packet)
+        self.assertEqual(pon_id, 0)
+        self.assertEqual(onu_id, 0)
+        self.assertEqual(msg_data.encode("hex"), '01234567')
+
+    def test_decode_sample_omci_packet_pon15_onu127(self):
+        msg = '76543210'
+        test_packet = json.dumps({"operation": "NOTIFY",
+                                  "url": "adtran-olt-pon-control/omci-message",
+                                  "pon-id": 15,
+                                  "onu-id": 127,
+                                  "message-contents": msg.decode("hex").encode("base64")})
+        pon_id, onu_id, msg_data, is_omci = self.pon_client.decode_packet(test_packet)
+        self.assertEqual(pon_id, 15)
+        self.assertEqual(onu_id, 127)
+        self.assertEqual(msg_data.encode("hex"), '76543210')
diff --git a/voltha/adapters/adtran_olt/test/resources/__init__.py b/voltha/adapters/adtran_olt/test/resources/__init__.py
new file mode 100644
index 0000000..29845c7
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/resources/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Adtran, Inc.

+#

+# 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.

diff --git a/voltha/adapters/adtran_olt/test/resources/test_adtran_olt_resource_manager.py b/voltha/adapters/adtran_olt/test/resources/test_adtran_olt_resource_manager.py
new file mode 100644
index 0000000..7491841
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/resources/test_adtran_olt_resource_manager.py
@@ -0,0 +1,229 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import mock
+import pytest
+
+
+from voltha.adapters.adtran_olt.resources.adtranolt_platform import MAX_ONUS_PER_PON,\
+    MIN_TCONT_ALLOC_ID, MAX_TCONT_ALLOC_ID, MIN_GEM_PORT_ID, MAX_GEM_PORT_ID, MAX_TCONTS_PER_ONU,\
+    MAX_GEM_PORTS_PER_ONU
+from voltha.adapters.adtran_olt.resources.adtran_olt_resource_manager import AdtranPONResourceManager
+from voltha.adapters.adtran_olt.resources.adtran_olt_resource_manager import AdtranOltResourceMgr
+
+
+@pytest.fixture(scope="module")
+def device_info():
+    device_info = mock.MagicMock()
+    device_info.technology = "xgspon"
+    device_info.onu_id_start = 0
+    device_info.onu_id_end = MAX_ONUS_PER_PON
+    device_info.alloc_id_start = MIN_TCONT_ALLOC_ID
+    device_info.alloc_id_end = MAX_TCONT_ALLOC_ID
+    device_info.gemport_id_start = MIN_GEM_PORT_ID
+    device_info.gemport_id_end = MAX_GEM_PORT_ID
+    device_info.pon_ports = 3
+    device_info.max_tconts = MAX_TCONTS_PER_ONU
+    device_info.max_gem_ports = MAX_GEM_PORTS_PER_ONU
+    device_info.intf_ids = ["1", "2", "3"]
+    return device_info
+
+
+@pytest.fixture(scope="module")
+def pon_rm():
+    pon_rm = mock.MagicMock(AdtranPONResourceManager)
+    return pon_rm
+
+
+@pytest.fixture(scope="module")
+def pon_intf_id_onu_id():
+    pon_intf_id_onu_id = (1, 1)
+    return pon_intf_id_onu_id
+
+
+class MockRegistry:
+    """ Need to revisit this class"""
+    def __init__(self):
+        pass
+
+    def __call__(self, name):
+        main_mock = mock.MagicMock()
+
+        def get_args():
+            args = mock.Mock()
+            args.backend = "etcd"
+            args.etcd = "6.7.8.9:1245"
+            return args
+        main_mock.get_args = get_args
+        return main_mock
+
+
+@pytest.fixture(scope="module")
+@mock.patch('voltha.adapters.adtran_olt.resources.adtran_olt_resource_manager.registry', MockRegistry())
+def olt_rm(device_info, pon_rm):
+    with mock.patch('voltha.adapters.adtran_olt.resources.adtran_olt_resource_manager.AdtranPONResourceManager',
+                    pon_rm):
+        olt_rm = AdtranOltResourceMgr("test_id", "1.2.3.4:830", "--olt_model adtran", device_info)
+        olt_rm.resource_managers = mock.MagicMock()
+        return olt_rm
+
+
+def test_properties(olt_rm):
+    assert olt_rm.device_id == "test_id"
+
+
+def test_get_onu_id(olt_rm, pon_intf_id_onu_id):
+    olt_rm.resource_mgr.get_resource_id.return_value = 1
+    onu_id = olt_rm.get_onu_id(1)
+    assert onu_id == 1
+    olt_rm.resource_mgr.init_resource_map.assert_called_with(pon_intf_id_onu_id)
+
+
+def test_free_onu_id(olt_rm, pon_intf_id_onu_id):
+    olt_rm.free_onu_id(1, 1)
+    olt_rm.resource_mgr.free_resource_id.assert_called_with(1, AdtranPONResourceManager.ONU_ID, 1)
+    olt_rm.resource_mgr.remove_resource_map.assert_called_with(pon_intf_id_onu_id)
+
+
+def test_get_alloc_id_per_onu(olt_rm, pon_intf_id_onu_id):
+    olt_rm.resource_mgr.get_current_alloc_ids_for_onu.return_value = [1024]
+    alloc_id = olt_rm.get_alloc_id(pon_intf_id_onu_id)
+    assert alloc_id == 1024
+
+
+def test_get_alloc_id_not_available(olt_rm):
+    olt_rm.resource_mgr.get_current_alloc_ids_for_onu.return_value = []
+    olt_rm.resource_mgr.get_resource_id.return_value = []
+    alloc_id = olt_rm.get_alloc_id((1, 1))
+    assert alloc_id is None
+
+
+def test_get_alloc_id_fetch_from_kv(olt_rm, pon_intf_id_onu_id):
+    pon_intf = pon_intf_id_onu_id[0]
+    onu_id = pon_intf_id_onu_id[1]
+    alloc_id_list = [1024]
+    olt_rm.resource_mgr.get_current_alloc_ids_for_onu.return_value = []
+    olt_rm.resource_mgr.get_resource_id.return_value = alloc_id_list
+    alloc_id = olt_rm.get_alloc_id(pon_intf_id_onu_id)
+    assert alloc_id == 1024
+    olt_rm.resource_mgr.get_resource_id.assert_called_with(pon_intf, AdtranPONResourceManager.ALLOC_ID, num_of_id=1,
+                                                           onu_id=onu_id)
+    olt_rm.resource_mgr.update_alloc_ids_for_onu.assert_called_with(pon_intf_id_onu_id, alloc_id_list)
+
+
+def test_free_pon_resources_for_onu(olt_rm, pon_intf_id_onu_id):
+    pon_intf_id = pon_intf_id_onu_id[0]
+    onu_id = pon_intf_id_onu_id[1]
+    alloc_ids = [1024]
+    gem_ids = [2176]
+    olt_rm.resource_mgr.get_current_alloc_ids_for_onu.return_value = alloc_ids
+    olt_rm.resource_mgr.get_current_gemport_ids_for_onu.return_value = gem_ids
+    olt_rm.kv_store = mock.MagicMock()
+    olt_rm.kv_store[str((pon_intf_id, gem_ids[0]))] = "Dummy Value"
+    olt_rm.free_pon_resources_for_onu(pon_intf_id_onu_id)
+    olt_rm.resource_mgr.free_resource_id.assert_any_call(pon_intf_id, AdtranPONResourceManager.ALLOC_ID, alloc_ids,
+                                                         onu_id=onu_id)
+    olt_rm.resource_mgr.free_resource_id.assert_any_call(pon_intf_id, AdtranPONResourceManager.GEMPORT_ID, gem_ids)
+    olt_rm.resource_mgr.free_resource_id.assert_any_call(pon_intf_id, AdtranPONResourceManager.ONU_ID, onu_id)
+    olt_rm.resource_mgr.remove_resource_map.assert_called_with(pon_intf_id_onu_id)
+
+
+def test_free_pon_resources_for_onu_handles_exceptions(olt_rm, pon_intf_id_onu_id):
+    with pytest.raises(UnboundLocalError):
+        olt_rm.resource_mgr.get_current_alloc_ids_for_onu = mock.Mock(side_effect=KeyError('test'))
+        olt_rm.resource_mgr.get_current_gemport_ids_for_onu = mock.Mock(side_effect=KeyError('test'))
+        olt_rm.resource_mgr.free_resource_id = mock.Mock(side_effect=KeyError('test'))
+        olt_rm.kv_store = mock.MagicMock()
+        olt_rm.free_pon_resources_for_onu(pon_intf_id_onu_id)
+
+
+def test_get_current_gemport_ids_for_onu(olt_rm, pon_intf_id_onu_id):
+    pon_intf_id = pon_intf_id_onu_id[0]
+    olt_rm.resource_managers[pon_intf_id].get_current_gemport_ids_for_onu.return_value = 1024
+    gemport_ids = olt_rm.get_current_gemport_ids_for_onu(pon_intf_id_onu_id)
+    assert gemport_ids == 1024
+
+
+@pytest.mark.parametrize("input_value, expected_value", [([1024], 1024), (None, None)])
+def test_get_current_alloc_ids_for_onu(olt_rm, pon_intf_id_onu_id, input_value, expected_value):
+    pon_intf_id = pon_intf_id_onu_id[0]
+    olt_rm.resource_managers[pon_intf_id].get_current_alloc_ids_for_onu.return_value = input_value
+    allocation_ids = olt_rm.get_current_alloc_ids_for_onu(pon_intf_id_onu_id)
+    assert allocation_ids == expected_value
+
+
+def test_update_gemports_ponport_to_onu_map_on_kv_store(olt_rm):
+    expected_output = {'(1, 2177)': '2 3', '(1, 2176)': '2 3'}
+    gemport_list = [2176, 2177]
+    pon_port = 1
+    onu_id = 2
+    uni_id = 3
+    olt_rm.kv_store = {}
+    olt_rm.update_gemports_ponport_to_onu_map_on_kv_store(gemport_list, pon_port, onu_id, uni_id)
+    assert expected_output == olt_rm.kv_store
+
+
+def test_get_onu_uni_from_ponport_gemport(olt_rm):
+    olt_rm.kv_store = {'(1, 2177)': '2 3', '(1, 2176)': '2 3'}
+    (onu_id, uni_id) = olt_rm.get_onu_uni_from_ponport_gemport(1,2177)
+    assert (onu_id, uni_id) == (2, 3)
+
+
+@pytest.mark.parametrize("flow_store_cookie, flow_category, expected_flow_id", [("cookie2", None, 4, ), (None, "test", 4)])
+def test_get_flow_id(olt_rm, flow_store_cookie, flow_category, expected_flow_id):
+    pon_intf_id = 1
+    onu_id = 2
+    uni_id = 3
+    flows = [{"flow_category": "test", "flow_store_cookie": "cookie1"}, {"flow_store_cookie": "cookie2"}]
+    olt_rm.resource_managers[pon_intf_id].get_current_flow_ids_for_onu.return_value = [4]
+    olt_rm.resource_managers[pon_intf_id].get_flow_id_info.return_value = flows
+    returned_flow_id = olt_rm.get_flow_id(pon_intf_id, onu_id, uni_id, flow_store_cookie, flow_category)
+    assert returned_flow_id == expected_flow_id
+
+
+def test_get_flow_id_handles_exception(olt_rm):
+    pon_intf_id = 1
+    onu_id = 2
+    uni_id = 3
+    flow_store_cookie = "dummy"
+    flow_category = "test"
+    olt_rm.resource_managers[pon_intf_id].get_current_flow_ids_for_onu.return_value = None
+    olt_rm.resource_managers[pon_intf_id].get_flow_id_info.return_value = [10, 20]
+    olt_rm.resource_managers[pon_intf_id].get_flow_id_info.return_value = None
+    olt_rm.resource_managers[pon_intf_id].get_resource_id.return_value = 4
+    returned_flow_id = olt_rm.get_flow_id(pon_intf_id, onu_id, uni_id, flow_store_cookie, flow_category)
+    assert returned_flow_id == 4
+    olt_rm.resource_managers[pon_intf_id].update_flow_id_for_onu.assert_called_with((1, 2, 3), 4)
+
+
+def test_get_current_flow_ids_for_uni(olt_rm):
+    pon_intf_id = 1
+    onu_id = 2
+    uni_id = 3
+    olt_rm.resource_managers[pon_intf_id].get_current_flow_ids_for_onu.return_value = 4
+    flow_id = olt_rm.get_current_flow_ids_for_uni(pon_intf_id, onu_id, uni_id)
+    assert flow_id == 4
+
+
+def test_update_flow_id_info_for_uni(olt_rm):
+    pon_intf_id = 1
+    onu_id = 2
+    uni_id = 3
+    pon_intf_onu_id = (pon_intf_id, onu_id, uni_id)
+    flow_id = 4
+    flow_data = {"Test": "Dummy"}
+    olt_rm.update_flow_id_info_for_uni(pon_intf_id, onu_id, uni_id, flow_id, flow_data)
+    olt_rm.resource_managers[pon_intf_id].update_flow_id_info_for_onu.assert_called_with(pon_intf_onu_id, flow_id,
+                                                                                         flow_data)
diff --git a/voltha/adapters/adtran_olt/test/resources/test_adtran_resource_manager.py b/voltha/adapters/adtran_olt/test/resources/test_adtran_resource_manager.py
new file mode 100644
index 0000000..e4fc648
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/resources/test_adtran_resource_manager.py
@@ -0,0 +1,243 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 mock import Mock, patch
+import pytest
+import json
+from bitstring import BitArray
+
+from voltha.adapters.adtran_olt.resources.adtran_resource_manager import AdtranPONResourceManager
+from voltha.adapters.adtran_olt.test.resources.test_adtran_olt_resource_manager import MockRegistry
+
+
+@pytest.fixture()
+@patch('common.tech_profile.tech_profile.registry', MockRegistry())
+def resource_manager():
+    rm = AdtranPONResourceManager("xgspon", "--olt_model adtran", "test_id", "etcd", "6.7.8.9", "1245")
+    rm.init_default_pon_resource_ranges()
+    rm._kv_store = Mock()
+    return rm
+
+
+@pytest.fixture(scope="module")
+def resource():
+    return {'onu_map': {'1': {'end_idx': 6, 'start_idx': 1, 'pool': '11111'},
+                        '2': {'end_idx': 5, 'start_idx': 1, 'pool': '1111'}}, 'pon_intf_id': 1}
+
+
+@pytest.fixture(scope="module")
+def resource_bin():
+    return {'onu_map': {'1': {'end_idx': 6, 'start_idx': 1, 'pool': BitArray('0b11000')},
+                        '2': {'end_idx': 5, 'start_idx': 1, 'pool': BitArray('0b1100')}}, 'pon_intf_id': 1}
+
+
+def test_resource_manager_initialization(resource_manager):
+    assert resource_manager.ONU_MAP == 'onu_map'
+
+
+def test__format_resource(resource_manager):
+    test_str = resource_manager._format_resource(1, 1, 4)
+    resource = json.loads(test_str)
+    assert resource[AdtranPONResourceManager.START_IDX] == 1
+    assert resource[AdtranPONResourceManager.END_IDX] == 4
+    assert resource[AdtranPONResourceManager.PON_INTF_ID] == 1
+    assert resource[AdtranPONResourceManager.POOL] == "000"
+
+
+def test__format_map_resource(resource_manager):
+    resource = dict()
+    resource[1] = [1, 2, 3, 4, 5]
+    resource[2] = [1, 2, 3, 4]
+    resource[3] = [1, 2, 3]
+    resource[4] = [1, 2]
+    test_str = resource_manager._format_map_resource(1, resource)
+    pon_map = json.loads(test_str)
+    pon_map["onu_map"]['1'][AdtranPONResourceManager.END_IDX] == 6
+    pon_map["onu_map"]['2'][AdtranPONResourceManager.END_IDX] == 5
+    pon_map["onu_map"]['3'][AdtranPONResourceManager.END_IDX] == 4
+    pon_map["onu_map"]['4'][AdtranPONResourceManager.END_IDX] == 3
+
+
+def test__get_resource_returns_none_when_path_is_not_available_in_kv(resource_manager):
+    resource_manager._kv_store.get_from_kv_store.return_value = None
+    result = resource_manager._get_resource("test", 1)
+    assert result is None
+
+
+@pytest.mark.parametrize("resource_map, path, onu_id",
+                         [(resource(), '1/alloc_id/1', 1),
+                          (resource()['onu_map']['2'], '1/onu_id/2', 2)])
+def test__get_resource_returns_resources(resource_manager, resource_map, path, onu_id):
+    resource_manager._kv_store.get_from_kv_store.return_value = json.dumps(resource_map)
+    result = resource_manager._get_resource(path, onu_id)
+    if 'alloc_id' in path:
+        assert result[AdtranPONResourceManager.ONU_MAP]['1'][AdtranPONResourceManager.POOL] == BitArray('0b11111')
+    else:
+        assert result[AdtranPONResourceManager.POOL] == BitArray('0b1111')
+
+
+def test__release_id(resource_manager, resource_bin):
+    resource_manager._release_id(resource_bin, 2, 1)
+    assert resource_bin['onu_map']['1']['pool'] == BitArray('0b10000')
+
+
+@pytest.mark.parametrize("onu_id", [None, 1])
+def test__generate_next_id(resource_manager, resource_bin, onu_id):
+    if onu_id is None:
+        result = resource_manager._generate_next_id(resource_bin[AdtranPONResourceManager.ONU_MAP]['2'])
+        assert result == 3
+    else:
+        result = resource_manager._generate_next_id(resource_bin, onu_id)
+        assert result == 2
+
+
+@pytest.mark.parametrize("path, onu_id", [('1/onu_id/2', None)])
+def test__update_resource(resource_manager, resource, resource_bin, path, onu_id):
+    resource_manager._kv_store.update_to_kv_store = Mock()
+    if 'alloc_id' in path:
+        resource_manager._update_resource(path, resource, onu_id)
+    else:
+        resource_manager._update_resource(path, resource_bin['onu_map']['1'], onu_id)
+    resource_manager._kv_store.update_to_kv_store.assert_called()
+
+
+def test_clear_device_resource_pool(resource_manager):
+    resource_manager.intf_ids = [1]
+    resource_manager.clear_resource_id_pool = Mock()
+    resource_manager.clear_device_resource_pool()
+    resource_manager.clear_resource_id_pool.assert_any_call(pon_intf_id=1,
+                                                            resource_type=AdtranPONResourceManager.ONU_ID)
+    resource_manager.clear_resource_id_pool.assert_any_call(pon_intf_id=1,
+                                                            resource_type=AdtranPONResourceManager.ALLOC_ID)
+    resource_manager.clear_resource_id_pool.assert_any_call(pon_intf_id=1,
+                                                            resource_type=AdtranPONResourceManager.GEMPORT_ID)
+
+
+@pytest.mark.parametrize("resource_map", [None, {}])
+def test_init_resource_id_pool(resource_manager, resource_map):
+    resource_manager._get_resource = Mock()
+    resource_manager._get_resource.return_value = None
+    resource_manager._format_resource = Mock()
+    resource_manager._format_resource.return_value = {}
+    resource_manager._kv_store.update_to_kv_store.return_value = True
+    status = resource_manager.init_resource_id_pool(pon_intf_id=1, resource_type=AdtranPONResourceManager.ONU_ID,
+                                                    resource_map=resource_map)
+    resource_manager._kv_store.update_to_kv_store.assert_called()
+    assert status is True
+
+
+def test_init_resource_id_pool_returns_false_when_get_path_is_none(resource_manager):
+    resource_manager._get_path = Mock()
+    resource_manager._get_path.return_value = None
+    status = resource_manager.init_resource_id_pool(pon_intf_id=1, resource_type=AdtranPONResourceManager.ONU_ID,
+                                                    resource_map=None)
+    assert status is False
+
+
+def test_init_resource_id_pool_returns_true_when_get_resources_is_not_none(resource_manager):
+    resource_manager._get_path = Mock()
+    resource_manager._get_path.return_value = "test"
+    resource_manager._get_resource = Mock()
+    resource_manager._get_resource.return_value = "test"
+    status = resource_manager.init_resource_id_pool(pon_intf_id=1, resource_type=AdtranPONResourceManager.ONU_ID,
+                                                    resource_map=None)
+    assert status is True
+
+
+def test_get_resource_id_validates_num_of_ids(resource_manager):
+    result = resource_manager.get_resource_id(1, AdtranPONResourceManager.ONU_ID, None, 0)
+    assert result is None
+
+
+def test_get_resource_id_returns_none_when_get_path_returns_none(resource_manager):
+    resource_manager._get_path = Mock()
+    resource_manager._get_path.return_value = None
+    result = resource_manager.get_resource_id(1, "test", None, 2)
+    assert result is None
+
+
+def test_get_resource_id_returns_none_when_resource_not_available(resource_manager):
+    resource_manager._get_path = Mock()
+    resource_manager._get_path.return_value = "test"
+    resource_manager._get_resource = Mock()
+    resource_manager._get_resource.return_value = "test"
+    result = resource_manager.get_resource_id(1, "test", None, 2)
+    assert result is None
+
+
+@pytest.mark.parametrize("resource_type, next_id, num_of_id, expected_id",
+                         [(AdtranPONResourceManager.ONU_ID, 1, 1, 1), (AdtranPONResourceManager.FLOW_ID, 1, 1, 1),
+                          (AdtranPONResourceManager.GEMPORT_ID, 1, 2, [1, 1]),
+                          (AdtranPONResourceManager.GEMPORT_ID, 1, 1, 1),
+                          (AdtranPONResourceManager.ALLOC_ID, 1, 2, [1, 1]),
+                          (AdtranPONResourceManager.ALLOC_ID, 1, 1, 1)])
+def test_get_resource_id_valid_data(resource_manager, resource_type, next_id, num_of_id, expected_id):
+    resource_manager._get_path = Mock()
+    resource_manager._get_path.return_value = "test"
+    resource_manager._get_resource = Mock()
+    resource_manager._get_resource.return_value = "test"
+    resource_manager._generate_next_id = Mock()
+    resource_manager._generate_next_id.return_value = next_id
+    resource_manager._update_resource = Mock()
+    result = resource_manager.get_resource_id(1, resource_type, 1, num_of_id)
+    assert result == expected_id
+    if resource_type is not AdtranPONResourceManager.ALLOC_ID:
+        resource_manager._generate_next_id.assert_any_call("test")
+    else:
+        resource_manager._generate_next_id.assert_any_call("test", 1)
+
+
+def test_init_resource_id_pool_handles_exception(resource_manager):
+    resource_manager._get_resource = Mock()
+    resource_manager._get_resource.side_effect = Exception("Test")
+    status = resource_manager.init_resource_id_pool(pon_intf_id=1, resource_type=AdtranPONResourceManager.ONU_ID)
+    assert status is False
+
+
+def test_init_device_resource_pool(resource_manager):
+    """Need to revisit its tests"""
+    resource_manager.intf_ids = [1]
+    resource_manager.init_resource_id_pool = Mock()
+    resource_manager.init_device_resource_pool()
+    resource_manager.init_resource_id_pool.assert_any_call(
+                pon_intf_id=1,
+                resource_type=AdtranPONResourceManager.ONU_ID,
+                start_idx=resource_manager.pon_resource_ranges[AdtranPONResourceManager.ONU_ID_START_IDX],
+                end_idx=resource_manager.pon_resource_ranges[AdtranPONResourceManager.ONU_ID_END_IDX])
+    resource_manager.init_resource_id_pool.assert_any_call(
+                pon_intf_id=1,
+                resource_type=AdtranPONResourceManager.GEMPORT_ID,
+                start_idx=resource_manager.pon_resource_ranges[AdtranPONResourceManager.GEMPORT_ID_START_IDX],
+                end_idx=resource_manager.pon_resource_ranges[AdtranPONResourceManager.GEMPORT_ID_END_IDX])
+
+
+def test_free_resource_id_invalid_resource_type(resource_manager):
+    result = resource_manager.free_resource_id(1, "test", None)
+    assert result is False
+
+
+@pytest.mark.parametrize("resource_type", [AdtranPONResourceManager.ONU_ID,
+                                           AdtranPONResourceManager.GEMPORT_ID, AdtranPONResourceManager.ALLOC_ID])
+def test_free_resource_id__valid_data(resource_manager, resource_type):
+    resource_manager._release_id = Mock()
+    resource_manager._update_resource = Mock()
+    resource_manager._update_resource.return_value = True
+    resource_manager._get_path = Mock()
+    resource_manager._get_path.return_value = "test1"
+    resource_manager._get_resource = Mock()
+    resource_manager._get_resource.return_value = "test2"
+    result = resource_manager.free_resource_id(1, resource_type, [1])
+    assert result is True
+
diff --git a/voltha/adapters/adtran_olt/test/resources/test_adtranolt_platform.py b/voltha/adapters/adtran_olt/test/resources/test_adtranolt_platform.py
new file mode 100644
index 0000000..5f26694
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/resources/test_adtranolt_platform.py
@@ -0,0 +1,504 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import voltha.adapters.adtran_olt.resources.adtranolt_platform as platform
+import pytest
+
+
+"""
+These test functions test the function "mk_uni_port_num()"
+in the class "adtran_platform()". For the tests with simple inputs,
+answers are reached by simply adding the bit shifted arguments
+because there aren't any overlapping 1 bits, so this should be the
+same as a logical OR. For the rest of the tests I manually calculated
+what the answers should be for a variety of scenarios.
+"""
+
+@pytest.fixture()
+def test():
+    return platform.adtran_platform()
+
+#simple args, and no arg for the parameter that has a default value
+def test_adtran_platform_mk_uni_port_num(test):
+    output = test.mk_uni_port_num(1, 1)
+    assert output == 2**11 + 2**4
+
+#simple args including one for the parameter that has a default value
+def test_adtran_platform_mk_uni_port_num_not_default(test):
+    output = test.mk_uni_port_num(1, 1, 1)
+    assert output == 2**11 + 2**4 + 1
+
+#tests scenario where the logical OR doesn't equal the sum
+def test_adtran_platform_mk_uni_port_num_sum_dne_bitwise_or(test):
+    output = test.mk_uni_port_num(1, 128, 1)
+    assert output == 128 * 2**4 +1
+
+#tests what happens when a negative number is introduced
+def test_adtran_platform_mk_uni_port_num_negative(test):
+    output = test.mk_uni_port_num(-1, 1, 1)
+    assert output == -2031
+
+#tests what happens when 2 negative numbers are introduced
+def test_adtran_platform_mk_uni_port_num_negatives(test):
+    output = test.mk_uni_port_num(-1, -1, 1)
+    assert output == -15
+
+#tests what happens when strings are passed as parameters
+def test_adtran_platform_mk_uni_port_num_strings(test):
+    with pytest.raises(TypeError):
+        output = test.mk_uni_port_num("test", "test", "test")
+
+#tests what happens when nothing is passed as a parameter
+def test_adtran_platform_mk_uni_port_num_no_args(test):
+    with pytest.raises(TypeError):
+        output = test.mk_uni_port_num()
+
+
+"""
+These test the function "uni_id_from_uni_port()" in the class
+"adtran_platform()". Several of these tests pass in a number between 0
+and 15 which should all return the same number back. Two more pass in huge
+numbers with the same last four digits. The one with all 1's returns 15 and
+the one with all 0's returns 0. Finally, I made sure that passing
+in either the wrong type of argument or none at all threw the
+appropriate type error.
+"""
+
+#this functions a logical AND of 15 and 15 which should stay the same
+def test_adtran_platform_uni_id_from_uni_port_same_num(test):
+    output = test.uni_id_from_uni_port(15)
+    assert output == 15
+
+#this function tests the logical AND of 15 and a huge num (all 1's in binary)
+def test_adtran_platform_uni_id_from_uni_port_huge_num_ones(test):
+    output = test.uni_id_from_uni_port(17179869183)
+    assert output == 15
+
+#logical AND of 15 and 0
+def test_adtran_platform_uni_id_from_uni_port_zero(test):
+    output = test.uni_id_from_uni_port(0)
+    assert output == 0
+
+#logical AND of 15 and a huge num (last 4 digits 0's in binary)
+def test_adtran_platfrom_uni_id_from_uni_port_huge_num_zeros(test):
+    output = test.uni_id_from_uni_port(17179869168)
+    assert output == 0
+
+#logical AND of 12 and 15
+def test_adtran_platfrom_uni_id_from_uni_port_twelve(test):
+    output = test.uni_id_from_uni_port(12)
+    assert output == 12
+
+#logical AND of 9 and 15
+def test_adtran_platform_uni_id_from_uni_port_nine(test):
+    output = test.uni_id_from_uni_port(9)
+    assert output == 9
+
+#logical AND of 3 and 15
+def test_adtran_platform_uni_id_from_uni_port_three(test):
+    output = test.uni_id_from_uni_port(3)
+    assert output == 3
+
+#passing in a string
+def test_adtran_platform_uni_id_from_uni_port_string(test):
+    with pytest.raises(TypeError):
+        output = test.uni_id_from_uni_port("test")
+
+#NO INPUTS AT ALL
+def test_adtran_platform_uni_id_from_uni_port_no_args(test):
+    with pytest.raises(TypeError):
+        output = test.uni_id_from_uni_port()
+
+
+"""
+These test functions test the function "mk_uni_port_num()"
+For the tests with simple inputs, answers are reached by simply
+adding the bit shifted arguments because there aren't any
+overlapping 1 bits, so this should be the same as a logical OR.
+For the rest of the tests I manually calculated what the answers
+should be for a variety of scenarios.
+"""
+
+#simple args, and no arg for the parameter that has a default value
+def test_mk_uni_port_num_default():
+    output = platform.mk_uni_port_num(1, 1)
+    assert output == 2**11 + 2**4
+
+#simple args including one for the parameter that has a default value
+def test_mk_uni_port_num_not_default():
+    output = platform.mk_uni_port_num(1, 1, 1)
+    assert output == 2**11 + 2**4 + 1
+
+#tests scenario where the logical OR doesn't equal the sum
+def test_mk_uni_port_num_sum_dne_bitwise_or():
+    output = platform.mk_uni_port_num(1, 128, 1)
+    assert output == 128 * 2**4 + 1
+
+#tests what happens when a negative number is introduced
+def test_mk_uni_port_num_negative():
+    output = platform.mk_uni_port_num(-1, 1, 1)
+    assert output == -2031
+
+#tests what happens when 2 negative numbers are introduced
+def test_mk_uni_port_num_negatives():
+    output = platform.mk_uni_port_num(-1, -1, 1)
+    assert output == -15
+
+#tests what happens when strings are passed as parameters
+def test_mk_uni_port_num_strings():
+    with pytest.raises(TypeError):
+        output = platform.mk_uni_port_num("test", "test", "test")
+
+#tests what happens when nothing is passed as a parameter
+def test_mk_uni_port_num_no_args():
+    with pytest.raises(TypeError):
+        output = platform.mk_uni_port_num()
+
+
+"""
+Several of these tests pass in a number between 0 and 15 which
+should all return the same number back. Two more pass in huge numbers
+with the same last four digits. The one with all 1's returns 15 and
+the one with all 0's returns 0. Finally, I made sure that passing
+in either the wrong type of argument or none at all threw the
+appropriate type error.
+"""
+
+#this functions a logical AND of 15 and 15 which should stay the same
+def test_uni_id_from_uni_port_same_num():
+    output = platform.uni_id_from_uni_port(15)
+    assert output == 15
+
+#this function tests the logical AND of 15 and a huge num (all 1's in binary)
+def test_uni_id_from_uni_port_huge_num_ones():
+    output = platform.uni_id_from_uni_port(17179869183)
+    assert output == 15
+
+#logical AND of 15 and 0
+def test_uni_id_from_uni_port_zero():
+    output = platform.uni_id_from_uni_port(0)
+    assert output == 0
+
+#logical AND of 15 and a huge num (last 4 digits 0's in binary)
+def test_uni_id_from_uni_port_huge_num_zeros():
+    output = platform.uni_id_from_uni_port(17179869168)
+    assert output == 0
+
+#logical AND of 12 and 15
+def test_uni_id_from_uni_port_twelve():
+    output = platform.uni_id_from_uni_port(12)
+    assert output == 12
+
+#logical AND of 9 and 15
+def test_uni_id_from_uni_port_nine():
+    output = platform.uni_id_from_uni_port(9)
+    assert output == 9
+
+#logical AND of 3 and 15
+def test_uni_id_from_uni_port_three():
+    output = platform.uni_id_from_uni_port(3)
+    assert output == 3
+
+#passing in a string
+def test_uni_id_from_uni_port_string():
+    with pytest.raises(TypeError):
+        output = platform.uni_id_from_uni_port("test")
+
+#NO INPUTS AT ALL
+def test_uni_id_from_uni_port_no_args():
+    with pytest.raises(TypeError):
+        output = platform.uni_id_from_uni_port()
+
+
+"""
+The first few tests try a few different scenarios to make sure that the bit shifting
+and logical AND are working as expected. There should never be a result that is
+larger than 15. Then I checked to make sure that passing the wrong argument or no
+arguments at all throws the expected type error.
+"""
+
+#test with the smallest number that remains non-zero after bit shift
+def test_intf_id_from_uni_port_num_smallest():
+    output = platform.intf_id_from_uni_port_num(2048)
+    assert output == 1
+
+#test with a number with different bits 1 through 11 to make sure they don't affect result
+def test_intf_id_from_uni_port_num_big():
+    output = platform.intf_id_from_uni_port_num(3458)
+    assert output == 1
+
+#test with massive number where bits 15 through 12 are 1010
+def test_intf_id_from_uni_port_num_massive():
+    output = platform.intf_id_from_uni_port_num(22459)
+    assert output == 10
+
+#test with smallest number that remains positive after bit shift, but is zero after the AND
+def test_intf_id_from_uni_port_num_big_zero():
+    output = platform.intf_id_from_uni_port_num(32768)
+    assert output == 0
+
+#test with largest number that gets bit shifted down to zero
+def test_intf_id_from_uni_port_num_bit_shift_to_zero():
+    output = platform.intf_id_from_uni_port_num(2047)
+    assert output == 0
+
+#test with a string passed in
+def test_intf_id_from_uni_port_num_string():
+    with pytest.raises(TypeError):
+        output = platform.intf_id_from_uni_port_num("test")
+
+#test with no args passed in
+def test_intf_id_from_uni_port_num_no_args():
+    with pytest.raises(TypeError):
+        output = platform.intf_id_from_uni_port_num()
+
+
+"""
+i did the standard tests to make sure that it returned the expected values
+for random normal cases and the max and min cases. I also checked args
+that were too big and too small. Then I made sure that the first arg truly
+didn't matter and that the default value of the last parameter worked.
+Finally, I checked to make sure string args and no args at all threw the
+appropriate errors.
+"""
+
+#testing with all args at 0 which should return 1024
+def test_mk_alloc_id_all_zeros():
+    output = platform.mk_alloc_id(0, 0, 0)
+    assert output == 1024
+
+#testing with onu_id out of bounds
+def test_mk_alloc_id_onu_id_too_big():
+    with pytest.raises(AssertionError):
+        output = platform.mk_alloc_id(0, 128, 0)
+
+#testing with idx out of bounds
+def test_mk_alloc_id_idx_idx_too_big():
+    with pytest.raises(AssertionError):
+        output = platform.mk_alloc_id(0, 0, 5)
+
+#test with both being negative
+def test_mk_alloc_id_both_args_negative():
+    with pytest.raises(AssertionError):
+        output = platform.mk_alloc_id(0, -1, -1)
+
+#testing with both parameters at their respective max
+def test_mk_alloc_id_both_max():
+    output = platform.mk_alloc_id(0, 127, 4)
+    assert output == 2175
+
+#testing with random values in the middle of their ranges and a string as the first arg
+def test_mk_alloc_id_random_args():
+    output = platform.mk_alloc_id("test", 100, 2)
+    assert output == 1636
+
+#testing with testing with the default value
+def test_mk_alloc_id_default_value():
+    output = platform.mk_alloc_id(0, 100)
+    assert output == 1124
+
+#testing with strings passed in
+def test_mk_alloc_id_strings():
+    with pytest.raises(AssertionError):
+        output = platform.mk_alloc_id("test", "test", "test")
+
+#testing with no args passed in
+def test_mk_alloc_id_no_args():
+    with pytest.raises(TypeError):
+        output = platform.mk_alloc_id()
+
+
+"""
+Just some basic tests to get coverage here.This function probably only
+exists to support backwards compatibility.
+"""
+
+#inputing a negative number
+def test_intf_id_from_nni_port_num_negative():
+    output = platform.intf_id_from_nni_port_num(-1)
+    assert output == -1
+
+#inputing zero
+def test_intf_id_from_nni_port_num_zero():
+    output = platform.intf_id_from_nni_port_num(0)
+    assert output == 0
+
+#inputing a positive number
+def test_intf_id_from_nni_port_num_positive():
+    output = platform.intf_id_from_nni_port_num(1)
+    assert output == 1
+
+#no args
+def test_intf_id_from_nni_port_num_no_args():
+    with pytest.raises(TypeError):
+        output = platform.intf_id_from_nni_port_num()
+
+
+"""
+This function is a pretty simple else if statement, so I just checked
+the edges of the ranges in the function, and the three 'out of bounds'
+zones. Then I tried the standard passing in the wrong type and passing
+nothing at all
+"""
+
+#testing the edges of the first range in the if statement
+def test_intf_id_to_intf_type_bottom_of_first_range():
+    output = platform.intf_id_to_intf_type(5)
+    assert output == platform.Port.PON_OLT
+
+#testing the edges of the range in the if statement
+def test_intf_id_to_intf_type_top_of_first_range():
+    output = platform.intf_id_to_intf_type(20)
+    assert output == platform.Port.PON_OLT
+
+#testing the edges of the range in the elif statement
+def test_intf_id_to_intf_type_bottom_of_second_range():
+    output = platform.intf_id_to_intf_type(1)
+    assert output == platform.Port.ETHERNET_NNI
+
+#testing the edges of the range in the elif statement
+def test_intf_id_to_intf_type_top_of_second_range():
+    output = platform.intf_id_to_intf_type(4)
+    assert output == platform.Port.ETHERNET_NNI
+
+#testing a value above the top of the higher range
+def test_intf_id_to_intf_type_out_of_range_high():
+    with pytest.raises(Exception):
+        output = platform.intf_id_to_intf_type(20.1)
+
+#testing a value between the ranges
+def test_intf_id_to_intf_type_out_of_range_mid():
+    with pytest.raises(Exception):
+        output = platform.intf_id_to_intf_type(4.5)
+
+#testing a value below the bottom of the lowest range
+def test_intf_id_to_intf_type_out_of_range_low():
+    with pytest.raises(Exception):
+        output = platform.intf_id_to_intf_type(0.9)
+
+#testing with a string passed in
+def test_intf_id_to_intf_type_string():
+    with pytest.raises(Exception):
+        output = platform.intf_id_to_intf_type("test")
+
+#testing with nothing passed in
+def test_intf_id_to_intf_type_no_args():
+    with pytest.raises(Exception):
+        output = platform.intf_id_to_intf_type()
+
+
+"""
+I tested all six of the values in the list, and each time tested
+a different value for the first parameter to make sure that it didn't matter.
+Then I tested a wrong value to make sure it returned false, and tested a
+string arg and no args.
+"""
+
+#testing the first value in the list of acceptable values
+def test_is_upstream_first_value():
+    output = platform.is_upstream(1, 1)
+    assert output == True
+
+#testing the second value in the list of acceptable values
+def test_is_upstream_second_value():
+    output = platform.is_upstream(18, 2)
+    assert output == True
+
+#testing the third value in the list of acceptable values
+def test_is_upstream_third_value():
+    output = platform.is_upstream(-800, 3)
+    assert output == True
+
+#testing the fourth value in the list of acceptable values
+def test_is_upstream_fourth_value():
+    output = platform.is_upstream(2.5, 4)
+
+#testing the fifth value in the list of acceptable values
+def test_is_upstream_fifth_value():
+    output = platform.is_upstream("test", 65533)
+    assert output == True
+
+#testing the sixth value in the list of acceptable values
+def test_is_upstream_sixth_value():
+    output = platform.is_upstream(2456, 4294967293)
+    assert output == True
+
+#testing a value that does not exist in the list of acceptable values
+def test_is_upstream_wrong_value():
+    output = platform.is_upstream(8, 19)
+    assert output == False
+
+#testing a string being passed in as an argument
+def test_is_upstream_string():
+    output = platform.is_upstream(42, "test")
+    assert output == False
+
+#testing nothing being passed in as an argument
+def test_is_upstream_no_args():
+    with pytest.raises(TypeError):
+        output = platform.is_upstream()
+
+
+"""
+I tested all six values again to make sure they returned false, then I tried
+a random value to make sure it returned true. Then, I tried passing a string
+and nothing again too.
+"""
+
+#testing with the first value in the list of unacceptable values
+def test_is_downstream_first_value():
+    output = platform.is_downstream(7, 1)
+    assert output == False
+
+#testing with the second value in the list of unacceptable values
+def test_is_downstream_second_value():
+    output = platform.is_downstream(34, 2)
+    assert output == False
+
+#testing with the third value in the list of unacceptable values
+def test_is_downstream_third_value():
+    output = platform.is_downstream("test", 3)
+    assert output == False
+
+#testing with the fourth value in the list of unacceptable values
+def test_is_downstream_fourth_value():
+    output = platform.is_downstream(68, 4)
+    assert output == False
+
+#testing with the fifth value in the list of unacceptable values
+def test_is_downstream_fifth_value():
+    output = platform.is_downstream(-345, 65533)
+    assert output == False
+
+#testing with the sixth value in the list of unacceptable values
+def test_is_downstream_sixth_value():
+    output = platform.is_downstream(.09, 4294967293)
+    assert output == False
+
+#testing a value that isn't in the list of unacceptable values
+def test_is_downstream_wrong_right_value():
+    output = platform.is_downstream(24, -65)
+    assert output == True
+
+#testing a string being passed in as an argument
+def test_is_downstream_string():
+    output = platform.is_downstream(11, "test")
+    assert output == True
+
+#testing nothing being passed in as an argument
+def test_is_downstream_no_args():
+    with pytest.raises(TypeError):
+        output = platform.is_downstream()
+
+
diff --git a/voltha/adapters/adtran_olt/test/test_adtran_device_handler.py b/voltha/adapters/adtran_olt/test/test_adtran_device_handler.py
new file mode 100644
index 0000000..95c833b
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/test_adtran_device_handler.py
@@ -0,0 +1,326 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Adtran generic VOLTHA device handler
+"""
+import pytest
+import argparse
+import pytest_twisted
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed, Deferred
+from twisted.internet.error import ConnectError
+import mock
+from voltha.adapters.adtran_olt.test.net.mock_netconf_client import MockNetconfClient
+from voltha.adapters.adtran_olt.adtran_device_handler import (
+    AdtranDeviceHandler, AdtranNetconfClient, AdtranRestClient, AdminState,
+    AdapterAlarms, OperStatus, ConnectStatus,
+    DEFAULT_MULTICAST_VLAN, _DEFAULT_NETCONF_PORT, _DEFAULT_RESTCONF_PORT,
+    DEFAULT_PON_AGENT_TCP_PORT, DEFAULT_PIO_TCP_PORT
+)
+
+
+@pytest.fixture()
+def device():
+    dev = mock.MagicMock()
+    dev.ipv4_address = '1.2.3.4'
+    dev.extra_args = '-u NCUSER -p NCPASS -U RUSER -P RPASS'
+    dev.images.image[0].version = b'test-version'
+    dev.hardware_version = 'A'
+    dev.serial_number = 'LBADTN123456789'
+    dev.id = b'test-id'
+    dev.admin_state = AdminState.ENABLED
+    yield dev
+
+
+@pytest.fixture()
+def simple_handler(device):
+    adapter = mock.MagicMock()
+    adapter.adapter_agent.get_device.return_value = device
+    adapter.adapter_agent.create_logical_device.return_value = device
+    adapter.adapter_agent.get_logical_device.return_value = device
+    AdtranDeviceHandler.NC_CLIENT = MockNetconfClient
+    yield AdtranDeviceHandler(**{
+        "adapter": adapter,
+        "device-id": '123'
+    })
+    AdtranDeviceHandler.NC_CLIENT = AdtranNetconfClient
+
+
+def test_properties(simple_handler):
+    assert simple_handler.netconf_client is None
+    assert simple_handler.rest_client is None
+    assert str(simple_handler) == "AdtranDeviceHandler: None"
+
+    simple_handler.northbound_ports = {1: '1'}
+    simple_handler.southbound_ports = {10: '10'}
+    assert ['1', '10'] == list(simple_handler.all_ports)
+
+
+def test_evcs(simple_handler):
+    evc = mock.MagicMock()
+    evc.name = "Magical EVC"
+    assert simple_handler.evcs == []
+    simple_handler.add_evc(evc)
+    simple_handler.add_evc(evc)
+    assert simple_handler.evcs == [evc]
+    simple_handler.remove_evc(evc)
+    simple_handler.remove_evc(evc)
+    assert simple_handler.evcs == []
+
+
+@pytest.mark.parametrize('method, args', [
+    ('is_uni_port', (1,)),
+    ('is_pon_port', (1,)),
+    ('get_port_name', (1,)),
+    ('initialize_resource_manager', ()),
+    ('packet_out', (None, None))
+])
+def test_abstract_method(simple_handler, method, args):
+    with pytest.raises(NotImplementedError):
+        getattr(simple_handler, method)(*args)
+
+
+@pytest.fixture(params=[
+    "'device_information', (None,)",
+    "'enumerate_northbound_ports', (None,)",
+    "'process_northbound_ports', (None, None)",
+    "'enumerate_southbound_ports', (None,)",
+    "'process_southbound_ports', (None, None)",
+    "'complete_device_specific_activation', (None, None)",
+    "'ready_network_access', ()",
+])
+def abstract_inline_callbacks(simple_handler, request):
+    method, args = eval(request.param)
+    d = getattr(simple_handler, method)(*args)
+    assert isinstance(d, Deferred)
+    reactor.callLater(0.0, d.callback, "Test Abstract Callback")
+    return pytest.blockon(d)
+
+
+def test_abstract_inline_callbacks(abstract_inline_callbacks):
+    assert abstract_inline_callbacks == "Test Abstract Callback"
+
+
+def test_parser_port_number():
+    args = AdtranDeviceHandler.PARSER.parse_args('')
+    assert args.nc_port == _DEFAULT_NETCONF_PORT
+    assert args.rc_port == _DEFAULT_RESTCONF_PORT
+    assert args.zmq_port == DEFAULT_PON_AGENT_TCP_PORT
+    assert args.pio_port == DEFAULT_PIO_TCP_PORT
+
+    args = AdtranDeviceHandler.PARSER.parse_args('-t 1'.split())
+    assert args.nc_port == 1
+
+    args = AdtranDeviceHandler.PARSER.parse_args('-t 65535'.split())
+    assert args.nc_port == 65535
+
+    with pytest.raises(argparse.ArgumentTypeError):
+        AdtranDeviceHandler.PARSER.parse_args('-t 0'.split())
+
+    with pytest.raises(argparse.ArgumentTypeError):
+        AdtranDeviceHandler.PARSER.parse_args('-t 65536'.split())
+
+    with pytest.raises(argparse.ArgumentTypeError):
+        AdtranDeviceHandler.PARSER.parse_args('-t sixty-six'.split())
+
+
+def test_parser_vlan():
+    args = AdtranDeviceHandler.PARSER.parse_args('')
+    assert args.multicast_vlan == [DEFAULT_MULTICAST_VLAN]
+
+    args = AdtranDeviceHandler.PARSER.parse_args('-M 1 1028 4094'.split())
+    assert args.multicast_vlan == [1, 1028, 4094]
+
+    with pytest.raises(argparse.ArgumentTypeError):
+        AdtranDeviceHandler.PARSER.parse_args('-M 4095'.split())
+
+    with pytest.raises(argparse.ArgumentTypeError):
+        AdtranDeviceHandler.PARSER.parse_args('-M 0'.split())
+
+    with pytest.raises(argparse.ArgumentTypeError):
+        AdtranDeviceHandler.PARSER.parse_args('-M forty-six'.split())
+
+
+@pytest.mark.parametrize('host_and_port', [None, '1.2.3.4:22'])
+def test_parse_provisioning_options(simple_handler, device, host_and_port):
+    assert simple_handler.netconf_username == ''
+    assert simple_handler.netconf_password == ''
+    assert simple_handler.rest_username == ''
+    assert simple_handler.rest_password == ''
+    if host_and_port:
+        device.ipv4_address = None
+        device.host_and_port = host_and_port
+
+    simple_handler.parse_provisioning_options(device)
+    assert simple_handler.multicast_vlans == [DEFAULT_MULTICAST_VLAN]
+    assert simple_handler.netconf_username == 'NCUSER'
+    assert simple_handler.netconf_password == 'NCPASS'
+    assert simple_handler.rest_username == 'RUSER'
+    assert simple_handler.rest_password == 'RPASS'
+
+
+@pytest_twisted.inlineCallbacks
+def test_make_netconf_connection(simple_handler):
+    AdtranDeviceHandler.NC_CLIENT = AdtranNetconfClient
+    with mock.patch('voltha.adapters.adtran_olt.net.adtran_netconf.AdtranNetconfClient.connect'):
+        yield simple_handler.make_netconf_connection()
+        assert isinstance(simple_handler.netconf_client, AdtranNetconfClient)
+        first_client = simple_handler.netconf_client
+
+        yield simple_handler.make_netconf_connection(close_existing_client=True)
+        assert isinstance(simple_handler.netconf_client, AdtranNetconfClient)
+        assert first_client is not simple_handler.netconf_client
+
+
+@pytest_twisted.inlineCallbacks
+def test_make_rest_connection(simple_handler):
+    with mock.patch('voltha.adapters.adtran_olt.net.adtran_rest.AdtranRestClient.request',
+                    return_value={'module-info': {}}) as request:
+        yield simple_handler.make_restconf_connection()
+        request.assert_called_once_with('GET', simple_handler.HELLO_URI, name='hello', timeout=simple_handler.timeout)
+        assert isinstance(simple_handler.rest_client, AdtranRestClient)
+
+
+@pytest_twisted.inlineCallbacks
+def test_make_rest_connection_bad_response(simple_handler):
+    with mock.patch('voltha.adapters.adtran_olt.net.adtran_rest.AdtranRestClient.request',
+                    return_value={}):
+        with pytest.raises(ConnectError):
+            yield simple_handler.make_restconf_connection()
+        assert simple_handler.rest_client is None
+
+
+@inlineCallbacks
+def mock_restconf_request(method, uri, name, timeout):
+    assert method == 'GET'
+    assert uri == AdtranDeviceHandler.HELLO_URI
+    assert name == 'hello'
+    assert timeout in [0, 20]
+    yield None
+    returnValue({'module-info': []})
+
+
+def test_check_pulse(simple_handler, device):
+    simple_handler.check_pulse()
+    assert simple_handler.heartbeat is None
+    assert simple_handler.heartbeat_count == 0
+
+    # Prepare for Successful Check Pulse
+    simple_handler._rest_client = mock.MagicMock(autospec=AdtranRestClient)
+    simple_handler._rest_client.request = mock_restconf_request
+    simple_handler.logical_device_id = 1234
+    simple_handler.HEARTBEAT_TIMEOUT = 0
+    simple_handler.alarms = AdapterAlarms(simple_handler.adapter_agent, device.id, device.id)
+
+    simple_handler.check_pulse()
+    assert simple_handler.heartbeat_miss == 0
+    assert simple_handler.heartbeat_count == 1
+    simple_handler._suspend_heartbeat()
+
+    simple_handler.check_pulse()
+    assert simple_handler.heartbeat_miss == 0
+    assert simple_handler.heartbeat_count == 2
+    simple_handler._suspend_heartbeat()
+
+    simple_handler.heartbeat_miss = 2
+    simple_handler._heartbeat_fail(Exception('Boom'))
+    assert simple_handler.heartbeat_miss == 3
+    assert simple_handler.heartbeat_count == 3
+    simple_handler._suspend_heartbeat()
+
+
+@pytest_twisted.inlineCallbacks
+def test_activate(simple_handler, device):
+    @inlineCallbacks
+    def mock_netconf_ready():
+        yield
+        returnValue('ready')
+
+    @inlineCallbacks
+    def enumerate_ports(_device):
+        yield None
+        returnValue([])
+
+    simple_handler.ready_network_access = mock_netconf_ready
+    simple_handler.enumerate_northbound_ports = enumerate_ports
+    simple_handler.process_northbound_ports = lambda dev, results: 'ok'
+    simple_handler.enumerate_southbound_ports = enumerate_ports
+    simple_handler.process_southbound_ports = lambda dev, results: 'ok'
+    simple_handler.initialize_resource_manager = lambda: 'done'
+    simple_handler.complete_device_specific_activation = lambda dev, rec: succeed('Done')
+
+    simple_handler._rest_client = mock.MagicMock(autospec=AdtranRestClient)
+    simple_handler._rest_client.request = mock_restconf_request
+
+    result = yield simple_handler.activate(None, False)
+    assert result == 'activated'
+
+    # Test Side Effects
+    assert simple_handler.netconf_client is not None
+    assert simple_handler.rest_client is not None
+    assert simple_handler.logical_device_id == 'test-id'
+    assert device.model == 'unknown'
+    assert device.vendor == 'Adtran Inc.'
+
+    # Reconcile
+    simple_handler._delete_logical_device()
+    result = yield simple_handler.activate(None, True)
+    assert result == 'activated'
+
+
+@pytest_twisted.inlineCallbacks
+def test_reenable(simple_handler, device):
+    simple_handler._initial_enable_complete = True
+    yield simple_handler.reenable()
+    assert device.oper_status == OperStatus.ACTIVE
+
+
+@pytest_twisted.inlineCallbacks
+def test_disable(simple_handler, device):
+    device.oper_status = OperStatus.ACTIVE
+    simple_handler.logical_device_id = 1234
+    yield simple_handler.disable()
+    assert device.oper_status == OperStatus.UNKNOWN
+
+
+@pytest_twisted.inlineCallbacks
+def test_reboot(simple_handler, device):
+    device.oper_status = OperStatus.ACTIVE
+    device.connect_status = ConnectStatus.REACHABLE
+    simple_handler._initial_enable_complete = True
+    yield simple_handler.make_netconf_connection()
+    yield simple_handler.reboot()
+    simple_handler._cancel_tasks()
+    assert device.oper_status == OperStatus.ACTIVATING
+    assert device.connect_status == ConnectStatus.UNREACHABLE
+
+
+@pytest_twisted.inlineCallbacks
+def test_finish_reboot(simple_handler, device):
+    device.oper_status = OperStatus.ACTIVATING
+    device.connect_status = ConnectStatus.UNREACHABLE
+    yield simple_handler._finish_reboot(0, OperStatus.ACTIVE, ConnectStatus.REACHABLE)
+    simple_handler._cancel_tasks()
+    assert device.oper_status == OperStatus.ACTIVE
+    assert device.connect_status == ConnectStatus.REACHABLE
+
+
+@pytest_twisted.inlineCallbacks
+def test_delete(simple_handler, device):
+    device.oper_status = OperStatus.ACTIVE
+    simple_handler.logical_device_id = 1234
+    yield simple_handler.delete()
+    assert device.reason == 'Deleting'
+
+
diff --git a/voltha/adapters/adtran_olt/test/test_example.py b/voltha/adapters/adtran_olt/test/test_example.py
new file mode 100644
index 0000000..9d2cb18
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/test_example.py
@@ -0,0 +1,18 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 voltha.adapters.adtran_olt import adtran_olt
+
+def test_example():
+    assert True
diff --git a/voltha/adapters/adtran_olt/test/test_pon_port.py b/voltha/adapters/adtran_olt/test/test_pon_port.py
new file mode 100644
index 0000000..4e51b20
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/test_pon_port.py
@@ -0,0 +1,59 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Adtran generic VOLTHA device handler
+"""
+import pytest
+from mock import MagicMock
+from voltha.adapters.adtran_olt.pon_port import PonPort
+from voltha.adapters.adtran_olt.adtran_olt import AdtranOltHandler
+
+
+@pytest.fixture()
+def simple_pon():
+    parent = MagicMock(autospec=AdtranOltHandler)
+    parent.__str__.return_value = 'test-olt'
+    yield PonPort(parent=parent,
+                  port_no=1,
+                  **{
+                      'pon-id': 2  # TODO: This is a kinda crumby API
+                  })
+
+
+def test_properties(simple_pon):
+    assert simple_pon.pon_id == 2
+    assert simple_pon.onus == frozenset()
+    assert simple_pon.onu_ids == frozenset()
+    assert simple_pon.onu(12345) is None
+    assert simple_pon.in_service_onus == 0
+    assert simple_pon.closest_onu_distance == -1
+    assert simple_pon.downstream_fec_enable is True
+    assert simple_pon.upstream_fec_enable is True
+    assert simple_pon.any_upstream_fec_enabled is False
+    assert simple_pon.mcast_aes is False
+    assert simple_pon.deployment_range == 25000
+    assert simple_pon.discovery_tick == 200.0
+    assert simple_pon.activation_method == 'autoactivate'
+    assert simple_pon.authentication_method == 'serial-number'
+    assert 'PonPort-pon-2: Admin: 3, Oper: 1, OLT: test-olt' == str(simple_pon)
+
+
+def test_get_port(simple_pon):
+    port = simple_pon.get_port()
+    assert """port_no: 1
+label: "pon-2"
+type: PON_OLT
+admin_state: ENABLED
+oper_status: DISCOVERED
+""" == str(port)
diff --git a/voltha/adapters/adtran_olt/test/xpon/test_best_effort.py b/voltha/adapters/adtran_olt/test/xpon/test_best_effort.py
new file mode 100644
index 0000000..ef6aca7
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/xpon/test_best_effort.py
@@ -0,0 +1,124 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 voltha.adapters.adtran_olt.xpon.best_effort import BestEffort
+from mock import patch, MagicMock
+import pytest
+
+# Globals
+# consider parametrizing these attributes
+bw = 100000000000
+pr = 127
+wt = 100
+
+pon_id = 0
+onu_id = 1
+alloc_id = 1024
+
+
+def test_best_effort_init_values_missing():
+    """
+    verify __init__ fails when no values are specified
+    """
+
+    with pytest.raises(Exception):
+        obj = BestEffort()
+
+
+@pytest.fixture(scope="module")
+def be():
+    be_obj = BestEffort(bw, pr, wt)
+    return be_obj
+
+
+def test_best_effort_init_values(be):
+    """
+    verify __init__ values are set properly
+    """
+
+    assert bw == be.bandwidth
+    assert pr == be.priority
+    assert wt == be.weight
+
+
+def test_best_effort_str_values(be):
+    """
+    verify __str__ values are set properly
+    """
+
+    expected_str_value = "BestEffort: {}/p-{}/w-{}".format(bw, pr, wt)
+    actual_str_val = str(be)
+
+    assert expected_str_value == actual_str_val
+
+
+def test_best_effort_dict_values(be):
+    """
+    verify dict values are set properly
+    """
+
+    expected_dict = {
+            'bandwidth': bw,
+            'priority': pr,
+            'weight': wt
+        }
+
+    actual_dict = be.to_dict()
+
+    assert expected_dict == actual_dict
+
+
+def test_add_to_hardware_with_internal_calls(be):
+    """
+    verify calls to add hardware. Uses internal calls
+    """
+    expected_uri = '/restconf/data/gpon-olt-hw:olt/pon=0/onus/onu=1/t-conts/t-cont=1024'
+    expected_data = {"best-effort": {"priority": pr, "bandwidth": bw, "weight": wt}}
+    expected_name = 'tcont-best-effort-{}-{}: {}'.format(pon_id, onu_id, alloc_id)
+    hardware_resp = 'Warning, Warning, Danger Will Robinson'
+
+    def evaluate_request(*args, **kwargs):
+        method, uri = args
+        assert method == 'PATCH'
+        assert uri == expected_uri
+        assert expected_data == eval(kwargs['data'])
+        assert expected_name == kwargs['name']
+        return hardware_resp
+
+    mock_session = MagicMock()
+    mock_session.request = evaluate_request
+    resp = be.add_to_hardware(mock_session, pon_id, onu_id, alloc_id, be)
+    assert resp == hardware_resp
+
+
+@patch('voltha.adapters.adtran_olt.xpon.best_effort.json.dumps', return_value='mocked_data')
+def test_add_to_hardware_isolated(mock_json_dumps, be):
+    """
+    verify calls to add hardware. Internal call to json.dumps is mocked
+    """
+
+    mock_best_effort = MagicMock()
+    mock_session = MagicMock()
+
+    mock_best_effort.to_dict.return_value = 'empty dictionary'
+
+    be.add_to_hardware(mock_session, pon_id, onu_id, alloc_id, mock_best_effort)
+
+    expected_uri = '/restconf/data/gpon-olt-hw:olt/pon=0/onus/onu=1/t-conts/t-cont=1024'
+    expected_data = 'mocked_data'
+    expected_name = 'tcont-best-effort-0-1: 1024'
+
+    mock_json_dumps.assert_called_once_with({'best-effort': 'empty dictionary'})
+
+    mock_session.request.assert_called_once_with('PATCH', expected_uri, data=expected_data, name=expected_name)
diff --git a/voltha/adapters/adtran_olt/test/xpon/test_gem_port.py b/voltha/adapters/adtran_olt/test/xpon/test_gem_port.py
new file mode 100644
index 0000000..dad03bd
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/xpon/test_gem_port.py
@@ -0,0 +1,115 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 voltha.adapters.adtran_olt.xpon.gem_port import GemPort
+from mock import patch, MagicMock
+import pytest
+
+g_in = 11
+a_in = 21
+u_in = 31
+te_in = 41
+e_in = 'encryption'
+m_in = 'multicast'
+tr_in = 'traffic_class'
+h_in = 'handler'
+i_in = 'ismock'
+
+
+def test_gem_port_init_values_missing():
+    """
+    verify __init__ fails when no values are specified
+    """
+
+    with pytest.raises(Exception):
+        obj = GemPort()
+
+
+def test_gem_port_init_values():
+    """
+    verify __init__ values are set properly
+    """
+    gp = GemPort(g_in, a_in, u_in, te_in, e_in, m_in, tr_in, h_in, i_in)
+
+    assert gp.gem_id == g_in
+    assert gp._alloc_id == a_in
+    assert gp.uni_id == u_in
+    assert gp.tech_profile_id == None           # TODO: code says default may change to a property
+    assert gp._encryption == e_in
+    assert gp.multicast == m_in
+    assert gp.traffic_class == tr_in
+    assert gp._handler == h_in
+    assert gp._is_mock == i_in
+
+def test_gem_port_init_values_default():
+    """
+    verify __init__ values are set properly using defaults
+    """
+    gp = GemPort(g_in, a_in, u_in, te_in)
+
+    assert gp.gem_id == g_in
+    assert gp._alloc_id == a_in
+    assert gp.uni_id == u_in
+    #assert gp.tech_profile_id == te_in
+    assert gp.tech_profile_id == None
+    assert gp._encryption == False
+    assert gp.multicast == False
+    assert gp.traffic_class == None
+    assert gp._handler == None
+    assert gp._is_mock == False
+
+
+@pytest.fixture(scope="module")
+def gp():
+    be_obj = GemPort(g_in, a_in, u_in, te_in, e_in, m_in, tr_in, h_in, i_in)
+    return be_obj
+
+def test_gem_port_str_values(gp):
+    """
+    verify __str__ values are set properly
+    """
+
+    expected_str_value = "GemPort: alloc-id: {}, gem-id: {}, uni-id: {}".format(a_in, g_in, u_in)
+
+    actual_str_val = str(gp)
+
+    assert expected_str_value == actual_str_val
+
+def test_gem_port_getter_properties(gp):
+    """
+    verify alloc_id and encryption @property getters
+    """
+
+    assert gp.alloc_id == a_in
+    assert gp.encryption == e_in
+
+def test_gem_port_dict_values(gp):
+    """
+    verify dict values are set properly
+    """
+
+    expected_dict = {
+            'port-id': g_in,
+            'alloc-id': a_in,
+            'encryption': e_in,
+            'omci-transport': False
+        }
+
+    actual_dict = gp.to_dict()
+
+    assert expected_dict == actual_dict
+
+# TODO - Exercise the rx and tx statistics
+
diff --git a/voltha/adapters/adtran_olt/test/xpon/test_olt_gem_port.py b/voltha/adapters/adtran_olt/test/xpon/test_olt_gem_port.py
new file mode 100644
index 0000000..03d7e79
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/xpon/test_olt_gem_port.py
@@ -0,0 +1,329 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 voltha.adapters.adtran_olt.xpon.olt_gem_port import OltGemPort
+import pytest
+from mock import patch, MagicMock
+import mock
+import json
+import pytest_twisted
+
+
+GEMID = 11
+ALLOCID = 21
+TECHPROFILEID = 31
+PONID = 41
+ONUID = 51
+UNIID = 61
+ENCRYPTION = False
+MULTICAST = 'multicast'
+TRAFFICCLASS = 'traffic_class'
+HANDLER = 'handler'
+ISMOCK = 'ismock'
+
+
+@pytest.fixture()
+def ogp():
+    ogp_obj = OltGemPort(GEMID, ALLOCID, TECHPROFILEID, PONID, ONUID, UNIID, ENCRYPTION, MULTICAST, TRAFFICCLASS,
+                         HANDLER, ISMOCK)
+    return ogp_obj
+
+
+@pytest.fixture()
+def ogp_defaults():
+    ogp_obj = OltGemPort(GEMID, ALLOCID, TECHPROFILEID, PONID, ONUID, UNIID)
+    return ogp_obj
+
+
+def test_olt_gem_port_init_values_missing():
+    """
+    verify __init__ fails when no values are specified
+    """
+
+    with pytest.raises(Exception):
+        OltGemPort()
+
+
+def test_olt_gem_port_init_values(ogp):
+    """
+    verify __init__ values are set properly
+    """
+
+    assert ogp.gem_id == GEMID
+    assert ogp._alloc_id == ALLOCID
+    assert ogp.uni_id == UNIID
+    # assert ogp.tech_profile_id == TECHPROFILEID
+    assert ogp.tech_profile_id is None           # TODO: code says default may change to a property
+    assert ogp.encryption == ENCRYPTION
+    assert ogp.multicast == MULTICAST
+    assert ogp.traffic_class == TRAFFICCLASS
+    assert ogp._handler == HANDLER
+    assert ogp._is_mock == ISMOCK
+    assert ogp._pon_id == PONID
+    assert ogp._onu_id == ONUID
+
+
+def test_olt_gem_port_init_values_default():
+    """
+    verify __init__ values are set properly using defaults
+    """
+
+    ogp = OltGemPort(GEMID, ALLOCID, TECHPROFILEID, PONID, ONUID, UNIID)
+
+    assert ogp.gem_id == GEMID
+    assert ogp._alloc_id == ALLOCID
+    assert ogp.uni_id == UNIID
+    # assert ogp.tech_profile_id == TECHPROFILEID
+    assert ogp.tech_profile_id is None           # TODO: code says default may change to a property
+    assert ogp.encryption is False
+    assert ogp.multicast is False
+    assert ogp.traffic_class is None
+    assert ogp._handler is None
+    assert ogp._is_mock is False
+    assert ogp._pon_id == PONID
+    assert ogp._onu_id == ONUID
+
+
+def test_olt_gem_port_getters_and_setters(ogp):
+    """
+    verify simple getters and setters (pon_id, onu_id, timestamp)
+    """
+
+    # ogp.pon_id('pon id set')
+    assert ogp.pon_id == PONID
+
+    # ogp.onu_id('onu id set')
+    assert ogp.onu_id == ONUID
+
+    ogp.timestamp = 'same bat time'
+    assert ogp.timestamp == 'same bat time'
+
+# prepare arguments so fixture sets up the class with known encryption setting
+# and a mock for the rest call
+
+
+@patch('voltha.adapters.adtran_olt.xpon.olt_gem_port.OltGemPort.set_config', return_value='not used')
+def test_olt_gem_port_encryption_getters_and_setters(mock_set_config):
+    """
+    verify getters and setters for encryption
+    """
+    _encryption = True
+    mock_handler = MagicMock()
+    # I'm mocking handler because set_config is called with _handler.rest_client.
+    # Mocking allows the rest_client attribute to be specified with no error
+    # while isolating the method being tested. _handler is in the super class.
+
+    ogp = OltGemPort(GEMID, ALLOCID, TECHPROFILEID, PONID, ONUID, UNIID, _encryption, MULTICAST, TRAFFICCLASS,
+                     mock_handler, ISMOCK)
+
+    # Set to same value
+    ogp.encryption = _encryption
+    assert ogp.encryption == _encryption
+
+    # Set to opposite value
+    _encryption_opposite = not _encryption
+    ogp.encryption = _encryption_opposite
+    assert ogp.encryption == _encryption_opposite
+
+    mock_set_config.assert_called_once_with(mock_handler.rest_client, 'encryption', _encryption_opposite)
+
+
+@pytest_twisted.inlineCallbacks
+def test_add_to_hardware_ismock():
+    """
+    verify call to add hardware when isMock = True
+    """
+
+    _isMock = True
+
+    ogp = OltGemPort(GEMID, ALLOCID, TECHPROFILEID, PONID, ONUID, UNIID, ENCRYPTION, MULTICAST, TRAFFICCLASS,
+                     HANDLER, _isMock)
+
+    result = yield ogp.add_to_hardware('session')
+    assert result == 'mock'
+
+
+@pytest_twisted.inlineCallbacks
+def test_add_to_hardware_post(ogp_defaults):
+    """
+    verify call to add hardware with no exception using POST
+    """
+
+    expected_uri = '/restconf/data/gpon-olt-hw:olt/pon={}/onus/onu={}/gem-ports/gem-port'.format(PONID, ONUID)
+    expected_data = {"port-id": GEMID, "alloc-id": ALLOCID, "encryption": ENCRYPTION, "omci-transport": False}
+    expected_name = 'gem-port-create-{}-{}: {}/{}'.format(PONID, ONUID, GEMID, ALLOCID)
+
+    hardware_resp = 'Warning, Warning, Danger Will Robinson'
+
+    def evaluate_request(*args, **kwargs):
+        method, uri = args
+        assert method == 'POST'
+        assert uri == expected_uri
+        assert expected_data == json.loads(kwargs['data'])
+        assert expected_name == kwargs['name']
+
+    mock_session = MagicMock()
+    mock_session.request.return_value = hardware_resp
+
+    resp = yield ogp_defaults.add_to_hardware(mock_session)
+    args, kwargs = mock_session.request.call_args
+
+    evaluate_request(*args, **kwargs)
+    assert resp == hardware_resp
+
+
+@pytest_twisted.inlineCallbacks
+def test_add_to_hardware_except_to_patch(ogp_defaults):
+    """
+    verify call to add hardware with exception using POST then switch to PATCH
+    """
+
+    expected_uri = '/restconf/data/gpon-olt-hw:olt/pon={}/onus/onu={}/gem-ports/gem-port'.format(PONID, ONUID)
+    # TODO Should uri display the gem port value? Code currently omits a value.
+
+    expected_data = {"port-id": GEMID, "alloc-id": ALLOCID, "encryption": ENCRYPTION, "omci-transport": False}
+    expected_name = 'gem-port-create-{}-{}: {}/{}'.format(PONID, ONUID, GEMID, ALLOCID)
+
+    hardware_resp = 'Warning, Warning, Danger Will Robinson'
+
+    def evaluate_request(*args, **kwargs):
+        method, uri = args
+        if method == 'POST':
+            raise Exception('Force an exception for POST')
+        if method == 'SPLAT':
+            raise Exception('Force an exception for SPLAT')
+        assert method == 'PATCH'
+        assert uri == expected_uri
+        assert expected_data == json.loads(kwargs['data'])
+        assert expected_name == kwargs['name']
+        return hardware_resp
+
+    mock_session = MagicMock()
+    mock_session.request = evaluate_request
+
+    # test with POST which will fail the try and exception to using PATCH
+    hw_resp = yield ogp_defaults.add_to_hardware(mock_session)
+    assert hw_resp == hardware_resp
+
+    # test with SPLAT which will fail the try and exception to logging and the method raising an exception
+
+    with mock.patch("voltha.adapters.adtran_olt.xpon.olt_gem_port.log.exception") as mock_log_exception:
+        with pytest.raises(Exception) as caught_ex:
+            yield ogp_defaults.add_to_hardware(mock_session, 'SPLAT')
+
+        # verify the raise
+        assert 'Force an exception for SPLAT' == str(caught_ex.value)
+
+    # Verify the args sent to the log
+    args, kwargs = mock_log_exception.call_args
+    msg = str(args)
+    gem = str(kwargs['gem'])
+    e = str(kwargs['e'])
+
+    assert msg == "('add-2-hw',)"
+    assert gem == 'GemPort: 41/51/61, alloc-id: 21, gem-id: 11'
+    assert e == 'Force an exception for SPLAT'
+
+
+@pytest_twisted.inlineCallbacks
+def test_remove_from_hardware_ismock():
+    """
+    verify call to remove hardware when isMock = True
+    """
+
+    _isMock = True
+
+    ogp = OltGemPort(GEMID, ALLOCID, TECHPROFILEID, PONID, ONUID, UNIID, ENCRYPTION, MULTICAST, TRAFFICCLASS, HANDLER,
+                     _isMock)
+
+    result = yield ogp.remove_from_hardware('session')
+    assert result == 'mock'
+
+
+@pytest_twisted.inlineCallbacks
+def test_remove_from_hardware(ogp_defaults):
+    """
+    verify call to remove hardware
+    """
+
+    expected_uri = '/restconf/data/gpon-olt-hw:olt/pon={}/onus/onu={}/gem-ports/gem-port={}'.format(PONID, ONUID, GEMID)
+    expected_name = 'gem-port-delete-{}-{}: {}'.format(PONID, ONUID, GEMID)
+
+    hardware_resp = 'Warning, Warning, Danger Will Robinson'
+
+    def evaluate_request(*args, **kwargs):
+        method, uri = args
+        assert method == 'DELETE'
+        assert uri == expected_uri
+        assert expected_name == kwargs['name']
+
+    mock_session = MagicMock()
+    mock_session.request.return_value = hardware_resp
+
+    resp = yield ogp_defaults.remove_from_hardware(mock_session)
+    args, kwargs = mock_session.request.call_args
+
+    evaluate_request(*args, **kwargs)
+    assert resp == hardware_resp
+
+
+def test_set_config(ogp_defaults):
+    """
+    verify call to set_config
+    """
+
+    _leaf = 'ima leaf'
+    _value = 'ima value'
+
+    expected_uri = '/restconf/data/gpon-olt-hw:olt/pon={}/onus/onu={}/gem-ports/gem-port={}'.format(PONID, ONUID, GEMID)
+    expected_data = {'ima leaf': 'ima value'}
+    expected_name = 'onu-set-config-{}-{}-{}'.format(PONID, _leaf, _value)
+
+    hardware_resp = "'alloc-id': {}, 'encryption': True, 'omci-transport': False, 'port-id': {}'".format(ALLOCID, PONID)
+
+    def evaluate_request(*args, **kwargs):
+        method, uri = args
+        assert method == 'PATCH'
+        assert uri == expected_uri
+        assert expected_data == json.loads(kwargs['data'])
+        assert expected_name == kwargs['name']
+        return hardware_resp
+
+    mock_session = MagicMock()
+    mock_session.request = evaluate_request
+
+    hw_resp = ogp_defaults.set_config(mock_session, _leaf, _value)
+    assert hw_resp == hardware_resp
+
+
+def test_create():
+    """
+    verify call to create
+    """
+    _gem = MagicMock()
+    _gem.gemport_id = 123
+    _gem.aes_encryption = "TrUe"
+    _ofp_port_num = 321
+
+    response = OltGemPort.create(HANDLER, _gem, ALLOCID, TECHPROFILEID, PONID, ONUID, UNIID, _ofp_port_num)
+
+    assert response.gem_id == _gem.gemport_id
+    assert response._alloc_id == ALLOCID
+    assert response.tech_profile_id is None     # TODO: code says default may change to a property
+    assert response._pon_id == PONID
+    assert response._onu_id == ONUID
+    assert response.uni_id == UNIID
+    assert response.encryption is True
+    assert response._handler == HANDLER
+    assert response.multicast is False
diff --git a/voltha/adapters/adtran_olt/test/xpon/test_tcont.py b/voltha/adapters/adtran_olt/test/xpon/test_tcont.py
new file mode 100644
index 0000000..0f9ba3d
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test/xpon/test_tcont.py
@@ -0,0 +1,89 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 voltha.adapters.adtran_olt.xpon.tcont import TCont
+# from mock import patch, MagicMock
+import pytest
+
+# Globals
+# consider parametrizing these attributes
+
+allo = 12
+tech = 22
+traf = 32
+unii = 42
+imck=False
+
+
+def test_tcont_init_values_missing():
+    """
+    verify __init__ fails when no values are specified
+    """
+
+    with pytest.raises(Exception):
+        tc_obj = TCont()
+
+
+@pytest.fixture(scope="module")
+def tc():
+    tc_obj = TCont(allo, tech, traf, unii, imck)
+    return tc_obj
+
+
+def test_tcont_init_values(tc):
+    """
+    verify __init__ values are set properly
+    """
+    assert allo == tc.alloc_id
+    assert traf == tc.traffic_descriptor
+    assert imck == tc._is_mock
+    assert tech == tc.tech_profile_id
+    assert unii == tc.uni_id
+
+def test_tcont_init_values_no_ismock():
+    """
+    verify __init__ values are set properly
+    """
+
+    tc1 = TCont(allo, tech, traf, unii)
+
+    assert allo == tc1.alloc_id
+    assert traf == tc1.traffic_descriptor
+    assert imck == tc1._is_mock
+    assert tech == tc1.tech_profile_id
+    assert unii == tc1.uni_id
+
+
+def test_tcont_init_values_ismock_true():
+    """
+    verify __init__ values are set properly
+    """
+    tc2 = TCont(allo, tech, traf, unii, True)
+
+    assert allo == tc2.alloc_id
+    assert traf == tc2.traffic_descriptor
+    assert True == tc2._is_mock
+    assert tech == tc2.tech_profile_id
+    assert unii == tc2.uni_id
+
+def test_tcont_str_values(tc):
+    """
+    verify __str__ values are set properly
+    """
+
+    expected_str_value = "TCont: alloc-id: {}, uni-id: {}".format(allo, unii)
+    actual_str_val = str(tc)
+
+    assert expected_str_value == actual_str_val
diff --git a/voltha/adapters/adtran_olt/test_requirements.txt b/voltha/adapters/adtran_olt/test_requirements.txt
new file mode 100644
index 0000000..a336cc3
--- /dev/null
+++ b/voltha/adapters/adtran_olt/test_requirements.txt
@@ -0,0 +1,5 @@
+-r ../../../requirements.txt
+pytest < 4.1
+pytest-cov
+pytest-twisted
+virtualenv
\ No newline at end of file
diff --git a/voltha/adapters/adtran_olt/verify-license.sh b/voltha/adapters/adtran_olt/verify-license.sh
new file mode 100644
index 0000000..4d93e04
--- /dev/null
+++ b/voltha/adapters/adtran_olt/verify-license.sh
@@ -0,0 +1,119 @@
+#!/bin/bash
+
+# licensecheck.sh
+# checks for copyright/license headers on files
+# excludes filename extensions where this check isn't pertinent
+
+set +e -u -o pipefail
+fail_licensecheck=0
+
+while IFS= read -r -d '' f
+do
+  grep -q "Copyright\|Apache License" "${f}"
+  rc=$?
+  if [[ $rc != 0 ]]; then
+    echo "ERROR: $f does not contain License Header"
+    fail_licensecheck=1
+  fi
+done < <(find . -name ".git" -prune -o -type f \
+  -name "*.*" \
+  ! -name "*.PNG" \
+  ! -name "*.asc" \
+  ! -name "*.bat" \
+  ! -name "*.cert" \
+  ! -name "*.cfg" \
+  ! -name "*.cnf" \
+  ! -name "*.conf" \
+  ! -name "*.cql" \
+  ! -name "*.crt" \
+  ! -name "*.csar" \
+  ! -name "*.csr" \
+  ! -name "*.csv" \
+  ! -name "*.ctmpl" \
+  ! -name "*.curl" \
+  ! -name "*.db" \
+  ! -name "*.der" \
+  ! -name "*.desc" \
+  ! -name "*.diff" \
+  ! -name "*.dnsmasq" \
+  ! -name "*.do" \
+  ! -name "*.docx" \
+  ! -name "*.eot" \
+  ! -name "*.gif" \
+  ! -name "*.gpg" \
+  ! -name "*.graffle" \
+  ! -name "*.groovy" \
+  ! -name "*.ico" \
+  ! -name "*.iml" \
+  ! -name "*.in" \
+  ! -name "*.inc" \
+  ! -name "*.install" \
+  ! -name "*.j2" \
+  ! -name "*.jar" \
+  ! -name "*.jks" \
+  ! -name "*.jpg" \
+  ! -name "*.json" \
+  ! -name "*.jsonld" \
+  ! -name "*.JSON" \
+  ! -name "*.key" \
+  ! -name "*.list" \
+  ! -name "*.local" \
+  ! -path "*.lock" \
+  ! -name "*.log" \
+  ! -name "*.mak" \
+  ! -name "*.md" \
+  ! -name "*.MF" \
+  ! -name "*.mk" \
+  ! -name "*.oar" \
+  ! -name "*.p12" \
+  ! -name "*.patch" \
+  ! -name "*.pdf" \
+  ! -name "*.pcap" \
+  ! -name "*.pem" \
+  ! -name "*.png" \
+  ! -name "*.properties" \
+  ! -name "*.proto" \
+  ! -name "*.pyc" \
+  ! -name "*.repo" \
+  ! -name "*.robot" \
+  ! -name "*.rst" \
+  ! -name "*.rules" \
+  ! -name "*.service" \
+  ! -name "*.svg" \
+  ! -name "*.swp" \
+  ! -name "*.tar" \
+  ! -name "*.tar.gz" \
+  ! -name "*.toml" \
+  ! -name "*.ttf" \
+  ! -name "*.txt" \
+  ! -name "*.woff" \
+  ! -name "*.xproto" \
+  ! -name "*.xtarget" \
+  ! -name "*ignore" \
+  ! -name "nosetests.*" \
+  ! -name "*rc" \
+  ! -name "Dockerfile" \
+  ! -name "Dockerfile.*" \
+  ! -name "Makefile" \
+  ! -name "Makefile.*" \
+  ! -name "coverage.*" \
+  ! -name "README" \
+  ! -name ".coverage" \
+  ! -name "junit-*" \
+  ! -path "*/vendor/*.go" \
+  ! -path "*nginx_config*" \
+  ! -path "*experiments*" \
+  ! -path "*netopeer*" \
+  ! -path "*compose*" \
+  ! -path "*git*" \
+  ! -path "*swagger*" \
+  ! -path "*venv*" \
+  ! -path "*protos*" \
+  ! -path "*swagger*" \
+  ! -path "*tmp*" \
+  ! -path "*htmlcov*" \
+  ! -path "*prof*" \
+  ! -path "*netconf/*" \
+  -print0 )
+
+exit ${fail_licensecheck}
diff --git a/voltha/adapters/adtran_olt/xpon/__init__.py b/voltha/adapters/adtran_olt/xpon/__init__.py
index b0fb0b2..88c95ef 100644
--- a/voltha/adapters/adtran_olt/xpon/__init__.py
+++ b/voltha/adapters/adtran_olt/xpon/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2017-present Open Networking Foundation
+# Copyright 2017-present Adtran, Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -11,3 +11,4 @@
 # 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.
+
diff --git a/voltha/adapters/adtran_olt/xpon/olt_gem_port.py b/voltha/adapters/adtran_olt/xpon/olt_gem_port.py
index 272fb07..b820d54 100644
--- a/voltha/adapters/adtran_olt/xpon/olt_gem_port.py
+++ b/voltha/adapters/adtran_olt/xpon/olt_gem_port.py
@@ -102,7 +102,8 @@
 
         except Exception as e:
             if operation == 'POST':
-                returnValue(self.add_to_hardware(session, operation='PATCH'))
+                result = yield self.add_to_hardware(session, operation='PATCH')
+                returnValue(result)
             else:
                 log.exception('add-2-hw', gem=self, e=e)
                 raise
diff --git a/voltha/adapters/adtran_onu/.coveragerc b/voltha/adapters/adtran_onu/.coveragerc
index bb377ea..aa8118f 100644
--- a/voltha/adapters/adtran_onu/.coveragerc
+++ b/voltha/adapters/adtran_onu/.coveragerc
@@ -4,6 +4,5 @@
 parallel = True
 
 [report]
-show_missing = True
 
 [html]
diff --git a/voltha/adapters/adtran_onu/.pylintrc b/voltha/adapters/adtran_onu/.pylintrc
index 33916ad..41cf299 100644
--- a/voltha/adapters/adtran_onu/.pylintrc
+++ b/voltha/adapters/adtran_onu/.pylintrc
@@ -1,6 +1,5 @@
 [MASTER]
 
-
 # A comma-separated list of package or module names from where C extensions may
 # be loaded. Extensions are loading into the active Python interpreter and may
 # run arbitrary code
diff --git a/voltha/adapters/adtran_onu/adtran_onu_handler.py b/voltha/adapters/adtran_onu/adtran_onu_handler.py
index aa05e3e..ac76d3f 100644
--- a/voltha/adapters/adtran_onu/adtran_onu_handler.py
+++ b/voltha/adapters/adtran_onu/adtran_onu_handler.py
@@ -332,7 +332,7 @@
         parts = tp_path.split('/')
         if len(parts) > 2:
             try:
-                return int(tp_path[1])
+                return int(parts[1])
             except ValueError:
                 return DEFAULT_TECH_PROFILE_TABLE_ID
 
diff --git a/voltha/adapters/adtran_onu/heartbeat.py b/voltha/adapters/adtran_onu/heartbeat.py
index 4a7ab1f..e8d7984 100644
--- a/voltha/adapters/adtran_onu/heartbeat.py
+++ b/voltha/adapters/adtran_onu/heartbeat.py
@@ -22,6 +22,7 @@
     """Wraps health-check support for ONU"""
     INITIAL_DELAY = 60                      # Delay after start until first check
     TICK_DELAY = 2                          # Heartbeat interval
+    HEARTBEAT_FAILED_LIMIT = 5
 
     def __init__(self, handler, device_id):
         self.log = structlog.get_logger(device_id=device_id)
@@ -33,13 +34,13 @@
         self._heartbeat_count = 0
         self._heartbeat_miss = 0
         self._alarms_raised_count = 0
-        self.heartbeat_failed_limit = 5
+        self.heartbeat_failed_limit = self.HEARTBEAT_FAILED_LIMIT
         self.heartbeat_last_reason = ''
         self.heartbeat_interval = self.TICK_DELAY
 
     def __str__(self):
-        return "HeartBeat: count:{}, miss: {}".format(self._heartbeat_count,
-                                                      self._heartbeat_miss)
+        return "HeartBeat: count: {}, miss: {}".format(self._heartbeat_count,
+                                                       self._heartbeat_miss)
 
     @staticmethod
     def create(handler, device_id):
@@ -49,7 +50,7 @@
         self._defer = reactor.callLater(delay, self.check_pulse)
 
     def _stop(self):
-        d, self._defeered = self._defeered, None
+        d, self._defer = self._defer, None
         if d is not None and not d.called():
             d.cancel()
 
@@ -122,7 +123,7 @@
             self._heartbeat_miss = self.heartbeat_failed_limit
             self.heartbeat_last_reason = e.message
 
-        self.heartbeat_check_status(results)
+        self.heartbeat_check_status()
 
     def _heartbeat_fail(self, failure):
         self._heartbeat_miss += 1
@@ -130,7 +131,7 @@
                       count=self._heartbeat_count,
                       miss=self._heartbeat_miss)
         self.heartbeat_last_reason = 'OMCI connectivity error'
-        self.heartbeat_check_status(None)
+        self.heartbeat_check_status()
 
     def on_heartbeat_alarm(self, active):
         # TODO: Do something here ?
@@ -138,7 +139,7 @@
         #  TODO: If failed (active = true) due to bad serial-number shut off the UNI port?
         pass
 
-    def heartbeat_check_status(self, results):
+    def heartbeat_check_status(self):
         """
         Check the number of heartbeat failures against the limit and emit an alarm if needed
         """
@@ -165,7 +166,6 @@
                     device.reason = ''
                     self._handler.adapter_agent.update_device(device)
                     HeartbeatAlarm(self._handler.alarms, 'onu').clear_alarm()
-
                     self._alarm_active = False
                     self._alarms_raised_count += 1
                     self.on_heartbeat_alarm(False)
diff --git a/voltha/adapters/adtran_onu/onu_tcont.py b/voltha/adapters/adtran_onu/onu_tcont.py
index 9751986..afafb61 100644
--- a/voltha/adapters/adtran_onu/onu_tcont.py
+++ b/voltha/adapters/adtran_onu/onu_tcont.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 import structlog
-from twisted.internet.defer import  inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue
 
 from voltha.adapters.adtran_olt.xpon.tcont import TCont
 from voltha.adapters.adtran_olt.xpon.traffic_descriptor import TrafficDescriptor
diff --git a/voltha/adapters/adtran_onu/test/omci/__init__.py b/voltha/adapters/adtran_onu/test/omci/__init__.py
new file mode 100644
index 0000000..18d64b2
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/omci/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
diff --git a/voltha/adapters/adtran_onu/test/omci/test_adtn_capabilities_task.py b/voltha/adapters/adtran_onu/test/omci/test_adtn_capabilities_task.py
new file mode 100644
index 0000000..6e33db9
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/omci/test_adtn_capabilities_task.py
@@ -0,0 +1,121 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 pytest import fixture
+from mock import MagicMock
+from mock import patch
+
+
+from voltha.adapters.adtran_onu.omci.adtn_capabilities_task import AdtnCapabilitiesTask
+from voltha.extensions.omci.omci_entities import EntityOperations
+
+
+@fixture(scope='function')
+def adtn_capabilities():
+    return AdtnCapabilitiesTask(MagicMock(), "test_id")
+
+
+@fixture(scope='function')
+def message_types():
+    op_11287800f1 = [
+        EntityOperations.Create,
+        EntityOperations.CreateComplete]
+    return op_11287800f1
+
+
+def test_properties(adtn_capabilities):
+    assert adtn_capabilities.name == "Adtran ONU Capabilities Task"
+
+
+def test_supported_managed_entities_when_entity_not_managed_via_omci(adtn_capabilities):
+    me_1287800f1 = [
+        2, 5, 6, 7, 11, 24, 45, 46, 47, 48, 49, 50, 51, 52, 79, 84, 89, 130,
+        131, 133, 134, 135, 136, 137, 148, 157, 158, 159, 171, 256, 257, 262,
+        263, 264, 266, 268, 272, 273, 274, 277, 278, 279, 280, 281, 297, 298,
+        299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312,
+        329, 330, 332, 334, 336, 340, 341, 342, 343, 348, 425, 426, 65300,
+        65400, 65401, 65402, 65403, 65404, 65406, 65407, 65408, 65409, 65410,
+        65411, 65412, 65413, 65414, 65420, 65421, 65422, 65423, 65424
+    ]
+    expected_result = frozenset(list(me_1287800f1))
+    capabilities = adtn_capabilities.supported_managed_entities
+    assert expected_result == capabilities
+
+
+def test_supported_managed_entities_when_entity_managed_via_omci(adtn_capabilities):
+    adtn_capabilities._supported_entities = [1, 2]
+    adtn_capabilities._omci_managed = True
+    capabilities = adtn_capabilities.supported_managed_entities
+    assert capabilities == frozenset([1, 2])
+
+
+def test_supported_message_types_when_entity_not_managed_via_omci(adtn_capabilities):
+    from voltha.extensions.omci.omci_entities import EntityOperations
+    op_11287800f1 = [
+        EntityOperations.Create,
+        EntityOperations.CreateComplete,
+        EntityOperations.Delete,
+        EntityOperations.Set,
+        EntityOperations.Get,
+        EntityOperations.GetComplete,
+        EntityOperations.GetAllAlarms,
+        EntityOperations.GetAllAlarmsNext,
+        EntityOperations.MibUpload,
+        EntityOperations.MibUploadNext,
+        EntityOperations.MibReset,
+        EntityOperations.AlarmNotification,
+        EntityOperations.AttributeValueChange,
+        EntityOperations.Test,
+        EntityOperations.StartSoftwareDownload,
+        EntityOperations.DownloadSection,
+        EntityOperations.EndSoftwareDownload,
+        EntityOperations.ActivateSoftware,
+        EntityOperations.CommitSoftware,
+        EntityOperations.SynchronizeTime,
+        EntityOperations.Reboot,
+        EntityOperations.GetNext,
+    ]
+    expected_result = frozenset(op_11287800f1)
+    message_types = adtn_capabilities.supported_message_types
+    assert expected_result == message_types
+
+
+def test_supported_message_types_when_entity_managed_via_omci(adtn_capabilities, message_types):
+    adtn_capabilities._omci_managed = True
+    adtn_capabilities._supported_msg_types = set(message_types)
+    capabilities = adtn_capabilities.supported_message_types
+    assert capabilities == frozenset(message_types)
+
+
+def test_perform_get_capabilities_when_not_managed_via_omci(adtn_capabilities):
+    adtn_capabilities._omci_managed = False
+    adtn_capabilities.perform_get_capabilities()
+    result = adtn_capabilities.deferred.result
+    assert result['supported-managed-entities'] == adtn_capabilities.supported_managed_entities
+    assert result['supported-message-types'] == adtn_capabilities.supported_message_types
+
+
+def test_perform_get_capabilities_when_managed_via_omci(adtn_capabilities, message_types):
+    adtn_capabilities._omci_managed = True
+    with patch('voltha.adapters.adtran_onu.omci.adtn_capabilities_task.AdtnCapabilitiesTask.get_supported_entities') as supp_ent,\
+        patch('voltha.adapters.adtran_onu.omci.adtn_capabilities_task.AdtnCapabilitiesTask.get_supported_message_types') as sup_msg:
+        supp_ent.return_value = [1, 2]
+        sup_msg.return_value = set(message_types)
+        adtn_capabilities.perform_get_capabilities()
+        result = adtn_capabilities.deferred.result
+        assert result['supported-managed-entities'] == frozenset([1, 2])
+        assert result['supported-message-types'] == frozenset(message_types)
+
+
diff --git a/voltha/adapters/adtran_onu/test/omci/test_adtn_get_mds_task.py b/voltha/adapters/adtran_onu/test/omci/test_adtn_get_mds_task.py
new file mode 100644
index 0000000..fbb12e1
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/omci/test_adtn_get_mds_task.py
@@ -0,0 +1,47 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 pytest import fixture
+from mock import MagicMock
+from mock import patch
+
+
+from voltha.adapters.adtran_onu.omci.adtn_get_mds_task import AdtnGetMdsTask
+
+
+@fixture(scope='function')
+def mds_task():
+    return AdtnGetMdsTask(MagicMock(), "test_id")
+
+
+def test_properties(mds_task):
+    assert mds_task.name == "ADTN: Get MDS Task"
+    assert mds_task._omci_managed is False
+    assert mds_task._device is not None
+
+
+def test_perform_get_mds_when_not_managed_via_omci(mds_task):
+    test = MagicMock()
+    mds_task.deferred.addCallback(test)
+    mds_task.perform_get_mds()
+    test.assert_called_with(mds_task.omci_agent.get_device().mib_synchronizer.mib_data_sync)
+
+
+def test_perform_get_mds_when_managed_via_omci(mds_task):
+    with patch('voltha.extensions.omci.tasks.get_mds_task.GetMdsTask.perform_get_mds') as get_mds:
+        mds_task._omci_managed = True
+        get_mds.return_value = 'test'
+        res = mds_task.perform_get_mds()
+        assert res == 'test'
diff --git a/voltha/adapters/adtran_onu/test/omci/test_adtn_install_flow.py b/voltha/adapters/adtran_onu/test/omci/test_adtn_install_flow.py
new file mode 100644
index 0000000..0e1ad99
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/omci/test_adtn_install_flow.py
@@ -0,0 +1,182 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest_twisted
+from pytest import fixture
+from pytest import mark
+from pytest import raises
+from mock import Mock
+from mock import patch
+
+
+from voltha.adapters.adtran_onu.omci.adtn_install_flow import AdtnInstallFlowTask
+from voltha.extensions.omci.tasks.task import Task
+from voltha.adapters.adtran_onu.adtran_onu_handler import AdtranOnuHandler
+from voltha.extensions.omci.openomci_agent import OpenOMCIAgent
+from voltha.adapters.adtran_onu.flow.flow_entry import FlowEntry
+from voltha.adapters.adtran_olt.test.resources.test_adtran_olt_resource_manager import MockRegistry
+from voltha.adapters.adtran_onu.uni_port import UniPort
+from voltha.adapters.adtran_onu.onu_gem_port import GemPort
+from voltha.adapters.adtran_onu.onu_tcont import TCont
+from voltha.extensions.omci.omci_messages import OmciCreateResponse
+from voltha.extensions.omci.omci_defs import ReasonCodes as RC
+
+
+class MockResponse:
+
+    def __init__(self):
+        self.fields = dict()
+
+
+@fixture(scope='function')
+@patch('voltha.adapters.adtran_onu.adtran_onu_handler.registry', MockRegistry())
+def handler():
+    handler = AdtranOnuHandler(Mock(), "test_id")
+    handler.pon_port = Mock()
+    handler.start = Mock()
+    uni_ports = dict()
+    port1 = UniPort(handler, "test1", 1, 1)
+    port2 = UniPort(handler, "test2", 2, 2)
+    port1.entity_id = 1
+    port2.entity_id = 2
+    uni_ports['1'] = port1
+    uni_ports['2'] = port2
+    handler._unis = uni_ports
+    handler.uni_port = Mock(return_value=port1)
+    gem_port = GemPort(1, 1, 1, None)
+    handler._pon.add_gem_port(gem_port)
+    tcont = TCont(1, None, Mock(), 1, True)
+    handler._pon.add_tcont(tcont)
+    handler.enabled = True
+    return handler
+
+
+@fixture()
+def omci_agent():
+    omci = OpenOMCIAgent(Mock())
+    omci.get_device = Mock()
+    return omci
+
+
+@fixture()
+def flow_entry():
+    flow = FlowEntry(Mock(), Mock())
+    return flow
+
+
+@fixture()
+def flow_task(handler, omci_agent, flow_entry):
+    ft = AdtnInstallFlowTask(omci_agent, handler, flow_entry)
+    ft._pon = handler._pon
+    return ft
+
+
+@fixture()
+def mock_res():
+    res = MockResponse()
+    res.fields['success_code'] = RC.Success
+    r = OmciCreateResponse()
+    r.fields['omci_message'] = res
+    return r
+
+
+def test_properties(flow_task, omci_agent, handler):
+    assert flow_task.task_priority == Task.DEFAULT_PRIORITY + 10
+    omci_agent.get_device.assert_called_with(handler.device_id)
+    handler.uni_port.assert_called()
+    handler.pon_port.assert_called()
+
+
+def test_start_should_install_flow(flow_task):
+    with patch('voltha.adapters.adtran_onu.omci.adtn_install_flow.reactor.callLater') as flow_task_reactor:
+        flow_task_reactor.return_value = "test"
+        flow_task.start()
+        flow_task_reactor.assert_called_with(0, flow_task.perform_flow_install)
+        assert flow_task._local_deferred == "test"
+
+
+@mark.parametrize("called_param", [True, False])
+def test_cancel_deferred(flow_task, called_param):
+    flow_task._local_deferred = mock_defer = Mock()
+    flow_task._local_deferred.called = called_param
+    flow_task.cancel_deferred()
+    if not called_param:
+        mock_defer.cancel.assert_called()
+    else:
+        mock_defer.cancel.assert_not_called()
+    assert flow_task._local_deferred is None
+
+
+@mark.parametrize("status, operation, expected_result", [(RC.Success, 'create', True),
+                                                         (RC.InstanceExists, 'set', False),
+                                                         (RC.UnknownInstance, 'delete', True),
+                                                         (RC.UnknownEntity, 'test', False)])
+def test_check_status_and_state(flow_task, mock_res, status, operation, expected_result):
+    flow_task._onu_device.omci_cc = Mock()
+    flow_task._onu_device.omci_cc.return_value = True
+    flow_task.strobe_watchdog = Mock()
+    mock_res.fields['omci_message'].fields['success_code'] = status
+    if status == RC.UnknownEntity:
+        with raises(Exception):
+            flow_task.check_status_and_state(mock_res, operation=operation)
+    else:
+        result = flow_task.check_status_and_state(mock_res, operation=operation)
+        assert result == expected_result
+
+
+@mark.parametrize("install_by_delete", [True, False])
+@pytest_twisted.inlineCallbacks
+def test_perform_flow_install(flow_task, install_by_delete, mock_res):
+    flow_task.check_status_and_state = Mock()
+    flow_task._onu_device.omci_cc.send = Mock()
+    flow_task._install_by_delete = install_by_delete
+    flow_task._onu_device.omci_cc.send.return_value = 'test'
+    yield flow_task.perform_flow_install()
+    if install_by_delete:
+        flow_task.check_status_and_state.assert_any_call('test', operation='delete')
+        flow_task.check_status_and_state.assert_any_call('test', 'flow-recreate-before-set')
+    flow_task.check_status_and_state.assert_any_call('test', 'set-extended-vlan-tagging-operation-configuration-data')
+    flow_task.check_status_and_state.assert_any_call('test', 'flow-set-ext-vlan-tagging-op-config-data-untagged')
+
+
+@mark.parametrize("enable_flag", [True, False])
+def test_perform_flow_install_handles_exceptions_appropriately(flow_task, enable_flag):
+
+    """This test case verifies negative cases as below
+    1: Handler disabled - Should goto else back and make a errCallback
+    2. Handler Enabled but exception thrown during OMCI Send req - Exception should be handled."""
+
+    flow_task.check_status_and_state = Mock()
+    flow_task._handler.enabled = enable_flag
+    flow_task._install_by_delete = not enable_flag
+    flow_task._onu_device.omci_cc = Mock()
+    flow_task._onu_device.omci_cc.send = Mock(side_effect=Exception("test"))
+    error_back = Mock()
+    flow_task.deferred.addErrback(error_back)
+    flow_task.perform_flow_install()
+    error_back.assert_called()
+
+
+def test_perform_flow_install_returns_if_flow_entry_vlan_vid_is_0(flow_task):
+    flow_task.check_status_and_state = Mock()
+    flow_task._onu_device.omci_cc = Mock()
+    flow_task._flow_entry.vlan_vid = 0
+    flow_task.perform_flow_install()
+    flow_task._onu_device.omci_cc.assert_not_called()
+
+
+def test_stop(flow_task):
+    flow_task.cancel_deferred = Mock()
+    flow_task.stop()
+    flow_task.cancel_deferred.assert_called()
diff --git a/voltha/adapters/adtran_onu/test/omci/test_adtn_mib_resync_task.py b/voltha/adapters/adtran_onu/test/omci/test_adtn_mib_resync_task.py
new file mode 100644
index 0000000..a38f00e
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/omci/test_adtn_mib_resync_task.py
@@ -0,0 +1,95 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import mock
+import pytest
+from array import array
+
+from voltha.adapters.adtran_onu.omci.adtn_mib_resync_task import *
+
+
+@pytest.fixture(autouse="True")
+def AMRTInstance():
+    new_mock = mock.MagicMock()
+    result = AdtnMibResyncTask(new_mock, new_mock)
+    return result
+
+#Tests the init function to make sure it calls the init of its parent class and successfully sets omci_fixed
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_resync_task.MibResyncTask.__init__')
+def test_AdtnMibResyncTask_init(mock_MibResyncTask_init):
+    test_mock = mock.MagicMock()
+    test = AdtnMibResyncTask(test_mock, test_mock)
+    assert mock_MibResyncTask_init.call_count == 1
+    assert test.omci_fixed == False
+
+#Tests what happens if omci_fixed is True
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_resync_task.MibResyncTask.compare_mibs')
+def test_compare_mibs_omci_fixed_true(mock_compare_mibs, AMRTInstance):
+    AMRTInstance.omci_fixed = True
+    mock_compare_mibs.return_value = 1, 2, 3
+    test1, test2, test3 = AMRTInstance.compare_mibs('test', 'test')
+    assert test1 == 1
+    assert test2 == 2
+    assert test3 == 3
+
+#Tests compare_mibs for the scenario where omci_fixed is false and on_olt_only is None
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_resync_task.MibResyncTask.compare_mibs')
+def test_compare_mibs_omci_fixed_false_on_olt_only_true(mock_compare_mibs, AMRTInstance):
+    testarr1 = [272, 'test', 'max_gem_payload_size']
+    testarr2 = [272, 'test', 'test']
+    testarr3 = ['test', 'test', 'max_gem_payload_size']
+    testarr4 = [268, 'test', 'test']
+    testarr5 = ['test', 'test', 'test']
+    attr_diffs = [testarr1, testarr2, testarr3, testarr4, testarr5]
+
+    mock_compare_mibs.return_value = None, None, attr_diffs
+
+    _, _, attr_diffs = AMRTInstance.compare_mibs('test', 'test')
+
+    assert mock_compare_mibs.call_count == 1
+    assert attr_diffs[0] == testarr2
+    assert attr_diffs[1] == testarr3
+    assert attr_diffs[2] == testarr5
+
+#Tests compare_mibs for the scenario where omci_fixed is false and on_olt_only is not None
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_resync_task.MibResyncTask.compare_mibs')
+def test_compare_mibs_omci_fixed_false_on_olt_only_not_none(mock_compare_mibs, AMRTInstance):
+    testarr1 = [272, 'test', 'max_gem_payload_size']
+    testarr2 = [272, 'test', 'test']
+    testarr3 = ['test', 'test', 'max_gem_payload_size']
+    testarr4 = [268, 'test', 'test']
+    testarr5 = ['test', 'test', 'test']
+    attr_diffs = [testarr1, testarr2, testarr3, testarr4, testarr5]
+
+    testtup1 = (130, 1)
+    testtup2 = (287, 4)
+    testtup3 = (145, 3456)
+    testtup4 = (130, -56)
+    on_olt_only = [testtup1, testtup2, testtup3, testtup4]
+
+    mock_compare_mibs.return_value = on_olt_only, None, attr_diffs
+
+    on_olt_only, _, attr_diffs = AMRTInstance.compare_mibs('test', 'test')
+
+    assert mock_compare_mibs.call_count == 1
+
+    assert attr_diffs[0] == testarr2
+    assert attr_diffs[1] == testarr3
+    assert attr_diffs[2] == testarr5
+
+    assert on_olt_only[0] == testtup2
+    assert on_olt_only[1] == testtup3
+
+
diff --git a/voltha/adapters/adtran_onu/test/omci/test_adtn_mib_sync.py b/voltha/adapters/adtran_onu/test/omci/test_adtn_mib_sync.py
new file mode 100644
index 0000000..e36d94d
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/omci/test_adtn_mib_sync.py
@@ -0,0 +1,106 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import mock
+import pytest
+
+from voltha.adapters.adtran_onu.omci.adtn_mib_sync import *
+
+@pytest.fixture(autouse="True")
+def AMSInstance():
+    new_mock = mock.MagicMock()
+    result = AdtnMibSynchronizer(new_mock, new_mock, new_mock, new_mock)
+    return result
+
+#Tests the init function to make sure it calls the init of its parent class and successfully sets fields
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_sync.MibSynchronizer.__init__')
+def test_AdtnMibSynchronizer_init(mock_AdtnMibSynchronizer_init):
+    test_mock = mock.MagicMock()
+    test = AdtnMibSynchronizer(test_mock, test_mock, test_mock, test_mock)
+    assert mock_AdtnMibSynchronizer_init.call_count == 1
+    assert test._first_in_sync == True
+    assert test._omci_managed == False
+
+#Tests function when omci managed is True
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_sync.MibSynchronizer.increment_mib_data_sync')
+def test_increment_mib_data_sync_omci_managed_true(mock_increment_mib_data_sync, AMSInstance):
+    AMSInstance._omci_managed = True
+    AMSInstance.increment_mib_data_sync()
+    assert mock_increment_mib_data_sync.call_count == 1
+    assert AMSInstance._mib_data_sync == 0
+
+#Tests function when omci managed is False
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_sync.MibSynchronizer.increment_mib_data_sync')
+def test_increment_mib_data_sync_omci_managed_false(mock_increment_mib_data_sync, AMSInstance):
+    AMSInstance.increment_mib_data_sync()
+    assert mock_increment_mib_data_sync.call_count == 0
+    assert AMSInstance._mib_data_sync == 0
+
+#Tests function when omci managed is true
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_sync.MibSynchronizer.on_enter_in_sync')
+def test_on_enter_in_sync_omci_managed_true(mock_on_enter_in_sync, AMSInstance):
+    AMSInstance._omci_managed = True
+    AMSInstance.on_enter_in_sync()
+    assert mock_on_enter_in_sync.call_count == 1
+
+#Tests function when omci managed is false and first in sync is true
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_sync.MibSynchronizer.on_enter_in_sync')
+def test_on_enter_in_sync_omci_managed_false_first_in_sync_true(mock_on_enter_in_sync, AMSInstance):
+    AMSInstance._first_in_sync = True
+    AMSInstance.on_enter_in_sync()
+    assert mock_on_enter_in_sync.call_count == 1
+    assert AMSInstance._first_in_sync == False
+
+#Tests function when omci managed is false and first in sync is false
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_sync.MibSynchronizer.on_enter_in_sync')
+def test_on_enter_in_sync_omci_managed_false_first_in_sync_false(mock_on_enter_in_sync, AMSInstance):
+    AMSInstance._first_in_sync = False
+    AMSInstance.on_enter_in_sync()
+    assert mock_on_enter_in_sync.call_count == 1
+    assert AMSInstance._audit_delay == 60
+    assert AMSInstance._resync_delay == 120
+
+#Tests function if if statement is triggered (mib data sync is supported)
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_sync.MibSynchronizer.on_enter_auditing')
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_sync.AdtnMibSynchronizer._check_if_mib_data_sync_supported')
+def test_on_enter_auditing_attempt_one(mock_check_if_mib_data_sync_supported, mock_on_enter_auditing, AMSInstance):
+    mock_check_if_mib_data_sync_supported.return_value = True
+    AMSInstance.on_enter_auditing()
+    assert AMSInstance._omci_managed == True
+    assert AMSInstance._resync_delay == 300
+    assert mock_on_enter_auditing.call_count == 1
+
+#Tests function if if statement is false isn't triggered (mib data sync isn't supported)
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_sync.MibSynchronizer.on_enter_auditing')
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_sync.AdtnMibSynchronizer._check_if_mib_data_sync_supported')
+def test_on_enter_auditing_attempt_two(mock_check_if_mib_data_sync_supported, mock_on_enter_auditing, AMSInstance):
+    mock_check_if_mib_data_sync_supported.return_value = False
+    AMSInstance.on_enter_auditing()
+    assert mock_on_enter_auditing.call_count == 1
+
+#Tests that function returns false
+def test__check_if_mib_data_sync_supported(AMSInstance):
+    result = AMSInstance._check_if_mib_data_sync_supported()
+    assert result == False
+
+#Tests this function to ensure it changes the right field and calls the right function
+@mock.patch('voltha.adapters.adtran_onu.omci.adtn_mib_sync.MibSynchronizer.on_mib_reset_response')
+def test_on_mib_reset_response(mock_on_mib_reset_response, AMSInstance):
+    test_mock = mock.MagicMock()
+    AMSInstance.on_mib_reset_response(test_mock, test_mock)
+    assert AMSInstance._first_in_sync == True
+    assert mock_on_mib_reset_response.call_count == 1
+
+
diff --git a/voltha/adapters/adtran_onu/test/resources/__init__.py b/voltha/adapters/adtran_onu/test/resources/__init__.py
new file mode 100644
index 0000000..56f8294
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/resources/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2019-present Adtran, Inc.
+#
+# 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.
diff --git a/voltha/adapters/adtran_onu/test/resources/technology_profile.py b/voltha/adapters/adtran_onu/test/resources/technology_profile.py
new file mode 100644
index 0000000..c21202a
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/resources/technology_profile.py
@@ -0,0 +1,235 @@
+# Copyright 2019-present Adtran, Inc.
+#
+# 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.
+
+
+#copied from voltha confluence
+#https://wiki.opencord.org/display/CORD/Technology+Profile+Instance
+
+
+tech_profile_json = {
+  "name": "4QueueHybridProfileMap1-instance-1",
+  "profile Type": "XPON-Instance",
+  "version": 1,
+  "instance_control": {
+    "onu": "multi-instance",
+    "uni": "single-instance",
+    "num_gem_ports": 4
+  },
+  "alloc-id": 1024,
+  "DBA-Extended-Mode": "disable",
+  "u_s_DBA_traffic_descripter": {
+    "Fixed-bw": 0,
+    "Assured-bw": 0,
+    "Max-bw": 0,
+    "Additional-bw-eligibility": "best_effort"
+  },
+  "us_scheduler": {
+    "additional_bw": "auto",
+    "priority": 0,
+    "weight": 0,
+    "q_sched_policy": "hybrid"
+  },
+  "d_s_traffic_descripter": {
+    "CIR": 0,
+    "CBS": 0,
+    "EIR": 0,
+    "EBS": 0
+  },
+  "d_s_scheduler": {
+    "priority": 0,
+    "weight": 0,
+    "q_sched_policy": "hybrid"
+  },
+  "upstream_gem_port_attribute_list": [
+    {
+      "gemport_id": 1024,
+      "pbit_map": "0b00000101",
+      "aes_encryption": "TRUE",
+      "scheduling_policy": "WRR",
+      "priority_q": 4,
+      "weight": 25,
+      "discard_policy": "TailDrop",
+      "max_q_size": "auto",
+      "discard_config": {
+        "min_threshold": 0,
+        "max_threshold": 0,
+        "max_probability": 0
+      },
+      "GEM_Traffic_Descriptor": "disable",
+      "u_s_traffic_descripter": {
+        "CIR": 0,
+        "CBS": 0,
+        "EIR": 0,
+        "EBS": 0
+      }
+    },
+    {
+      "gemport_id": 1025,
+      "pbit_map": "0b00011010",
+      "aes_encryption": "TRUE",
+      "scheduling_policy": "WRR",
+      "priority_q": 3,
+      "weight": 75,
+      "discard_policy": "TailDrop",
+      "max_q_size": "auto",
+      "discard_config": {
+        "min_threshold": 0,
+        "max_threshold": 0,
+        "max_probability": 0
+      },
+      "GEM_U_S_Traffic_Descriptor": "disable",
+      "u_s_traffic_descripter": {
+        "CIR": 0,
+        "CBS": 0,
+        "EIR": 0,
+        "EBS": 0
+      }
+    },
+    {
+      "gemport_id": 1026,
+      "pbit_map": "0b00100000",
+      "aes_encryption": "TRUE",
+      "scheduling_policy": "StrictPriority",
+      "priority_q": 2,
+      "weight": 0,
+      "discard_policy": "TailDrop",
+      "max_q_size": "auto",
+      "discard_config": {
+        "min_threshold": 0,
+        "max_threshold": 0,
+        "max_probability": 0
+      },
+      "GEM_U_S_Traffic_Descriptor": "disable",
+      "u_s_traffic_descripter": {
+        "CIR": 0,
+        "CBS": 0,
+        "EIR": 0,
+        "EBS": 0
+      }
+    },
+    {
+      "gemport_id": 1027,
+      "pbit_map": "0b11000000",
+      "aes_encryption": "TRUE",
+      "scheduling_policy": "StrictPriority",
+      "priority_q": 1,
+      "weight": 0,
+      "discard_policy": "TailDrop",
+      "max_q_size": "auto",
+      "discard_config": {
+        "min_threshold": 0,
+        "max_threshold": 0,
+        "max_probability": 0
+      },
+      "GEM_U_S_Traffic_Descriptor": "disable",
+      "u_s_traffic_descripter": {
+        "CIR": 0,
+        "CBS": 0,
+        "EIR": 0,
+        "EBS": 0
+      }
+    }
+  ],
+  "downstream_gem_port_attribute_list": [
+    {
+      "gemport_id": 1024,
+      "pbit_map": "0b00000101",
+      "aes_encryption": "TRUE",
+      "scheduling_policy": "WRR",
+      "priority_q": 4,
+      "weight": 10,
+      "discard_policy": "TailDrop",
+      "max_q_size": "auto",
+      "discard_config": {
+        "min_threshold": 0,
+        "max_threshold": 0,
+        "max_probability": 0
+      },
+      "GEM_D_S_Traffic_Descriptor": "disable",
+      "d_s_traffic_descripter": {
+        "CIR": 0,
+        "CBS": 0,
+        "EIR": 0,
+        "EBS": 0
+      }
+    },
+    {
+      "gemport_id": 1025,
+      "pbit_map": "0b00011010",
+      "aes_encryption": "TRUE",
+      "scheduling_policy": "WRR",
+      "priority_q": 3,
+      "weight": 90,
+      "discard_policy": "TailDrop",
+      "max_q_size": "auto",
+      "discard_config": {
+        "min_threshold": 0,
+        "max_threshold": 0,
+        "max_probability": 0
+      },
+      "GEM_D_S_Traffic_Descriptor": "disable",
+      "d_s_traffic_descripter": {
+        "CIR": 0,
+        "CBS": 0,
+        "EIR": 0,
+        "EBS": 0
+      }
+    },
+    {
+      "gemport_id": 1026,
+      "pbit_map": "0b00100000",
+      "aes_encryption": "TRUE",
+      "scheduling_policy": "StrictPriority",
+      "priority_q": 2,
+      "weight": 0,
+      "discard_policy": "TailDrop",
+      "max_q_size": "auto",
+      "discard_config": {
+        "min_threshold": 0,
+        "max_threshold": 0,
+        "max_probability": 0
+      },
+      "GEM_D_S_Traffic_Descriptor": "disable",
+      "d_s_traffic_descripter": {
+        "CIR": 0,
+        "CBS": 0,
+        "EIR": 0,
+        "EBS": 0
+      }
+    },
+    {
+      "gemport_id": 1027,
+      "pbit_map": "0b11000000",
+      "aes_encryption": "TRUE",
+      "scheduling_policy": "StrictPriority",
+      "priority_q": 1,
+      "weight": 25,
+      "discard_policy": "TailDrop",
+      "max_q_size": "auto",
+      "discard_config": {
+        "min_threshold": 0,
+        "max_threshold": 0,
+        "max_probability": 0
+      },
+      "GEM_D_S_Traffic_Descriptor": "disable",
+      "d_s_traffic_descripter": {
+        "CIR": 0,
+        "CBS": 0,
+        "EIR": 0,
+        "EBS": 0
+      }
+    }
+  ]
+}
+
diff --git a/voltha/adapters/adtran_onu/test/test_adtran_onu.py b/voltha/adapters/adtran_onu/test/test_adtran_onu.py
new file mode 100644
index 0000000..6a3e3b3
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/test_adtran_onu.py
@@ -0,0 +1,175 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+from mock import MagicMock
+from mock import patch
+
+
+from voltha.adapters.adtran_onu.adtran_onu import AdtranOnuAdapter
+
+
+@pytest.fixture(scope='function')
+def device_info():
+    device = MagicMock()
+    device.root = False
+    device.vendor = 'Adtran Inc.'
+    device.model = 'n/a'
+    device.hardware_version = 'n/a'
+    device.firmware_version = 'n/a'
+    device.reason = ''
+    device.id = "test_1"
+    return device
+
+
+@pytest.fixture(scope='function')
+def adapter_agent():
+    aa = MagicMock()
+    return aa
+
+
+@pytest.fixture(scope='function')
+def onu_handler():
+    onu_handler = MagicMock()
+    return onu_handler
+
+
+@pytest.fixture(scope='function')
+def onu_adapter(adapter_agent, onu_handler):
+    adtran_onu = AdtranOnuAdapter(adapter_agent, "test_1")
+    adtran_onu.devices_handlers["test_1"] = onu_handler
+    return adtran_onu
+
+
+def test_initialized_properties(onu_adapter):
+    assert onu_adapter.name == 'adtran_onu'
+    assert onu_adapter.adtran_omci is not None
+    assert onu_adapter.omci_agent is not None
+
+
+@pytest.mark.parametrize('method, args', [
+    ('suppress_alarm', (None, )),
+    ('unsuppress_alarm', (None, )),
+    ('download_image', (None, None)),
+    ('activate_image_update', (None, None)),
+    ('cancel_image_download', (None, None)),
+    ('revert_image_update', (None, None)),
+    ('get_image_download_status', (None, None)),
+    ('update_flows_incrementally', (None, None, None)),
+    ('send_proxied_message', (None, None)),
+    ('get_device_details', (None, )),
+    ('change_master_state', (None, )),
+    ('abandon_device', (None, )),
+    ('receive_onu_detect_state', (None, None)),
+    ('receive_packet_out', (None, None, None)),
+])
+def test_method_throws_not_implemented_error(onu_adapter, method, args):
+    with pytest.raises(NotImplementedError):
+        getattr(onu_adapter, method)(*args)
+
+
+@pytest.mark.parametrize('method, args', [
+    ('create_multicast_gemport', (None, None)),
+    ('update_multicast_gemport', (None, None)),
+    ('remove_multicast_gemport', (None, None)),
+    ('create_multicast_distribution_set', (None, None)),
+    ('update_multicast_distribution_set', (None, None)),
+    ('remove_multicast_distribution_set', (None, None))
+])
+def test_method_throws_type_error(onu_adapter, method, args):
+    with pytest.raises(TypeError):
+        getattr(onu_adapter, method)(*args)
+
+
+@pytest.mark.parametrize("device", [device_info(), None])
+def test_receive_inter_adapter_message(onu_adapter, onu_handler, device):
+    msg = dict()
+    msg['proxy_address'] = '1.2.3.4'
+    onu_adapter.adapter_agent.get_child_device_with_proxy_address.return_value = device
+    onu_adapter.receive_inter_adapter_message(msg)
+    onu_adapter.adapter_agent.get_child_device_with_proxy_address.assert_called_with('1.2.3.4')
+    if device is not None:
+        onu_handler.event_messages.put.assert_called_with(msg)
+
+
+def test_receive_proxied_message(onu_adapter, onu_handler, device_info):
+    onu_adapter.adapter_agent.get_child_device_with_proxy_address.return_value = device_info
+    onu_adapter.receive_proxied_message(device_info, 'test')
+    onu_adapter.adapter_agent.get_child_device_with_proxy_address.assert_called_with(device_info)
+    onu_handler.receive_message.assert_called_with('test')
+
+
+def test_create_interface(onu_adapter, onu_handler, device_info):
+    data = {"test": "dummy"}
+    with patch('voltha.adapters.adtran_onu.adtran_onu.reactor.callLater') as call_later:
+        onu_adapter.create_interface(device_info, data)
+    call_later.assert_called_with(0, onu_handler.xpon_create, data)
+
+
+def test_update_interface(onu_adapter, onu_handler, device_info):
+    data = {"test": "dummy"}
+    onu_adapter.update_interface(device_info, data)
+    onu_handler.xpon_update.assert_called_with(data)
+
+
+def test_remove_interface(onu_adapter, onu_handler, device_info):
+    data = {"test": "dummy"}
+    onu_adapter.remove_interface(device_info, data)
+    onu_handler.xpon_remove.assert_called_with(data)
+
+
+def test_create_tcont(onu_adapter, onu_handler, device_info):
+    onu_adapter.create_tcont(device_info, "tcont_data", "traffic_desriptor_data")
+    onu_handler.create_tcont.assert_called_with("tcont_data", "traffic_desriptor_data")
+
+
+def test_update_tcont(onu_adapter, onu_handler, device_info):
+    onu_adapter.update_tcont(device_info, "tcont_data", "traffic_desriptor_data")
+    onu_handler.update_tcont.assert_called_with("tcont_data", "traffic_desriptor_data")
+
+
+def test_remove_tcont(onu_adapter, onu_handler, device_info):
+    onu_adapter.remove_tcont(device_info, "tcont_data", "traffic_desriptor_data")
+    onu_handler.remove_tcont.assert_called_with("tcont_data", "traffic_desriptor_data")
+
+
+def test_create_gemport(onu_adapter, onu_handler, device_info):
+    data = {"test": "dummy"}
+    onu_adapter.create_gemport(device_info, data)
+    onu_handler.xpon_create.assert_called_with(data)
+
+
+def test_update_gemport(onu_adapter, onu_handler, device_info):
+    data = {"test": "dummy"}
+    onu_adapter.update_gemport(device_info, data)
+    onu_handler.xpon_update.assert_called_with(data)
+
+
+def test_remove_gemport(onu_adapter, onu_handler, device_info):
+    data = {"test": "dummy"}
+    onu_adapter.remove_gemport(device_info, data)
+    onu_handler.xpon_remove.assert_called_with(data)
+
+
+def test_adapter_start(onu_adapter):
+    onu_adapter._omci_agent.start = MagicMock()
+    onu_adapter.start()
+    onu_adapter._omci_agent.start.assert_called()
+
+
+def test_adapter_stop(onu_adapter):
+    onu_adapter._omci_agent.stop = MagicMock()
+    onu_adapter.stop()
+    assert onu_adapter._omci_agent is None
+
diff --git a/voltha/adapters/adtran_onu/test/test_adtran_onu_handler.py b/voltha/adapters/adtran_onu/test/test_adtran_onu_handler.py
new file mode 100644
index 0000000..38def80
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/test_adtran_onu_handler.py
@@ -0,0 +1,145 @@
+# Copyright 2019-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+from mock import Mock
+from mock import MagicMock
+from mock import patch
+
+
+from voltha.adapters.adtran_onu.adtran_onu_handler import AdtranOnuHandler
+from voltha.adapters.adtran_onu.adtran_onu import AdtranOnuAdapter
+from voltha.adapters.adtran_olt.test.resources.test_adtran_olt_resource_manager import MockRegistry
+from voltha.protos.common_pb2 import OperStatus, ConnectStatus
+from common.tech_profile.tech_profile import DEFAULT_TECH_PROFILE_TABLE_ID
+from voltha.protos.voltha_pb2 import SelfTestResponse
+from voltha.adapters.adtran_onu.test.resources.technology_profile import tech_profile_json
+from voltha.adapters.adtran_onu.pon_port import PonPort
+
+
+@pytest.fixture()
+def device_info():
+    device = MagicMock()
+    device.id = "test_1"
+    device.parent_id = "test_2"
+    device.proxy_address.device_id = "test_3"
+    return device
+
+
+@pytest.fixture()
+def adapter_agent():
+    aa = MagicMock()
+    return aa
+
+
+@pytest.fixture()
+def onu_adapter(adapter_agent):
+    adtran_onu = AdtranOnuAdapter(adapter_agent, "test_1")
+    return adtran_onu
+
+
+@pytest.fixture()
+@patch('voltha.adapters.adtran_onu.adtran_onu_handler.registry', MockRegistry())
+def onu_handler(onu_adapter):
+    onu_handler = AdtranOnuHandler(onu_adapter, "test_1")
+    return onu_handler
+
+
+def test_onu_handler_initialization(onu_handler):
+    assert str(onu_handler) == "AdtranOnuHandler: test_1"
+
+
+def test_activate(onu_handler, device_info):
+    with patch('voltha.adapters.adtran_onu.adtran_onu_handler.reactor.callLater') as onu_handler_reactor:
+        parent_device = Mock()
+        parent_device.parent_id = "test_5"
+        onu_handler.adapter_agent.get_device.return_value = parent_device
+        onu_handler._pon.get_port = Mock()
+        onu_handler.activate(device_info)
+        onu_handler.adapter_agent.register_for_proxied_messages.assert_called_with(device_info.proxy_address)
+        onu_handler.adapter_agent.update_device.assert_called_with(device_info)
+        onu_handler_reactor.assert_any_call(30, onu_handler.pm_metrics.start_collector)
+
+
+def test_activate_handles_exceptions(onu_handler, device_info):
+    onu_handler.adapter_agent.register_for_proxied_messages.side_effect = Exception()
+    onu_handler.activate(device_info)
+    assert device_info.connect_status == ConnectStatus.UNREACHABLE
+    assert device_info.oper_status == OperStatus.FAILED
+    onu_handler.adapter_agent.update_device.assert_called_with(device_info)
+
+
+def test_reconcile(onu_handler, device_info):
+    parent_device = Mock()
+    parent_device.parent_id = "test_5"
+    onu_handler.adapter_agent.get_device.return_value = parent_device
+    onu_handler.reconcile(device_info)
+    onu_handler.adapter_agent.register_for_proxied_messages.assert_called_with(device_info.proxy_address)
+    assert parent_device.connect_status == ConnectStatus.REACHABLE
+    assert parent_device.oper_status == OperStatus.ACTIVE
+    assert onu_handler.enabled is True
+
+
+@pytest.mark.parametrize("tp_path, expected_res", [('/255/xpon/1024', 255),
+                                                   ('test/test/1', DEFAULT_TECH_PROFILE_TABLE_ID),
+                                                   ('test', None)])
+def test_tp_path_to_tp_id(onu_handler, tp_path, expected_res):
+    tp_id = onu_handler._tp_path_to_tp_id(tp_path)
+    assert tp_id == expected_res
+
+
+@pytest.mark.parametrize("q_sched_policy, alloc_id, sc_po", [("STRICTPRIORITY", 1, 1), ("WRR", 2, 2)])
+def test__create_tcont(onu_handler, q_sched_policy, alloc_id, sc_po):
+    onu_handler._pon.add_tcont = Mock()
+    us_scheduler = dict()
+    us_scheduler['q_sched_policy'] = q_sched_policy
+    us_scheduler['alloc_id'] = alloc_id
+    tcont = onu_handler._create_tcont(1, us_scheduler, 1)
+    onu_handler._pon.add_tcont.assert_called_with(tcont)
+    assert tcont.alloc_id == alloc_id
+    assert tcont.sched_policy == sc_po
+
+
+def test_self_test_device(onu_handler, device_info):
+    res = onu_handler.self_test_device(device_info)
+    assert res.result == SelfTestResponse.NOT_SUPPORTED
+
+
+def test_create_gemports(onu_handler):
+    onu_handler._pon = PonPort.create(onu_handler, onu_handler._pon_port_number)
+    onu_handler._create_gemports(tech_profile_json['upstream_gem_port_attribute_list'],
+                                 tech_profile_json['downstream_gem_port_attribute_list'], MagicMock(), 1, '255')
+    assert onu_handler._pon.gem_ids == [1024, 1025, 1026, 1027]
+
+
+def test__do_tech_profile_configuration(onu_handler):
+    with patch('voltha.adapters.adtran_onu.adtran_onu_handler.AdtranOnuHandler._create_tcont') as tcont_patch,\
+            patch('voltha.adapters.adtran_onu.adtran_onu_handler.AdtranOnuHandler._create_gemports') as gemport_patch:
+        tech_profile_id = '255'
+        uni_id = 1
+        tcont_patch.return_value = tcont_obj = Mock()
+        onu_handler._do_tech_profile_configuration(uni_id, tech_profile_json, tech_profile_id)
+        upstream = tech_profile_json['upstream_gem_port_attribute_list']
+        downstream = tech_profile_json['downstream_gem_port_attribute_list']
+        us_scheduler = tech_profile_json['us_scheduler']
+        tcont_patch.assert_called_with(uni_id, us_scheduler, tech_profile_id)
+        gemport_patch.assert_called_with(upstream, downstream, tcont_obj, uni_id, tech_profile_id)
+
+
+def test_update_pm_config(onu_handler, device_info):
+        onu_handler.pm_metrics = Mock()
+        pm_config = Mock()
+        onu_handler.update_pm_config(device_info, pm_config)
+        onu_handler.pm_metrics.update.assert_called_with(pm_config)
+
diff --git a/voltha/adapters/adtran_onu/test/test_heartbeat.py b/voltha/adapters/adtran_onu/test/test_heartbeat.py
new file mode 100644
index 0000000..dd6d182
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/test_heartbeat.py
@@ -0,0 +1,244 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+from mock import patch, MagicMock
+from voltha.adapters.adtran_onu.heartbeat import HeartBeat
+from voltha.adapters.adtran_onu.adtran_onu_handler import AdtranOnuHandler
+from voltha.protos.common_pb2 import OperStatus, ConnectStatus
+
+
+#####################################
+# Constants for moodule-level scope #
+#####################################
+
+DEVICE_ID = 0x12345678          # Arbitrary device ID
+DELAY = 100                     # Delay for HeartBeat._start()
+
+
+############
+# Fixtures #
+############
+
+@pytest.fixture(scope='function', name='fxt_heartbeat')
+def heartbeat():
+    with patch('voltha.adapters.adtran_onu.heartbeat.structlog.get_logger'):
+        return HeartBeat(MagicMock(spec=AdtranOnuHandler), DEVICE_ID)
+
+
+##############
+# Unit tests #
+##############
+
+# Basic test of HeartBeat() object creation
+def test_heartbeat_init(fxt_heartbeat):
+    assert fxt_heartbeat._device_id == DEVICE_ID
+
+
+# Test Heartbeat.__str__() to ensure proper return value
+def test_heartbeat___str__(fxt_heartbeat):
+    assert str(fxt_heartbeat) == "HeartBeat: count: 0, miss: 0"
+
+
+# Test static method constructor for HeartBeat()
+def test_heartbeat_create():
+    handler = MagicMock(spec=AdtranOnuHandler)
+    heartbeat = HeartBeat.create(handler, DEVICE_ID)
+    assert heartbeat._handler == handler
+    assert heartbeat._device_id == DEVICE_ID
+
+
+# Test Heartbeat._start() for expected operation
+def test_heartbeat__start(fxt_heartbeat):
+    with patch('voltha.adapters.adtran_onu.heartbeat.reactor.callLater') as mk_callLater:
+        fxt_heartbeat._start(DELAY)
+    mk_callLater.assert_called_once_with(DELAY, fxt_heartbeat.check_pulse)
+    assert fxt_heartbeat._defer is not None
+
+
+# Test Heartbeat._stop() for expected operation
+def test_heartbeat__stop(fxt_heartbeat):
+    # Must save mock reference because source code clears the _defer attribute
+    fxt_heartbeat._defer = mk_defer = MagicMock()
+    fxt_heartbeat._defer.called = MagicMock()
+    fxt_heartbeat._defer.called.return_value = False
+    fxt_heartbeat._stop()
+    mk_defer.cancel.assert_called_once_with()
+
+
+@pytest.mark.parametrize("setting, result", [(True, True), (False, False)])
+# Test Heartbeat.enabled() property for expected operation
+def test_heartbeat_enabled_getter(fxt_heartbeat, setting, result):
+    fxt_heartbeat._enabled = setting
+    assert fxt_heartbeat.enabled == result
+
+
+@pytest.mark.parametrize("setting, result", [(True, True), (False, False)])
+# Test Heartbeat.enabled() property for expected operation
+def test_heartbeat_enabled_setter(fxt_heartbeat, setting, result):
+    fxt_heartbeat._enabled = False
+    fxt_heartbeat.enabled = setting
+    assert fxt_heartbeat._enabled == result
+
+
+# Test Heartbeat.check_item() property for expected operation
+def test_heartbeat_check_item(fxt_heartbeat):
+    assert fxt_heartbeat.check_item == 'vendor_id'
+
+
+# Test Heartbeat.check_value() property for expected operation
+def test_heartbeat_check_value(fxt_heartbeat):
+    assert fxt_heartbeat.check_value == 'ADTN'
+
+
+@pytest.mark.parametrize("setting, result", [(True, True), (False, False)])
+# Test Heartbeat.alarm_active() property for expected operation
+def test_heartbeat_alarm_active(fxt_heartbeat, setting, result):
+    fxt_heartbeat._alarm_active = setting
+    assert fxt_heartbeat.alarm_active == result
+
+
+# Test Heartbeat.heartbeat_count() property for expected operation
+def test_heartbeat_heartbeat_count(fxt_heartbeat):
+    fxt_heartbeat._heartbeat_count = 10
+    assert fxt_heartbeat.heartbeat_count == 10
+
+
+# Test Heartbeat.heartbeat_miss() property for expected operation
+def test_heartbeat_heartbeat_miss(fxt_heartbeat):
+    fxt_heartbeat._heartbeat_miss = 5
+    assert fxt_heartbeat.heartbeat_miss == 5
+
+
+# Test Heartbeat.alarms_raised_count() property for expected operation
+def test_heartbeat_alarms_raised_count(fxt_heartbeat):
+    fxt_heartbeat._alarms_raised_count = 2
+    assert fxt_heartbeat.alarms_raised_count == 2
+
+
+# Test Heartbeat.check_pulse() for expected operation
+def test_heartbeat_check_pulse(fxt_heartbeat):
+    fxt_heartbeat._enabled = True
+    fxt_heartbeat.check_pulse()
+    fxt_heartbeat._handler.openomci.omci_cc.send.assert_called_once()
+    fxt_heartbeat._defer.addCallbacks.assert_called_once_with(fxt_heartbeat._heartbeat_success,
+                                                              fxt_heartbeat._heartbeat_fail)
+
+
+# Test Heartbeat.check_pulse() for exception from calling send()
+def test_heartbeat_check_pulse_exception(fxt_heartbeat):
+    with patch('voltha.adapters.adtran_onu.heartbeat.reactor.callLater') as mk_callLater:
+        fxt_heartbeat._enabled = True
+        fxt_heartbeat._handler.openomci.omci_cc.send.side_effect = exception = AssertionError()
+        fxt_heartbeat.check_pulse()
+    fxt_heartbeat._handler.openomci.omci_cc.send.assert_called_once()
+    fxt_heartbeat._defer.addCallbacks.assert_not_called()
+    mk_callLater.assert_called_once_with(5, fxt_heartbeat._heartbeat_fail, exception)
+
+
+@pytest.mark.parametrize("id_code, id_resp, hb_miss, reason",
+                         [('vendor_id', 'ADTN', 0, ''),
+                          ('vendor_id', 'ABCD', HeartBeat.HEARTBEAT_FAILED_LIMIT,
+                           "Invalid vendor_id, got 'ABCD' but expected 'ADTN'"),
+                          ('invalid', 'bogus', HeartBeat.HEARTBEAT_FAILED_LIMIT,
+                           "vendor_id")])
+# Test Heartbeat._heartbeat_success() for various parametrized conditions
+def test_heartbeat__heartbeat_success(fxt_heartbeat, id_code, id_resp, hb_miss, reason):
+    results = MagicMock()
+    results.getfieldval.return_value = MagicMock()
+    results.getfieldval.return_value.getfieldval.return_value = {id_code: id_resp}
+    fxt_heartbeat.heartbeat_check_status = MagicMock()
+    fxt_heartbeat._heartbeat_miss = fxt_heartbeat.heartbeat_failed_limit + 1
+    fxt_heartbeat._heartbeat_success(results)
+    assert fxt_heartbeat._heartbeat_miss == hb_miss
+    assert fxt_heartbeat.heartbeat_last_reason == reason
+    fxt_heartbeat.heartbeat_check_status.assert_called_once_with()
+
+
+# Test Heartbeat._heartbeat_fail() for incrementing _heartbeat_miss attribute and proper reason setting
+def test_heartbeat__heartbeat_fail(fxt_heartbeat):
+    fxt_heartbeat.heartbeat_check_status = MagicMock()
+    fxt_heartbeat._heartbeat_miss = fxt_heartbeat.heartbeat_failed_limit - 1
+    fxt_heartbeat._heartbeat_fail(None)
+    assert fxt_heartbeat._heartbeat_miss == fxt_heartbeat.heartbeat_failed_limit
+    assert fxt_heartbeat.heartbeat_last_reason == 'OMCI connectivity error'
+    fxt_heartbeat.heartbeat_check_status.assert_called_once_with()
+
+
+@pytest.mark.parametrize("active", [True, False])
+# Test Heartbeat.on_heartbeat_alarm() property for expected operation
+def test_heartbeat_on_heartbeat_alarm(fxt_heartbeat, active):
+    fxt_heartbeat.on_heartbeat_alarm(active)
+
+
+@pytest.mark.parametrize("hb_miss, dcs_in, aa_in, dcs_out, dos_out, dr_out, aa_out, arc, oha",
+                         [(HeartBeat.HEARTBEAT_FAILED_LIMIT, ConnectStatus.REACHABLE, False, ConnectStatus.UNREACHABLE,
+                          OperStatus.FAILED, 'REASON', True, 5, True),
+                          (0, ConnectStatus.UNKNOWN, True, ConnectStatus.REACHABLE,
+                           OperStatus.ACTIVE, '', False, 6, False)])
+# Test Heartbeat.heartbeat_check_status() for various parametrized conditions
+def test_heartbeat_heartbeat_check_status(fxt_heartbeat, hb_miss, dcs_in, aa_in, dcs_out, dos_out, dr_out, aa_out, arc, oha):
+    with patch('voltha.adapters.adtran_onu.heartbeat.reactor.callLater') as mk_callLater, \
+            patch('voltha.extensions.alarms.heartbeat_alarm.HeartbeatAlarm', autospec=True):
+        fxt_heartbeat.on_heartbeat_alarm = MagicMock()
+        fxt_heartbeat._handler.alarms = MagicMock()
+        fxt_heartbeat._handler.adapter_agent = MagicMock()
+        fxt_heartbeat._handler.adapter_agent.get_device.return_value = device = MagicMock()
+        device.connect_status = dcs_in
+        device.oper_status = OperStatus.UNKNOWN
+        device.reason = None
+        fxt_heartbeat.heartbeat_last_reason = 'REASON'
+        fxt_heartbeat._heartbeat_miss = hb_miss
+        fxt_heartbeat._alarm_active = aa_in
+        fxt_heartbeat._alarms_raised_count = 5
+        fxt_heartbeat._enabled = True
+        fxt_heartbeat._heartbeat_count = 10
+        fxt_heartbeat.heartbeat_check_status()
+
+    fxt_heartbeat._handler.adapter_agent.update_device.assert_called_once_with(device)
+    assert device.connect_status == dcs_out
+    assert device.oper_status == dos_out
+    assert device.reason == dr_out
+    assert fxt_heartbeat._alarm_active == aa_out
+    assert fxt_heartbeat._alarms_raised_count == arc
+    fxt_heartbeat.on_heartbeat_alarm.assert_called_once_with(oha)
+    fxt_heartbeat.log.exception.assert_not_called()
+    assert fxt_heartbeat.heartbeat_count == 11
+    mk_callLater.assert_called_once_with(fxt_heartbeat.heartbeat_interval, fxt_heartbeat.check_pulse)
+
+
+# Test Heartbeat.heartbeat_check_status() for AssertionError in call to _handler.adapter_agent.update_device()
+def test_heartbeat_heartbeat_check_status_error(fxt_heartbeat):
+    with patch('voltha.adapters.adtran_onu.heartbeat.reactor.callLater') as mk_callLater, \
+            patch('voltha.extensions.alarms.heartbeat_alarm.HeartbeatAlarm', autospec=True):
+        fxt_heartbeat.on_heartbeat_alarm = MagicMock()
+        fxt_heartbeat._handler.alarms = MagicMock()
+        fxt_heartbeat._handler.adapter_agent = MagicMock()
+        fxt_heartbeat._handler.adapter_agent.get_device.return_value = device = MagicMock()
+        fxt_heartbeat._handler.adapter_agent.update_device.side_effect = AssertionError()
+        device.connect_status = ConnectStatus.REACHABLE
+        device.oper_status = OperStatus.UNKNOWN
+        device.reason = None
+        fxt_heartbeat.heartbeat_last_reason = 'REASON'
+        fxt_heartbeat._heartbeat_miss = HeartBeat.HEARTBEAT_FAILED_LIMIT
+        fxt_heartbeat._alarm_active = False
+        fxt_heartbeat._alarms_raised_count = 5
+        fxt_heartbeat._enabled = False
+        fxt_heartbeat._heartbeat_count = 10
+        fxt_heartbeat.heartbeat_check_status()
+
+    fxt_heartbeat._handler.adapter_agent.update_device.assert_called_once_with(device)
+    fxt_heartbeat.log.exception.assert_called_once()
+    assert fxt_heartbeat.heartbeat_count == 10
+    mk_callLater.assert_not_called()
diff --git a/voltha/adapters/adtran_onu/test/test_onu_gem_port.py b/voltha/adapters/adtran_onu/test/test_onu_gem_port.py
new file mode 100644
index 0000000..dd9df77
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/test_onu_gem_port.py
@@ -0,0 +1,22 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 voltha.adapters.adtran_olt.xpon.olt_gem_port import OltGemPort
+import pytest
+from mock import patch, MagicMock
+import mock
+import json
+import pytest_twisted
+
+
diff --git a/voltha/adapters/adtran_onu/test/test_onu_tcont.py b/voltha/adapters/adtran_onu/test/test_onu_tcont.py
new file mode 100644
index 0000000..acd40d3
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/test_onu_tcont.py
@@ -0,0 +1,155 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+import pytest_twisted
+import mock
+
+from voltha.adapters.adtran_onu.onu_traffic_descriptor import OnuTrafficDescriptor
+from voltha.adapters.adtran_onu.onu_tcont import OnuTCont
+from voltha.adapters.adtran_onu.adtran_onu_handler import AdtranOnuHandler
+
+EXAMPLE_DEVICE_ID = 0x12345678
+EXAMPLE_TCONT_ENTITY_ID = 3345
+
+
+@pytest.fixture()
+def sched_policy():
+    q_sched_policy = {
+        'strictpriority': 1,
+        'wrr': 2
+    }
+    return q_sched_policy
+
+
+@pytest.fixture()
+def tcont_data(sched_policy):
+    tcont_data = {
+        'tech-profile-id': 57,
+        'uni-id': 633,
+        'alloc-id': 2048,
+        'q-sched-policy': sched_policy
+    }
+    return tcont_data
+
+
+@pytest.fixture()
+def onu_handler():
+    handler = mock.MagicMock(spec=AdtranOnuHandler)
+    handler.device_id = EXAMPLE_DEVICE_ID
+    return handler
+
+
+@pytest.fixture(name='tcont_fixture')
+def onu_tcont(onu_handler, sched_policy, tcont_data):
+    with mock.patch('voltha.adapters.adtran_onu.onu_tcont.structlog.get_logger'):
+        return OnuTCont(onu_handler,
+                        tcont_data['alloc-id'],
+                        sched_policy,
+                        tcont_data['tech-profile-id'],
+                        tcont_data['uni-id'],
+                        mock.MagicMock(spec=OnuTrafficDescriptor))
+
+
+def test_onu_tcont_create(onu_handler, tcont_data):
+    td = mock.MagicMock(spec=OnuTrafficDescriptor)
+    onu_tcont = OnuTCont.create(onu_handler, tcont_data, td)
+    onu_tcont._entity_id = 1234
+    assert onu_tcont._handler == onu_handler
+    assert onu_tcont.entity_id == 1234
+    assert onu_tcont.tech_profile_id == 57
+    assert onu_tcont.uni_id == 633
+    assert onu_tcont.alloc_id == 2048
+    assert isinstance(onu_tcont.sched_policy, dict)
+    assert onu_tcont.FREE_TCONT_ALLOC_ID == 0xFFFF
+    assert onu_tcont.FREE_GPON_TCONT_ALLOC_ID == 0xFF
+
+
+@pytest_twisted.inlineCallbacks
+def test_add_mock_onu_tcont_to_hardware(tcont_fixture):
+    with mock.patch("voltha.extensions.omci.omci_cc") as omci:
+        tcont_fixture._is_mock = True
+        tcont_fixture._handler.device_id = EXAMPLE_DEVICE_ID
+        output = yield tcont_fixture.add_to_hardware(omci, tcont_fixture.FREE_TCONT_ALLOC_ID, tcont_fixture.alloc_id)
+        assert output == "mock"
+
+
+@pytest_twisted.inlineCallbacks
+def test_add_onu_tcont_to_hardware(tcont_fixture):
+    with mock.patch("voltha.extensions.omci.omci_cc") as omci:
+        tcont_fixture._is_mock = False
+        yield tcont_fixture.add_to_hardware(omci, EXAMPLE_TCONT_ENTITY_ID, tcont_fixture.alloc_id)
+        assert tcont_fixture._free_alloc_id == tcont_fixture.alloc_id
+        omci.send.assert_called_once()
+
+
+@pytest_twisted.inlineCallbacks
+def test_add_onu_tcont_to_hardware_default_id(tcont_fixture):
+    with mock.patch("voltha.extensions.omci.omci_cc") as omci:
+        tcont_fixture._is_mock = False
+        yield tcont_fixture.add_to_hardware(omci, EXAMPLE_TCONT_ENTITY_ID)
+        assert tcont_fixture._free_alloc_id == tcont_fixture.FREE_TCONT_ALLOC_ID
+        omci.send.assert_called_once()
+
+
+@pytest_twisted.inlineCallbacks
+def test_entity_already_set_onu_tcont_to_hardware(tcont_fixture):
+    with mock.patch("voltha.extensions.omci.omci_cc") as omci:
+        tcont_fixture._is_mock = False
+        output = yield tcont_fixture.add_to_hardware(omci, tcont_fixture.entity_id, tcont_fixture.alloc_id)
+        assert output == "Already set"
+
+
+@pytest_twisted.inlineCallbacks
+def test_already_assigned_onu_tcont_to_hardware(tcont_fixture):
+    with mock.patch("voltha.extensions.omci.omci_cc") as omci:
+        tcont_fixture._is_mock = False
+        tcont_fixture._entity_id = 1234
+        with pytest.raises(KeyError):
+            yield tcont_fixture.add_to_hardware(omci, EXAMPLE_TCONT_ENTITY_ID, tcont_fixture.alloc_id)
+
+
+@pytest_twisted.inlineCallbacks
+def test_add_onu_tcont_to_hardware_exception(tcont_fixture):
+    with mock.patch("voltha.extensions.omci.omci_cc") as omci:
+        tcont_fixture._is_mock = False
+        tcont_fixture.alloc_id = "some bad value"
+        with pytest.raises(Exception):
+            yield tcont_fixture.add_to_hardware(omci, EXAMPLE_TCONT_ENTITY_ID, tcont_fixture.alloc_id)
+
+
+@pytest_twisted.inlineCallbacks
+def test_remove_mock_onu_tcont_from_hardware(tcont_fixture):
+    with mock.patch("voltha.extensions.omci.omci_cc") as omci:
+        tcont_fixture._is_mock = True
+        output = yield tcont_fixture.remove_from_hardware(omci)
+        assert output == "mock"
+
+
+@pytest_twisted.inlineCallbacks
+def test_remove_onu_tcont_from_hardware_exception(tcont_fixture):
+    with mock.patch("voltha.extensions.omci.omci_cc") as omci:
+        tcont_fixture._is_mock = False
+        omci.send.side_effect = Exception
+        with pytest.raises(Exception):
+            yield tcont_fixture.remove_from_hardware(omci)
+
+
+@pytest_twisted.inlineCallbacks
+def test_remove_onu_tcont_to_hardware(tcont_fixture):
+    with mock.patch("voltha.extensions.omci.omci_cc") as omci:
+        tcont_fixture._is_mock = False
+        tcont_fixture._entity_id = 2048
+        yield tcont_fixture.remove_from_hardware(omci)
+        omci.send.assert_called_once()
diff --git a/voltha/adapters/adtran_onu/test/test_onu_traffic_descriptor.py b/voltha/adapters/adtran_onu/test/test_onu_traffic_descriptor.py
new file mode 100644
index 0000000..5db3b97
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/test_onu_traffic_descriptor.py
@@ -0,0 +1,66 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+from voltha.adapters.adtran_onu.onu_traffic_descriptor import OnuTrafficDescriptor
+from voltha.adapters.adtran_olt.xpon.traffic_descriptor import TrafficDescriptor
+from voltha.adapters.adtran_olt.xpon.best_effort import BestEffort
+
+
+FIXED_BW = 1000000000               # String name of UNI port
+ASSURED_BW = 2000000000             # String name of UNI port
+MAX_BW = 4000000000                 # String name of UNI port
+
+
+# Basic test of OnuTrafficDescriptor() object creation
+def test_traf_desc_init():
+    otd = OnuTrafficDescriptor(FIXED_BW, ASSURED_BW, MAX_BW, TrafficDescriptor.AdditionalBwEligibility.NONE, None)
+    assert otd.fixed_bandwidth == FIXED_BW
+    assert otd.assured_bandwidth == ASSURED_BW
+    assert otd.maximum_bandwidth == MAX_BW
+    assert otd.additional_bandwidth_eligibility == TrafficDescriptor.AdditionalBwEligibility.NONE
+    assert otd.best_effort is None
+
+
+@pytest.mark.parametrize("f_bw, a_bw, m_bw, addl_ind", [(FIXED_BW, ASSURED_BW, MAX_BW, 0),
+                                                        (FIXED_BW, ASSURED_BW, MAX_BW, 1),
+                                                        (FIXED_BW, ASSURED_BW, MAX_BW, 2),
+                                                        (FIXED_BW, ASSURED_BW, MAX_BW, 3)])
+# Test static method constructor for OnuTrafficDescriptor() for various parametrized combinations
+def test_traf_desc_create(f_bw, a_bw, m_bw, addl_ind):
+    otd_data = dict()
+    otd_data['fixed-bandwidth'] = f_bw
+    otd_data['assured-bandwidth'] = a_bw
+    otd_data['maximum-bandwidth'] = m_bw
+    otd_data['additional-bw-eligibility-indicator'] = addl_ind
+    otd_data['priority'] = 0
+    otd_data['weight'] = 0
+    otd = OnuTrafficDescriptor.create(otd_data)
+    assert otd.fixed_bandwidth == f_bw
+    assert otd.assured_bandwidth == a_bw
+    assert otd.maximum_bandwidth == m_bw
+
+    if addl_ind == 0:
+        assert otd.additional_bandwidth_eligibility == TrafficDescriptor.AdditionalBwEligibility.NONE
+    elif addl_ind == 1:
+        assert otd.additional_bandwidth_eligibility == TrafficDescriptor.AdditionalBwEligibility.BEST_EFFORT_SHARING
+    elif addl_ind == 2:
+        assert otd.additional_bandwidth_eligibility == TrafficDescriptor.AdditionalBwEligibility.NON_ASSURED_SHARING
+    elif addl_ind == 3:
+        assert otd.additional_bandwidth_eligibility == TrafficDescriptor.AdditionalBwEligibility.NONE
+
+    if addl_ind == 1:
+        assert isinstance(otd.best_effort, BestEffort)
+    else:
+        assert otd.best_effort is None
diff --git a/voltha/adapters/adtran_onu/test/test_uni_port.py b/voltha/adapters/adtran_onu/test/test_uni_port.py
new file mode 100644
index 0000000..9928ef4
--- /dev/null
+++ b/voltha/adapters/adtran_onu/test/test_uni_port.py
@@ -0,0 +1,300 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+from mock import patch, MagicMock
+from voltha.adapters.adtran_onu.uni_port import UniPort
+from voltha.protos.common_pb2 import OperStatus, AdminState
+from voltha.protos.device_pb2 import Port
+from voltha.protos.openflow_13_pb2 import OFPPS_LIVE, OFPPF_10GB_FD, OFPPF_FIBER
+
+
+UNI_PORT_NAME = 'uni-10240'         # String name of UNI port
+UNI_PHYS_PORT_NUM = 10240           # Integer physical port number for UNI
+UNI_OF_PORT_NUM = 'uni-10240'       # String OpenFlow port number for UNI (legacy XPON mode)
+DEVICE_ID = 0                       # Arbitrary device ID
+LOGICAL_DEVICE_ID = 100             # Arbitrary logical device ID
+LOGICAL_PORT_NUM = 10240            # Arbitrary logical port number
+
+
+@pytest.fixture(scope='function', name='fxt_uni_port')
+def uni_port():
+    with patch('voltha.adapters.adtran_onu.uni_port.structlog.get_logger'):
+        handler = MagicMock()
+        handler.device_id = DEVICE_ID
+        handler.logical_device_id = LOGICAL_DEVICE_ID
+        return UniPort(handler, UNI_PORT_NAME, UNI_PHYS_PORT_NUM, UNI_OF_PORT_NUM)
+
+
+# Basic test of UniPort() object creation
+def test_uni_port_init(fxt_uni_port):
+    assert fxt_uni_port._name == UNI_PORT_NAME
+    assert fxt_uni_port._port_number == UNI_PHYS_PORT_NUM
+    assert fxt_uni_port._ofp_port_no == UNI_OF_PORT_NUM
+
+
+# Test UniPort.__str__() to ensure proper return value
+def test_uni_port___str__(fxt_uni_port):
+    assert str(fxt_uni_port) == "UniPort: {}:{}".format(UNI_PORT_NAME, UNI_PHYS_PORT_NUM)
+
+
+# Test static method constructor for UniPort()
+def test_uni_port_create():
+    handler = MagicMock()
+    handler.device_id = DEVICE_ID
+    uni_port = UniPort.create(handler, UNI_PORT_NAME, UNI_PHYS_PORT_NUM, UNI_OF_PORT_NUM)
+    assert uni_port._handler == handler
+    assert uni_port._name == UNI_PORT_NAME
+    assert uni_port._port_number == UNI_PHYS_PORT_NUM
+    assert uni_port._ofp_port_no == UNI_OF_PORT_NUM
+
+
+# Test UniPort._start() for expected operation
+def test_uni_port__start(fxt_uni_port):
+    fxt_uni_port._cancel_deferred = MagicMock()
+    fxt_uni_port._update_adapter_agent = MagicMock()
+    fxt_uni_port._start()
+    assert fxt_uni_port._admin_state == AdminState.ENABLED
+    assert fxt_uni_port._oper_status == OperStatus.ACTIVE
+    fxt_uni_port._cancel_deferred.assert_called_once_with()
+    fxt_uni_port._update_adapter_agent.assert_called_once_with()
+
+
+# # Test UniPort._stop() for expected operation
+def test_uni_port__stop(fxt_uni_port):
+    fxt_uni_port._cancel_deferred = MagicMock()
+    fxt_uni_port._update_adapter_agent = MagicMock()
+    fxt_uni_port._stop()
+    assert fxt_uni_port._admin_state == AdminState.DISABLED
+    assert fxt_uni_port._oper_status == OperStatus.UNKNOWN
+    fxt_uni_port._cancel_deferred.assert_called_once_with()
+    fxt_uni_port._update_adapter_agent.assert_called_once_with()
+
+
+# # Test UniPort.delete() for expected operation
+def test_uni_port_delete(fxt_uni_port):
+    fxt_uni_port._start = MagicMock()
+    fxt_uni_port._stop = MagicMock()
+    fxt_uni_port.delete()
+    assert fxt_uni_port._enabled is False
+    assert fxt_uni_port._handler is None
+
+
+# # Test UniPort._cancel_deferred() for expected operation
+def test_uni_port__cancel_deferred(fxt_uni_port):
+    fxt_uni_port._cancel_deferred()
+
+
+# Test UniPort.name() getter property for expected operation
+def test_uni_port_name_getter(fxt_uni_port):
+    fxt_uni_port._name = 'uni-10256'
+    assert fxt_uni_port.name == 'uni-10256'
+
+
+@pytest.mark.parametrize("setting", [True, False])
+# Test UniPort.enabled() getter property for expected operation
+def test_uni_port_enabled_getter(fxt_uni_port, setting):
+    fxt_uni_port._enabled = setting
+    assert fxt_uni_port.enabled == setting
+
+
+@pytest.mark.parametrize("initial, setting", [(False, True), (True, False), (False, False), (True, True)])
+# Test UniPort.enabled() setter property for expected operation
+def test_uni_port_enabled_setter(fxt_uni_port, initial, setting):
+    fxt_uni_port._start = MagicMock()
+    fxt_uni_port._stop = MagicMock()
+    fxt_uni_port._enabled = initial
+    fxt_uni_port.enabled = setting
+    assert fxt_uni_port._enabled == setting
+    if (initial is False) and (setting is True):
+        fxt_uni_port._start.assert_called_once_with()
+        fxt_uni_port._stop.assert_not_called()
+    elif (initial is True) and (setting is False):
+        fxt_uni_port._start.assert_not_called()
+        fxt_uni_port._stop.assert_called_once_with()
+    else:
+        fxt_uni_port._start.assert_not_called()
+        fxt_uni_port._stop.assert_not_called()
+
+
+# Test UniPort.port_number() getter property for expected operation
+def test_uni_port_port_number_getter(fxt_uni_port):
+    fxt_uni_port._port_number = 10256
+    assert fxt_uni_port.port_number == 10256
+
+
+# Test UniPort.mac_bridge_port_num() getter property for expected operation
+def test_uni_port_mac_bridge_port_num_getter(fxt_uni_port):
+    fxt_uni_port._mac_bridge_port_num = 1
+    assert fxt_uni_port.mac_bridge_port_num == 1
+
+
+# Test UniPort.mac_bridge_port_num() setter property for expected operation
+def test_uni_port_mac_bridge_port_num_setter(fxt_uni_port):
+    fxt_uni_port._mac_bridge_port_num = 0
+    fxt_uni_port.mac_bridge_port_num = 1
+    assert fxt_uni_port._mac_bridge_port_num == 1
+
+
+# Test UniPort.entity_id() getter property for expected operation
+def test_uni_port_entity_id_getter(fxt_uni_port):
+    fxt_uni_port._entity_id = 1
+    assert fxt_uni_port.entity_id == 1
+
+
+# Test UniPort.entity_id() setter property for expected operation
+def test_uni_port_entity_id_setter(fxt_uni_port):
+    fxt_uni_port._entity_id = None
+    fxt_uni_port.entity_id = 1
+    assert fxt_uni_port._entity_id == 1
+
+
+# Test UniPort.entity_id() setter property for being called more than once (can only set entity_id once)
+def test_uni_port_entity_id_setter_already_set(fxt_uni_port):
+    fxt_uni_port._entity_id = 1
+    with pytest.raises(AssertionError):
+        fxt_uni_port.entity_id = 2
+    assert fxt_uni_port._entity_id == 1
+
+
+# Test UniPort.logical_port_number() getter property for expected operation
+def test_uni_port_logical_port_number_getter(fxt_uni_port):
+    fxt_uni_port._logical_port_number = 10256
+    assert fxt_uni_port.logical_port_number == 10256
+
+
+# Test UniPort._update_adapter_agent() for expected operation with self._port = None
+def test_uni_port__update_adapter_agent_port_new(fxt_uni_port):
+    fxt_uni_port._port = None
+    fxt_uni_port.get_port = MagicMock()
+    fxt_uni_port._update_adapter_agent()
+    fxt_uni_port.get_port.assert_called_once_with()
+    fxt_uni_port._handler.adapter_agent.add_port.assert_called_once_with(DEVICE_ID, fxt_uni_port.get_port.return_value)
+
+
+# Test UniPort._update_adapter_agent() for expected operation with self._port != None
+def test_uni_port__update_adapter_agent_port_exists(fxt_uni_port):
+    fxt_uni_port._admin_state = AdminState.ENABLED
+    fxt_uni_port._oper_status = OperStatus.ACTIVE
+    fxt_uni_port._port = MagicMock()
+    fxt_uni_port.get_port = MagicMock()
+    fxt_uni_port._update_adapter_agent()
+    assert fxt_uni_port._port.admin_state == AdminState.ENABLED
+    assert fxt_uni_port._port.oper_status == OperStatus.ACTIVE
+    fxt_uni_port.get_port.assert_called_once_with()
+    fxt_uni_port._handler.adapter_agent.add_port.assert_called_once_with(DEVICE_ID, fxt_uni_port.get_port.return_value)
+
+
+# Test UniPort._update_adapter_agent() for failed operation due to KeyError exception in add_port() call
+def test_uni_port__update_adapter_agent_port_add_key_excep(fxt_uni_port):
+    fxt_uni_port._port = None
+    fxt_uni_port.get_port = MagicMock()
+    fxt_uni_port._handler.adapter_agent.add_port.side_effect = KeyError()
+    fxt_uni_port._update_adapter_agent()
+    fxt_uni_port.get_port.assert_called_once_with()
+    fxt_uni_port._handler.adapter_agent.add_port.assert_called_once_with(DEVICE_ID, fxt_uni_port.get_port.return_value)
+
+
+# Test UniPort._update_adapter_agent() for failed operation due to any other exception in add_port() call
+def test_uni_port__update_adapter_agent_port_add_other_excep(fxt_uni_port):
+    fxt_uni_port._port = None
+    fxt_uni_port.get_port = MagicMock()
+    fxt_uni_port._handler.adapter_agent.add_port.side_effect = AssertionError()
+    fxt_uni_port._update_adapter_agent()
+    fxt_uni_port.get_port.assert_called_once_with()
+    fxt_uni_port._handler.adapter_agent.add_port.assert_called_once_with(DEVICE_ID, fxt_uni_port.get_port.return_value)
+    fxt_uni_port.log.exception.assert_called_once()
+
+
+# Test UniPort.get_port() for expected operation with self._port = None
+def test_uni_port_get_port(fxt_uni_port):
+    with patch('voltha.adapters.adtran_onu.uni_port.Port', autospec=True) as mk_port:
+        fxt_uni_port._port = None
+        fxt_uni_port.port_id_name = MagicMock()
+        fxt_uni_port._admin_state = AdminState.ENABLED
+        fxt_uni_port._oper_status = OperStatus.ACTIVE
+        mk_port.ETHERNET_UNI = Port.ETHERNET_UNI
+        assert fxt_uni_port.get_port() == mk_port.return_value
+        mk_port.assert_called_once_with(port_no=UNI_PHYS_PORT_NUM, label=fxt_uni_port.port_id_name.return_value,
+                                        type=Port.ETHERNET_UNI, admin_state=AdminState.ENABLED,
+                                        oper_status=OperStatus.ACTIVE)
+
+
+# Test UniPort.port_id_name() getter property for expected operation
+def test_uni_port_port_id_name(fxt_uni_port):
+    fxt_uni_port._port_number = 10256
+    assert fxt_uni_port.port_id_name() == 'uni-10256'
+
+
+@pytest.mark.parametrize("log_port_num, multi_uni_naming, ofp_port_name", [(None, False, 'ADTN12345678'),
+                                                                           (LOGICAL_PORT_NUM, True, 'ADTN12345678-1')])
+# Test UniPort.add_logical_port() for expected operation with various parametrized variables
+def test_uni_port_add_logical_port(fxt_uni_port, log_port_num, multi_uni_naming, ofp_port_name):
+    with patch('voltha.adapters.adtran_onu.uni_port.ofp_port', autospec=True) as mk_ofp_port, \
+            patch('voltha.adapters.adtran_onu.uni_port.LogicalPort', autospec=True) as mk_LogicalPort:
+        fxt_uni_port._logical_port_number = log_port_num
+        fxt_uni_port._handler.adapter_agent.get_logical_port.return_value = None
+        device = fxt_uni_port._handler.adapter_agent.get_device.return_value
+        device.parent_port_no = 1
+        device.serial_number = 'ADTN12345678'
+        device.id = 0
+        fxt_uni_port._mac_bridge_port_num = 1
+        fxt_uni_port.port_id_name = MagicMock()
+        fxt_uni_port.port_id_name.return_value = 'uni-{}'.format(UNI_PHYS_PORT_NUM)
+        fxt_uni_port.add_logical_port(LOGICAL_PORT_NUM, multi_uni_naming)
+        assert fxt_uni_port._logical_port_number == LOGICAL_PORT_NUM
+        fxt_uni_port._handler.adapter_agent.get_device.assert_called_once_with(DEVICE_ID)
+        mk_ofp_port.assert_called_once_with(port_no=LOGICAL_PORT_NUM, hw_addr=(8, 0, 0, 1, 40, 0), config=0,
+                                            state=OFPPS_LIVE, curr=(OFPPF_10GB_FD | OFPPF_FIBER), name=ofp_port_name,
+                                            curr_speed=OFPPF_10GB_FD, advertised=(OFPPF_10GB_FD | OFPPF_FIBER),
+                                            max_speed=OFPPF_10GB_FD, peer=(OFPPF_10GB_FD | OFPPF_FIBER))
+        mk_LogicalPort.assert_called_once_with(id='uni-{}'.format(UNI_PHYS_PORT_NUM), ofp_port=mk_ofp_port.return_value,
+                                               device_id=device.id, device_port_no=UNI_PHYS_PORT_NUM)
+        fxt_uni_port._handler.adapter_agent.add_logical_port.assert_called_once_with(LOGICAL_DEVICE_ID,
+                                                                                     mk_LogicalPort.return_value)
+        if log_port_num is not None:
+            fxt_uni_port._handler.adapter_agent.get_logical_port.assert_called_once_with(LOGICAL_DEVICE_ID,
+                                                                                         'uni-{}'.format(UNI_PHYS_PORT_NUM))
+            fxt_uni_port._handler.adapter_agent.delete_logical_port.assert_called_once_with(LOGICAL_DEVICE_ID, None)
+
+
+# Test UniPort.add_logical_port() for exception in call to delete_logical_port() method
+def test_uni_port_add_logical_port_exception(fxt_uni_port):
+    with patch('voltha.adapters.adtran_onu.uni_port.ofp_port', autospec=True) as mk_ofp_port, \
+            patch('voltha.adapters.adtran_onu.uni_port.LogicalPort', autospec=True) as mk_LogicalPort:
+        fxt_uni_port._logical_port_number = LOGICAL_PORT_NUM
+        fxt_uni_port._handler.adapter_agent.get_logical_port.return_value = None
+        device = fxt_uni_port._handler.adapter_agent.get_device.return_value
+        device.parent_port_no = 1
+        device.serial_number = 'ADTN12345678'
+        device.id = 0
+        fxt_uni_port._mac_bridge_port_num = 1
+        fxt_uni_port.port_id_name = MagicMock()
+        fxt_uni_port.port_id_name.return_value = 'uni-{}'.format(UNI_PHYS_PORT_NUM)
+        # Creating an exception, but there is nothing to check for because the `except` statement only does a `pass`
+        fxt_uni_port._handler.adapter_agent.delete_logical_port.side_effect = AssertionError()
+        fxt_uni_port.add_logical_port(LOGICAL_PORT_NUM, False)
+        assert fxt_uni_port._logical_port_number == LOGICAL_PORT_NUM
+        fxt_uni_port._handler.adapter_agent.get_device.assert_called_once_with(DEVICE_ID)
+        mk_ofp_port.assert_called_once_with(port_no=LOGICAL_PORT_NUM, hw_addr=(8, 0, 0, 1, 40, 0), config=0,
+                                            state=OFPPS_LIVE, curr=(OFPPF_10GB_FD | OFPPF_FIBER), name='ADTN12345678',
+                                            curr_speed=OFPPF_10GB_FD, advertised=(OFPPF_10GB_FD | OFPPF_FIBER),
+                                            max_speed=OFPPF_10GB_FD, peer=(OFPPF_10GB_FD | OFPPF_FIBER))
+        mk_LogicalPort.assert_called_once_with(id='uni-{}'.format(UNI_PHYS_PORT_NUM), ofp_port=mk_ofp_port.return_value,
+                                               device_id=device.id, device_port_no=UNI_PHYS_PORT_NUM)
+        fxt_uni_port._handler.adapter_agent.add_logical_port.assert_called_once_with(LOGICAL_DEVICE_ID,
+                                                                                     mk_LogicalPort.return_value)
+        fxt_uni_port._handler.adapter_agent.get_logical_port.assert_called_once_with(LOGICAL_DEVICE_ID,
+                                                                                     'uni-{}'.format(UNI_PHYS_PORT_NUM))
+        fxt_uni_port._handler.adapter_agent.delete_logical_port.assert_called_once_with(LOGICAL_DEVICE_ID, None)