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)