Initial commit moving openolt adapter from voltha-go to the new repo.
This version works with ponsim rather than openolt, this is temporary.
It is currently being fixed to work with openolt.
Change-Id: I34a800c98f050140b367e2d474b7aa8b79f34b9a
Signed-off-by: William Kurkian <wkurkian@cisco.com>
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..c0ab82c
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.0.1-dev
diff --git a/python/Makefile b/python/Makefile
new file mode 100644
index 0000000..3ee4c38
--- /dev/null
+++ b/python/Makefile
@@ -0,0 +1,205 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+ifneq ($(VOLTHA_BUILD),docker)
+ifeq ($(VOLTHA_BASE)_set,_set)
+$(error To get started, please source the env.sh file)
+endif
+endif
+
+ifeq ($(TAG),)
+TAG := latest
+endif
+
+ifeq ($(TARGET_TAG),)
+TARGET_TAG := latest
+endif
+
+# If no DOCKER_HOST_IP is specified grab a v4 IP address associated with
+# the default gateway
+ifeq ($(DOCKER_HOST_IP),)
+DOCKER_HOST_IP := $(shell ifconfig $$(netstat -rn | grep -E '^(default|0.0.0.0)' | head -1 | awk '{print $$NF}') | grep inet | awk '{print $$2}' | sed -e 's/addr://g')
+endif
+
+ifneq ($(http_proxy)$(https_proxy),)
+# Include proxies from the environment
+DOCKER_PROXY_ARGS = \
+ --build-arg http_proxy=$(http_proxy) \
+ --build-arg https_proxy=$(https_proxy) \
+ --build-arg ftp_proxy=$(ftp_proxy) \
+ --build-arg no_proxy=$(no_proxy) \
+ --build-arg HTTP_PROXY=$(HTTP_PROXY) \
+ --build-arg HTTPS_PROXY=$(HTTPS_PROXY) \
+ --build-arg FTP_PROXY=$(FTP_PROXY) \
+ --build-arg NO_PROXY=$(NO_PROXY)
+endif
+
+DOCKER_BUILD_ARGS = \
+ --build-arg TAG=$(TAG) \
+ --build-arg REGISTRY=$(REGISTRY) \
+ --build-arg REPOSITORY=$(REPOSITORY) \
+ $(DOCKER_PROXY_ARGS) $(DOCKER_CACHE_ARG) \
+ --rm --force-rm \
+ $(DOCKER_BUILD_EXTRA_ARGS)
+
+VENVDIR := venv-$(shell uname -s | tr '[:upper:]' '[:lower:]')
+
+DOCKER_IMAGE_LIST = \
+ openolt-protos \
+ adapter-openolt \
+
+# The following list was scavanged from the compose / stack files as well as
+# from the Dockerfiles. If nothing else it highlights that VOLTHA is not
+# using consistent versions for some of the containers.
+
+FETCH_COMPOSE_IMAGE_LIST = \
+ wurstmeister/kafka:latest \
+ wurstmeister/zookeeper:latest
+
+# find k8s -type f | xargs grep image: | awk '{print $NF}' | sed -e 's/\"//g' | sed '/:.*$/!s/$/:latest/g' | sort -u | sed -e 's/^/ /g' -e 's/$/ \\/g'
+# Manually remove some image from this list as they don't reflect the new
+# naming conventions for the VOLTHA build
+FETCH_K8S_IMAGE_LIST = \
+ wurstmeister/kafka:1.0.0 \
+ zookeeper:3.4.11
+
+FETCH_IMAGE_LIST = $(shell echo $(FETCH_BUILD_IMAGE_LIST) $(FETCH_COMPOSE_IMAGE_LIST) $(FETCH_K8S_IMAGE_LIST) | tr ' ' '\n' | sort -u)
+
+.PHONY: $(DIRS) $(DIRS_CLEAN) $(DIRS_FLAKE8) flake8 base openolt ofagent kafka common start stop tag push pull
+
+# This should to be the first and default target in this Makefile
+help:
+ @echo "Usage: make [<target>]"
+ @echo "where available targets are:"
+ @echo
+ @echo "build : Build the Adapters protos and docker images.\n\
+ If this is the first time you are building, choose \"make build\" option."
+ @echo "clean : Remove files created by the build and tests"
+ @echo "distclean : Remove venv directory"
+ @echo "fetch : Pre-fetch artifacts for subsequent local builds"
+ @echo "help : Print this help"
+ @echo "protoc : Build a container with protoc installed"
+ @echo "protos : Compile all grpc/protobuf files"
+ @echo "rebuild-venv : Rebuild local Python virtualenv from scratch"
+ @echo "venv : Build local Python virtualenv if did not exist yet"
+ @echo "containers : Build all the docker containers"
+ @echo "base : Build the base docker container used by all other dockers"
+ @echo "tag : Tag a set of images"
+ @echo "push : Push the docker images to an external repository"
+ @echo "pull : Pull the docker images from a repository"
+ @echo
+
+## New directories can be added here
+#DIRS:=
+
+## If one directory depends on another directory that
+## dependency can be expressed here
+##
+## For example, if the Tibit directory depended on the eoam
+## directory being built first, then that can be expressed here.
+## driver/tibit: eoam
+
+# Parallel Build
+$(DIRS):
+ @echo " MK $@"
+ $(Q)$(MAKE) -C $@
+
+# Parallel Clean
+DIRS_CLEAN = $(addsuffix .clean,$(DIRS))
+$(DIRS_CLEAN):
+ @echo " CLEAN $(basename $@)"
+ $(Q)$(MAKE) -C $(basename $@) clean
+
+# Parallel Flake8
+DIRS_FLAKE8 = $(addsuffix .flake8,$(DIRS))
+$(DIRS_FLAKE8):
+ @echo " FLAKE8 $(basename $@)"
+ -$(Q)$(MAKE) -C $(basename $@) flake8
+
+build: openolt_protos containers
+
+containers: adapter_openolt
+
+adapter_openolt:
+ docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}voltha-adapter-openolt:${TAG} -f docker/Dockerfile.adapter_openolt .
+
+tag: $(patsubst %,%.tag,$(DOCKER_IMAGE_LIST))
+
+push: tag $(patsubst %,%.push,$(DOCKER_IMAGE_LIST))
+
+pull: $(patsubst %,%.pull,$(DOCKER_IMAGE_LIST))
+
+%.tag:
+ docker tag ${REGISTRY}${REPOSITORY}voltha-$(subst .tag,,$@):${TAG} ${TARGET_REGISTRY}${TARGET_REPOSITORY}voltha-$(subst .tag,,$@):${TARGET_TAG}
+
+%.push:
+ docker push ${TARGET_REGISTRY}${TARGET_REPOSITORY}voltha-$(subst .push,,$@):${TARGET_TAG}
+
+%.pull:
+ docker pull ${REGISTRY}${REPOSITORY}voltha-$(subst .pull,,$@):${TAG}
+
+openolt_protos:
+ifneq ($(VOLTHA_BUILD),docker)
+ make -C protos
+else
+ docker build $(DOCKER_BUILD_ARGS) -t ${REGISTRY}${REPOSITORY}voltha-openolt-protos:${TAG} -f docker/Dockerfile.openolt_protos .
+endif
+
+install-protoc:
+ make -C voltha/protos install-protoc
+
+clean:
+ find . -name '*.pyc' | xargs rm -f
+ rm -f ./protos/*_pb2.py
+ rm -f ./protos/*_pb2_grpc.py
+ rm -f ./protos/*.desc
+
+distclean: clean
+ rm -rf ${VENVDIR}
+
+fetch:
+ @bash -c ' \
+ for i in $(FETCH_IMAGE_LIST); do \
+ docker pull $$i; \
+ done'
+
+purge-venv:
+ rm -fr ${VENVDIR}
+
+rebuild-venv: purge-venv venv
+
+ifneq ($(VOLTHA_BUILD),docker)
+venv: ${VENVDIR}/.built
+else
+venv:
+endif
+
+${VENVDIR}/.built:
+ @ virtualenv ${VENVDIR}
+ @ . ${VENVDIR}/bin/activate && \
+ pip install --upgrade pip; \
+ if ! pip install -r requirements.txt; \
+ then \
+ echo "On MAC OS X, if the installation failed with an error \n'<openssl/opensslv.h>': file not found,"; \
+ echo "see the BUILD.md file for a workaround"; \
+ else \
+ uname -s > ${VENVDIR}/.built; \
+ fi
+
+
+flake8: $(DIRS_FLAKE8)
+
+# end file
diff --git a/python/__init__.py b/python/__init__.py
new file mode 100644
index 0000000..cfcdc97
--- /dev/null
+++ b/python/__init__.py
@@ -0,0 +1,15 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
\ No newline at end of file
diff --git a/python/adapters/BUILD_ENV.md b/python/adapters/BUILD_ENV.md
new file mode 100644
index 0000000..e445141
--- /dev/null
+++ b/python/adapters/BUILD_ENV.md
@@ -0,0 +1,53 @@
+
+Build Environment for Ubuntu
+============================
+
+1. The following commands were executed on a Ubuntu 16.04 (b4 bits) VM with following configuation (OpenStack Flavor: m1.medium):
+ - vCPUs - 2
+ - RAM - 4 GB
+ - HDD - 40 GB
+ - Network - Floating IP attached to Internal IP NIC. Internet access available
+
+
+2. login as root as docker command needs root access. When logged-in as root 'sudo' becomes optional in each command below
+
+git clone https://gerrit.opencord.org/voltha-go
+sudo apt-get install make
+sudo apt-get install python
+sudo apt-get install virtualenv
+sudo apt-get install gcc
+sudo apt-get install python-dev
+sudo apt-get install g++
+sudo apt-get install libpcap-dev
+sudo apt-get install apt-utils
+sudo apt-get install docker-compose
+
+
+3. Install docker CE for Ubuntu as mentioned in this link: https://docs.docker.com/install/linux/docker-ce/ubuntu/#set-up-the-repository. These are commands executed to install docker
+
+sudo apt-get update
+
+sudo apt-get install \
+ apt-transport-https \
+ ca-certificates \
+ curl \
+ software-properties-common
+
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
+
+sudo apt-key fingerprint 0EBFCD88
+
+sudo add-apt-repository \
+ "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
+ $(lsb_release -cs) \
+ stable"
+
+sudo apt-get update
+
+sudo apt-get install docker-ce
+
+
+4. Verify that Docker CE is installed correctly by running the hello-world image. The following command downloads a test image and runs it in a container. When the container runs, it prints an informational message and exits.
+
+sudo docker run hello-world
+
diff --git a/python/adapters/README.md b/python/adapters/README.md
new file mode 100644
index 0000000..72d7395
--- /dev/null
+++ b/python/adapters/README.md
@@ -0,0 +1,116 @@
+# How to Build and Run a Voltha Adapter
+
+The build and development environment of a Voltha Adapter is left to the developer's choice. The
+environment used below is the macOS.
+
+# Create fresh build setup
+When creating a fresh build setup various packages, applications and libraries are required to Build successfully.
+Please refer BUILD_ENV.md for details to create fresh setup on your choice of OS.
+This file will increment as new setups are created on difference OSes.
+
+### Build
+
+To build the adapters (so far Ponsim OLT and Ponsim ONU adapters) and dependent containers follow
+the commands below. The base directory is voltha-go.
+
+```
+cd python
+source env.sh
+VOLTHA_BUILD=docker make build
+```
+
+The above build command generates a few docker images. An example is below:
+
+```
+$ docker images
+REPOSITORY TAG IMAGE ID CREATED SIZE
+voltha-adapter-ponsim-onu latest 3638b16b5262 36 seconds ago 774MB
+voltha-adapter-ponsim-olt latest 9e98a3a8e1aa 58 seconds ago 775MB
+voltha-base latest 40ed93942a6a 23 minutes ago 771MB
+voltha-protos latest d458a391cc81 12 days ago 2.66MB
+voltha-protoc latest a67dda73f695 2 months ago 1.41GB
+```
+
+Now build the Voltha Core from the voltha-go directory
+```
+make rw_core
+```
+
+Now build the Voltha CLI image from the voltha-go/python directory (used below for provisioning)
+```
+make cli
+```
+
+This will create the following images:
+```
+REPOSITORY TAG IMAGE ID CREATED SIZE
+voltha-rw-core latest eab4b288adea 47 seconds ago 36.8MB
+voltha-cli latest a0a4b8f25373 About an hour ago 827MB
+```
+
+### Run the Adapters locally (development environment)
+
+We will use Ponsim as the example. Ponsim has two containers, one being the Ponsim OLT Adapter and
+the other one the Ponsim ONU Adapter. We will use the docker-compose command to run these containers locally
+as it is straight forward.
+
+#### Setup
+Since an adapter communicates with the Voltha Core via the Kafka bus and uses the Etcd KV store then we
+need to have these containers running as well. There is no dependency in the order in which they need to
+start as an adapter must be able to handle this scenario.
+
+First run the dependent containers from the voltha-go directory. In the commands below, replace the IP
+with the IP of the host.
+```
+DOCKER_HOST_IP=<Host IP> docker-compose -f compose/docker-compose-zk-kafka-test.yml up -d
+DOCKER_HOST_IP=<Host IP> docker-compose -f compose/docker-compose-etcd.yml up -d
+DOCKER_HOST_IP=<Host IP> docker-compose -f compose/rw_core.yml up -d
+DOCKER_HOST_IP=<Host IP> docker-compose -f compose/cli.yml up -d
+```
+#### Running the Ponsim Adapters
+
+Start the Ponsim OLT and ONU adapters
+```
+DOCKER_HOST_IP=<Host IP> docker-compose -f compose/adapters-ponsim.yml up -d
+```
+
+Start also the Ponsim OLT and ONU containers. We are using only PonsimV2. You may want to change the
+image names from the yml files below if you are pulling the Ponsim OLT and ONU images from somewhere else.
+
+```
+docker-compose -f compose/ponsim_olt.yml up -d
+docker-compose -f compose/ponsim_onu.yml up -d
+```
+
+#### Provisioning a device
+
+First get the IP address of the Ponsim OLT container by using the docker inspect command.
+
+Now, start the CLI. Password for 'voltha' user is 'admin'. Please see Dockerfile.cli for passwords
+
+```$xslt
+ssh -p 5022 voltha@localhost
+```
+
+Perform the provisioning
+
+```$xslt
+preprovision_olt -t ponsim_olt -H <IP of Ponsim OLT>:50060
+enable <deviceId> // Use the device ID returned in the previous command
+```
+
+At this point you can send flows to the devices using the test option in the CLI.
+```$xslt
+test
+install_eapol_flow <logical_device_id>
+install_dhcp_flows <logical_device_id>
+install_all_controller_bound_flows <logical_device_id>
+install_all_sample_flows <logical_device_id>
+```
+
+You can also see the metrics the Ponsim OLT and ONU adapters are pushing onto the kafka bus.
+
+```$xslt
+kafkacat -b <host IP>:9092 -t voltha.kpis -p 0 -o beginning
+```
+
diff --git a/python/adapters/__init__.py b/python/adapters/__init__.py
new file mode 100644
index 0000000..58aca1e
--- /dev/null
+++ b/python/adapters/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/common/__init__.py b/python/adapters/common/__init__.py
new file mode 100644
index 0000000..58aca1e
--- /dev/null
+++ b/python/adapters/common/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/common/frameio/__init__.py b/python/adapters/common/frameio/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/common/frameio/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/common/frameio/frameio.py b/python/adapters/common/frameio/frameio.py
new file mode 100644
index 0000000..0657257
--- /dev/null
+++ b/python/adapters/common/frameio/frameio.py
@@ -0,0 +1,437 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+A module that can send and receive raw ethernet frames on a set of interfaces
+and it can manage a set of vlan interfaces on top of existing
+interfaces. Due to reliance on raw sockets, this module requires
+root access. Also, raw sockets are hard to deal with in Twisted (not
+directly supported) we need to run the receiver select loop on a dedicated
+thread.
+"""
+
+import os
+import socket
+import struct
+import uuid
+from pcapy import BPFProgram
+from threading import Thread, Condition
+
+import fcntl
+
+import select
+import structlog
+import sys
+
+from scapy.data import ETH_P_ALL
+from twisted.internet import reactor
+from zope.interface import implementer
+
+from python.common.utils.registry import IComponent
+
+if sys.platform.startswith('linux'):
+ from third_party.oftest import afpacket, netutils
+elif sys.platform == 'darwin':
+ from scapy.arch import pcapdnet, BIOCIMMEDIATE, dnet
+
+log = structlog.get_logger()
+
+
+def hexify(buffer):
+ """
+ Return a hexadecimal string encoding of input buffer
+ """
+ return ''.join('%02x' % ord(c) for c in buffer)
+
+
+class _SelectWakerDescriptor(object):
+ """
+ A descriptor that can be mixed into a select loop to wake it up.
+ """
+ def __init__(self):
+ self.pipe_read, self.pipe_write = os.pipe()
+ fcntl.fcntl(self.pipe_write, fcntl.F_SETFL, os.O_NONBLOCK)
+
+ def __del__(self):
+ os.close(self.pipe_read)
+ os.close(self.pipe_write)
+
+ def fileno(self):
+ return self.pipe_read
+
+ def wait(self):
+ os.read(self.pipe_read, 1)
+
+ def notify(self):
+ """Trigger a select loop"""
+ os.write(self.pipe_write, '\x00')
+
+
+class BpfProgramFilter(object):
+ """
+ Convenience packet filter based on the well-tried Berkeley Packet Filter,
+ used by many well known open source tools such as pcap and tcpdump.
+ """
+ def __init__(self, program_string):
+ """
+ Create a filter using the BPF command syntax. To learn more,
+ consult 'man pcap-filter'.
+ :param program_string: The textual definition of the filter. Examples:
+ 'vlan 1000'
+ 'vlan 1000 and ip src host 10.10.10.10'
+ """
+ self.bpf = BPFProgram(program_string)
+
+ def __call__(self, frame):
+ """
+ Return 1 if frame passes filter.
+ :param frame: Raw frame provided as Python string
+ :return: 1 if frame satisfies filter, 0 otherwise.
+ """
+ return self.bpf.filter(frame)
+
+
+class FrameIOPort(object):
+ """
+ Represents a network interface which we can send/receive raw
+ Ethernet frames.
+ """
+
+ RCV_SIZE_DEFAULT = 4096
+ ETH_P_ALL = 0x03
+ RCV_TIMEOUT = 10000
+ MIN_PKT_SIZE = 60
+
+ def __init__(self, iface_name):
+ self.iface_name = iface_name
+ self.proxies = []
+ self.socket = self.open_socket(self.iface_name)
+ log.debug('socket-opened', fn=self.fileno(), iface=iface_name)
+ self.received = 0
+ self.discarded = 0
+
+ def add_proxy(self, proxy):
+ self.proxies.append(proxy)
+
+ def del_proxy(self, proxy):
+ self.proxies = [p for p in self.proxies if p.name != proxy.name]
+
+ def open_socket(self, iface_name):
+ raise NotImplementedError('to be implemented by derived class')
+
+ def rcv_frame(self):
+ raise NotImplementedError('to be implemented by derived class')
+
+ def __del__(self):
+ if self.socket:
+ self.socket.close()
+ self.socket = None
+ log.debug('socket-closed', iface=self.iface_name)
+
+ def fileno(self):
+ return self.socket.fileno()
+
+ def _dispatch(self, proxy, frame):
+ log.debug('calling-publisher', proxy=proxy.name, frame=hexify(frame))
+ try:
+ proxy.callback(proxy, frame)
+ except Exception as e:
+ log.exception('callback-error',
+ explanation='Callback failed while processing frame',
+ e=e)
+
+ def recv(self):
+ """Called on the select thread when a packet arrives"""
+ try:
+ frame = self.rcv_frame()
+ except RuntimeError as e:
+ # we observed this happens sometimes right after the socket was
+ # attached to a newly created veth interface. So we log it, but
+ # allow to continue.
+ log.warn('afpacket-recv-error', code=-1)
+ return
+
+ log.debug('frame-received', iface=self.iface_name, len=len(frame),
+ hex=hexify(frame))
+ self.received +=1
+ dispatched = False
+ for proxy in self.proxies:
+ if proxy.filter is None or proxy.filter(frame):
+ log.debug('frame-dispatched')
+ dispatched = True
+ reactor.callFromThread(self._dispatch, proxy, frame)
+
+ if not dispatched:
+ self.discarded += 1
+ log.debug('frame-discarded')
+
+ def send(self, frame):
+ log.debug('sending', len=len(frame), iface=self.iface_name)
+ sent_bytes = self.send_frame(frame)
+ if sent_bytes != len(frame):
+ log.error('send-error', iface=self.iface_name,
+ wanted_to_send=len(frame), actually_sent=sent_bytes)
+ return sent_bytes
+
+ def send_frame(self, frame):
+ try:
+ return self.socket.send(frame)
+ except socket.error, err:
+ if err[0] == os.errno.EINVAL:
+ if len(frame) < self.MIN_PKT_SIZE:
+ padding = '\x00' * (self.MIN_PKT_SIZE - len(frame))
+ frame = frame + padding
+ return self.socket.send(frame)
+ else:
+ raise
+
+ def up(self):
+ if sys.platform.startswith('darwin'):
+ pass
+ else:
+ os.system('ip link set {} up'.format(self.iface_name))
+ return self
+
+ def down(self):
+ if sys.platform.startswith('darwin'):
+ pass
+ else:
+ os.system('ip link set {} down'.format(self.iface_name))
+ return self
+
+ def statistics(self):
+ return self.received, self.discarded
+
+
+class LinuxFrameIOPort(FrameIOPort):
+
+ def open_socket(self, iface_name):
+ s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, 0)
+ afpacket.enable_auxdata(s)
+ s.bind((self.iface_name, self.ETH_P_ALL))
+ netutils.set_promisc(s, iface_name)
+ s.settimeout(self.RCV_TIMEOUT)
+ return s
+
+ def rcv_frame(self):
+ return afpacket.recv(self.socket, self.RCV_SIZE_DEFAULT)
+
+
+class DarwinFrameIOPort(FrameIOPort):
+
+ def open_socket(self, iface_name):
+ sin = pcapdnet.open_pcap(iface_name, 1600, 1, 100)
+ try:
+ fcntl.ioctl(sin.fileno(), BIOCIMMEDIATE, struct.pack("I",1))
+ except:
+ pass
+
+ # need a different kind of socket for sending out
+ self.sout = dnet.eth(iface_name)
+
+ return sin
+
+ def send_frame(self, frame):
+ return self.sout.send(frame)
+
+ def rcv_frame(self):
+ pkt = self.socket.next()
+ if pkt is not None:
+ ts, pkt = pkt
+ return pkt
+
+
+if sys.platform == 'darwin':
+ _FrameIOPort = DarwinFrameIOPort
+elif sys.platform.startswith('linux'):
+ _FrameIOPort = LinuxFrameIOPort
+else:
+ raise Exception('Unsupported platform {}'.format(sys.platform))
+ sys.exit(1)
+
+
+class FrameIOPortProxy(object):
+ """Makes FrameIOPort sharable between multiple users"""
+
+ def __init__(self, frame_io_port, callback, filter=None, name=None):
+ self.frame_io_port = frame_io_port
+ self.callback = callback
+ self.filter = filter
+ self.name = uuid.uuid4().hex[:12] if name is None else name
+
+ @property
+ def iface_name(self):
+ return self.frame_io_port.iface_name
+
+ def get_iface_name(self):
+ return self.frame_io_port.iface_name
+
+ def send(self, frame):
+ return self.frame_io_port.send(frame)
+
+ def up(self):
+ self.frame_io_port.up()
+ return self
+
+ def down(self):
+ self.frame_io_port.down()
+ return self
+
+
+@implementer(IComponent)
+class FrameIOManager(Thread):
+ """
+ Packet/Frame IO manager that can be used to send/receive raw frames
+ on a set of network interfaces.
+ """
+ def __init__(self):
+ super(FrameIOManager, self).__init__()
+
+ self.ports = {} # iface_name -> ActiveFrameReceiver
+ self.queue = {} # iface_name -> TODO
+
+ self.cvar = Condition()
+ self.waker = _SelectWakerDescriptor()
+ self.stopped = False
+ self.ports_changed = False
+
+ # ~~~~~~~~~~~ exposed methods callable from main thread ~~~~~~~~~~~~~~~~~~~
+
+ def start(self):
+ """
+ Start the IO manager and its select loop thread
+ """
+ log.debug('starting')
+ super(FrameIOManager, self).start()
+ log.info('started')
+ return self
+
+ def stop(self):
+ """
+ Stop the IO manager and its thread with the select loop
+ """
+ log.debug('stopping')
+ self.stopped = True
+ self.waker.notify()
+ self.join()
+ del self.ports
+ log.info('stopped')
+
+ def list_interfaces(self):
+ """
+ Return list of interfaces listened on
+ :return: List of FrameIOPort objects
+ """
+ return self.ports
+
+ def open_port(self, iface_name, callback, filter=None, name=None):
+ """
+ Add a new interface and start receiving on it.
+ :param iface_name: Name of the interface. Must be an existing Unix
+ interface (eth0, en0, etc.)
+ :param callback: Called on each received frame;
+ signature: def callback(port, frame) where port is the FrameIOPort
+ instance at which the frame was received, frame is the actual frame
+ received (as binay string)
+ :param filter: An optional filter (predicate), with signature:
+ def filter(frame). If provided, only frames for which filter evaluates
+ to True will be forwarded to callback.
+ :return: FrmaeIOPortProxy instance.
+ """
+
+ port = self.ports.get(iface_name)
+ if port is None:
+ port = _FrameIOPort(iface_name)
+ self.ports[iface_name] = port
+ self.ports_changed = True
+ self.waker.notify()
+
+ proxy = FrameIOPortProxy(port, callback, filter, name)
+ port.add_proxy(proxy)
+
+ return proxy
+
+ def close_port(self, proxy):
+ """
+ Remove the proxy. If this is the last proxy on an interface, stop and
+ remove the named interface as well
+ :param proxy: FrameIOPortProxy reference
+ :return: None
+ """
+ assert isinstance(proxy, FrameIOPortProxy)
+ iface_name = proxy.get_iface_name()
+ assert iface_name in self.ports, "iface_name {} unknown".format(iface_name)
+ port = self.ports[iface_name]
+ port.del_proxy(proxy)
+
+ if not port.proxies:
+ del self.ports[iface_name]
+ # need to exit select loop to reconstruct select fd lists
+ self.ports_changed = True
+ self.waker.notify()
+
+ def send(self, iface_name, frame):
+ """
+ Send frame on given interface
+ :param iface_name: Name of previously registered interface
+ :param frame: frame as string
+ :return: number of bytes sent
+ """
+ return self.ports[iface_name].send(frame)
+
+ # ~~~~~~~~~~~~~ Thread methods (running on non-main thread ~~~~~~~~~~~~~~~~
+
+ def run(self):
+ """
+ Called on the alien thread, this is the core multi-port receive loop
+ """
+
+ log.debug('select-loop-started')
+
+ # outer loop constructs sockets list for select
+ while not self.stopped:
+ sockets = [self.waker] + self.ports.values()
+ self.ports_changed = False
+ empty = []
+ # inner select loop
+
+ while not self.stopped:
+ try:
+ _in, _out, _err = select.select(sockets, empty, empty, 1)
+ except Exception as e:
+ log.exception('frame-io-select-error', e=e)
+ break
+ with self.cvar:
+ for port in _in:
+ if port is self.waker:
+ self.waker.wait()
+ continue
+ else:
+ port.recv()
+ self.cvar.notify_all()
+ if self.ports_changed:
+ break # break inner loop so we reconstruct sockets list
+
+ log.debug('select-loop-exited')
+
+ def del_interface(self, iface_name):
+ """
+ Delete interface for stopping
+ """
+
+ log.info('Delete interface')
+ del self.ports[iface_name]
+ log.info('Interface(port) is deleted')
diff --git a/python/adapters/common/frameio/third_party/__init__.py b/python/adapters/common/frameio/third_party/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/common/frameio/third_party/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/common/frameio/third_party/oftest/LICENSE b/python/adapters/common/frameio/third_party/oftest/LICENSE
new file mode 100644
index 0000000..3216042
--- /dev/null
+++ b/python/adapters/common/frameio/third_party/oftest/LICENSE
@@ -0,0 +1,36 @@
+OpenFlow Test Framework
+
+Copyright (c) 2010 The Board of Trustees of The Leland Stanford
+Junior University
+
+Except where otherwise noted, this software is distributed under
+the OpenFlow Software License. See
+http://www.openflowswitch.org/wp/legal/ for current details.
+
+We are making the OpenFlow specification and associated documentation
+(Software) available for public use and benefit with the expectation
+that others will use, modify and enhance the Software and contribute
+those enhancements back to the community. However, since we would like
+to make the Software available for broadest use, with as few
+restrictions as possible permission is hereby granted, free of charge,
+to any person obtaining a copy of this Software to deal in the
+Software under the copyrights without restriction, including without
+limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED -Y´AS IS¡, WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+The name and trademarks of copyright holder(s) may NOT be used in
+advertising or publicity pertaining to the Software or any derivatives
+without specific, written prior permission.
diff --git a/python/adapters/common/frameio/third_party/oftest/README.md b/python/adapters/common/frameio/third_party/oftest/README.md
new file mode 100644
index 0000000..f0cb649
--- /dev/null
+++ b/python/adapters/common/frameio/third_party/oftest/README.md
@@ -0,0 +1,6 @@
+Files in this directory are derived from the respective files
+in oftest (http://github.com/floodlight/oftest).
+
+For the licensing terms of these files, see LICENSE in this dir.
+
+
diff --git a/python/adapters/common/frameio/third_party/oftest/__init__.py b/python/adapters/common/frameio/third_party/oftest/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/common/frameio/third_party/oftest/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/common/frameio/third_party/oftest/afpacket.py b/python/adapters/common/frameio/third_party/oftest/afpacket.py
new file mode 100644
index 0000000..9ae8075
--- /dev/null
+++ b/python/adapters/common/frameio/third_party/oftest/afpacket.py
@@ -0,0 +1,124 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+AF_PACKET receive support
+
+When VLAN offload is enabled on the NIC Linux will not deliver the VLAN tag
+in the data returned by recv. Instead, it delivers the VLAN TCI in a control
+message. Python 2.x doesn't have built-in support for recvmsg, so we have to
+use ctypes to call it. The recv function exported by this module reconstructs
+the VLAN tag if it was offloaded.
+"""
+
+import struct
+from ctypes import *
+
+ETH_P_8021Q = 0x8100
+SOL_PACKET = 263
+PACKET_AUXDATA = 8
+TP_STATUS_VLAN_VALID = 1 << 4
+
+class struct_iovec(Structure):
+ _fields_ = [
+ ("iov_base", c_void_p),
+ ("iov_len", c_size_t),
+ ]
+
+class struct_msghdr(Structure):
+ _fields_ = [
+ ("msg_name", c_void_p),
+ ("msg_namelen", c_uint32),
+ ("msg_iov", POINTER(struct_iovec)),
+ ("msg_iovlen", c_size_t),
+ ("msg_control", c_void_p),
+ ("msg_controllen", c_size_t),
+ ("msg_flags", c_int),
+ ]
+
+class struct_cmsghdr(Structure):
+ _fields_ = [
+ ("cmsg_len", c_size_t),
+ ("cmsg_level", c_int),
+ ("cmsg_type", c_int),
+ ]
+
+class struct_tpacket_auxdata(Structure):
+ _fields_ = [
+ ("tp_status", c_uint),
+ ("tp_len", c_uint),
+ ("tp_snaplen", c_uint),
+ ("tp_mac", c_ushort),
+ ("tp_net", c_ushort),
+ ("tp_vlan_tci", c_ushort),
+ ("tp_padding", c_ushort),
+ ]
+
+libc = CDLL("libc.so.6")
+recvmsg = libc.recvmsg
+recvmsg.argtypes = [c_int, POINTER(struct_msghdr), c_int]
+recvmsg.retype = c_int
+
+def enable_auxdata(sk):
+ """
+ Ask the kernel to return the VLAN tag in a control message
+
+ Must be called on the socket before afpacket.recv.
+ """
+ sk.setsockopt(SOL_PACKET, PACKET_AUXDATA, 1)
+
+def recv(sk, bufsize):
+ """
+ Receive a packet from an AF_PACKET socket
+ @sk Socket
+ @bufsize Maximum packet size
+ """
+ buf = create_string_buffer(bufsize)
+
+ ctrl_bufsize = sizeof(struct_cmsghdr) + sizeof(struct_tpacket_auxdata) + sizeof(c_size_t)
+ ctrl_buf = create_string_buffer(ctrl_bufsize)
+
+ iov = struct_iovec()
+ iov.iov_base = cast(buf, c_void_p)
+ iov.iov_len = bufsize
+
+ msghdr = struct_msghdr()
+ msghdr.msg_name = None
+ msghdr.msg_namelen = 0
+ msghdr.msg_iov = pointer(iov)
+ msghdr.msg_iovlen = 1
+ msghdr.msg_control = cast(ctrl_buf, c_void_p)
+ msghdr.msg_controllen = ctrl_bufsize
+ msghdr.msg_flags = 0
+
+ rv = recvmsg(sk.fileno(), byref(msghdr), 0)
+ if rv < 0:
+ raise RuntimeError("recvmsg failed: rv=%d", rv)
+
+ # The kernel only delivers control messages we ask for. We
+ # only enabled PACKET_AUXDATA, so we can assume it's the
+ # only control message.
+ assert msghdr.msg_controllen >= sizeof(struct_cmsghdr)
+
+ cmsghdr = struct_cmsghdr.from_buffer(ctrl_buf) # pylint: disable=E1101
+ assert cmsghdr.cmsg_level == SOL_PACKET
+ assert cmsghdr.cmsg_type == PACKET_AUXDATA
+
+ auxdata = struct_tpacket_auxdata.from_buffer(ctrl_buf, sizeof(struct_cmsghdr)) # pylint: disable=E1101
+
+ if auxdata.tp_vlan_tci != 0 or auxdata.tp_status & TP_STATUS_VLAN_VALID:
+ # Insert VLAN tag
+ tag = struct.pack("!HH", ETH_P_8021Q, auxdata.tp_vlan_tci)
+ return buf.raw[:12] + tag + buf.raw[12:rv]
+ else:
+ return buf.raw[:rv]
diff --git a/python/adapters/common/frameio/third_party/oftest/netutils.py b/python/adapters/common/frameio/third_party/oftest/netutils.py
new file mode 100644
index 0000000..092d490
--- /dev/null
+++ b/python/adapters/common/frameio/third_party/oftest/netutils.py
@@ -0,0 +1,73 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Network utilities for the OpenFlow test framework
+"""
+
+###########################################################################
+## ##
+## Promiscuous mode enable/disable ##
+## ##
+## Based on code from Scapy by Phillippe Biondi ##
+## ##
+## ##
+## This program is free software; you can redistribute it and/or modify it ##
+## under the terms of the GNU General Public License as published by the ##
+## Free Software Foundation; either version 2, or (at your option) any ##
+## later version. ##
+## ##
+## This program is distributed in the hope that it will be useful, but ##
+## WITHOUT ANY WARRANTY; without even the implied warranty of ##
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ##
+## General Public License for more details. ##
+## ##
+#############################################################################
+
+import socket
+from fcntl import ioctl
+import struct
+
+# From net/if_arp.h
+ARPHDR_ETHER = 1
+ARPHDR_LOOPBACK = 772
+
+# From bits/ioctls.h
+SIOCGIFHWADDR = 0x8927 # Get hardware address
+SIOCGIFINDEX = 0x8933 # name -> if_index mapping
+
+# From netpacket/packet.h
+PACKET_ADD_MEMBERSHIP = 1
+PACKET_DROP_MEMBERSHIP = 2
+PACKET_MR_PROMISC = 1
+
+# From bits/socket.h
+SOL_PACKET = 263
+
+def get_if(iff,cmd):
+ s=socket.socket()
+ ifreq = ioctl(s, cmd, struct.pack("16s16x",iff))
+ s.close()
+ return ifreq
+
+def get_if_index(iff):
+ return int(struct.unpack("I",get_if(iff, SIOCGIFINDEX)[16:20])[0])
+
+def set_promisc(s,iff,val=1):
+ mreq = struct.pack("IHH8s", get_if_index(iff), PACKET_MR_PROMISC, 0, "")
+ if val:
+ cmd = PACKET_ADD_MEMBERSHIP
+ else:
+ cmd = PACKET_DROP_MEMBERSHIP
+ s.setsockopt(SOL_PACKET, cmd, mreq)
+
diff --git a/python/adapters/common/kvstore/__init__.py b/python/adapters/common/kvstore/__init__.py
new file mode 100644
index 0000000..4a82628
--- /dev/null
+++ b/python/adapters/common/kvstore/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/common/kvstore/consul_client.py b/python/adapters/common/kvstore/consul_client.py
new file mode 100644
index 0000000..789e797
--- /dev/null
+++ b/python/adapters/common/kvstore/consul_client.py
@@ -0,0 +1,304 @@
+# Copyright 2018-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from kv_client import DEFAULT_TIMEOUT, Event, KVClient, KVPair, RETRY_BACKOFF
+from python.common.utils.asleep import asleep
+from python.common.utils.deferred_utils import DeferredWithTimeout, TimeOutError
+from consul import ConsulException
+from consul.twisted import Consul
+from structlog import get_logger
+from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
+
+log = get_logger()
+
+class ConsulClient(KVClient):
+
+ def __init__(self, kv_host, kv_port):
+ KVClient.__init__(self, kv_host, kv_port)
+ self.session_id = None
+ self.client = Consul(kv_host, kv_port)
+
+ def watch(self, key, key_change_callback, timeout=DEFAULT_TIMEOUT):
+ self._retriggering_watch(key, key_change_callback, timeout)
+
+ @inlineCallbacks
+ def _retriggering_watch(self, key, key_change_callback, timeout):
+ self.key_watches[key] = ConsulWatch(self.client, key, key_change_callback, timeout)
+ yield self.key_watches[key].start()
+
+ def close_watch(self, key, timeout=DEFAULT_TIMEOUT):
+ if key in self.key_watches:
+ self.key_watches[key].stop()
+
+ @inlineCallbacks
+ def _op_with_retry(self, operation, key, value, timeout, *args, **kw):
+ log.debug('kv-op', operation=operation, key=key, timeout=timeout, args=args, kw=kw)
+ err = None
+ result = None
+ while True:
+ try:
+ if operation == 'GET':
+ result = yield self._get(key, **kw)
+ elif operation == 'LIST':
+ result, err = yield self._list(key)
+ elif operation == 'PUT':
+ # Put returns a boolean response
+ result = yield self.client.kv.put(key, value)
+ if not result:
+ err = 'put-failed'
+ elif operation == 'DELETE':
+ # Delete returns a boolean response
+ result = yield self.client.kv.delete(key)
+ if not result:
+ err = 'delete-failed'
+ elif operation == 'RESERVE':
+ result, err = yield self._reserve(key, value, **kw)
+ elif operation == 'RENEW':
+ result, err = yield self._renew_reservation(key)
+ elif operation == 'RELEASE':
+ result, err = yield self._release_reservation(key)
+ elif operation == 'RELEASE-ALL':
+ err = yield self._release_all_reservations()
+ self._clear_backoff()
+ break
+ except ConsulException as ex:
+ if 'ConnectionRefusedError' in ex.message:
+ log.exception('comms-exception', ex=ex)
+ yield self._backoff('consul-not-up')
+ else:
+ log.error('consul-specific-exception', ex=ex)
+ err = ex
+ except Exception as ex:
+ log.error('consul-exception', ex=ex)
+ err = ex
+
+ if timeout > 0 and self.retry_time > timeout:
+ err = 'operation-timed-out'
+ if err is not None:
+ self._clear_backoff()
+ break
+
+ returnValue((result,err))
+
+ @inlineCallbacks
+ def _get(self, key, **kw):
+ kvp = None
+ index, rec = yield self.client.kv.get(key, **kw)
+ if rec is not None:
+ kvp = KVPair(rec['Key'], rec['Value'], index)
+ returnValue(kvp)
+
+ @inlineCallbacks
+ def _list(self, key):
+ err = None
+ list = []
+ index, recs = yield self.client.kv.get(key, recurse=True)
+ for rec in recs:
+ list.append(KVPair(rec['Key'], rec['Value'], rec['ModifyIndex']))
+ returnValue((list, err))
+
+ @inlineCallbacks
+ def _reserve(self, key, value, **kw):
+ for name, val in kw.items():
+ if name == 'ttl':
+ ttl = val
+ break
+ reserved = False
+ err = 'reservation-failed'
+ owner = None
+
+ # Create a session
+ self.session_id = yield self.client.session.create(behavior='delete',
+ ttl=ttl) # lock_delay=1)
+ log.debug('create-session', id=self.session_id)
+ # Try to acquire the key
+ result = yield self.client.kv.put(key, value, acquire=self.session_id)
+ log.debug('key-acquire', key=key, value=value, sess=self.session_id, result=result)
+
+ # Check if reservation succeeded
+ index, record = yield self.client.kv.get(key)
+ if record is not None and 'Value' in record:
+ owner = record['Value']
+ log.debug('get-key', session=record['Session'], owner=owner)
+ if record['Session'] == self.session_id and owner == value:
+ reserved = True
+ log.debug('key-reserved', key=key, value=value, ttl=ttl)
+ # Add key to reservation list
+ self.key_reservations[key] = self.session_id
+ else:
+ log.debug('reservation-held-by-another', owner=owner)
+
+ if reserved:
+ err = None
+ returnValue((owner, err))
+
+ @inlineCallbacks
+ def _renew_reservation(self, key):
+ result = None
+ err = None
+ if key not in self.key_reservations:
+ err = 'key-not-reserved'
+ else:
+ session_id = self.key_reservations[key]
+ # A successfully renewed session returns an object with fields:
+ # Node, CreateIndex, Name, ModifyIndex, ID, Behavior, TTL,
+ # LockDelay, and Checks
+ result = yield self.client.session.renew(session_id=session_id)
+ log.debug('session-renew', result=result)
+ if result is None:
+ err = 'session-renewal-failed'
+ returnValue((result, err))
+
+ @inlineCallbacks
+ def _release_reservation(self, key):
+ err = None
+ if key not in self.key_reservations:
+ err = 'key-not-reserved'
+ else:
+ session_id = self.key_reservations[key]
+ # A successfully destroyed session returns a boolean result
+ success = yield self.client.session.destroy(session_id)
+ log.debug('session-destroy', result=success)
+ if not success:
+ err = 'session-destroy-failed'
+ self.session_id = None
+ self.key_reservations.pop(key)
+ returnValue((success, err))
+
+ @inlineCallbacks
+ def _release_all_reservations(self):
+ err = None
+ keys_to_delete = []
+ for key in self.key_reservations:
+ session_id = self.key_reservations[key]
+ # A successfully destroyed session returns a boolean result
+ success = yield self.client.session.destroy(session_id)
+ if not success:
+ err = 'session-destroy-failed'
+ log.debug('session-destroy', id=session_id, result=success)
+ self.session_id = None
+ keys_to_delete.append(key)
+ for key in keys_to_delete:
+ self.key_reservations.pop(key)
+ returnValue(err)
+
+
+class ConsulWatch():
+
+ def __init__(self, consul, key, callback, timeout):
+ self.client = consul
+ self.key = key
+ self.index = None
+ self.callback = callback
+ self.timeout = timeout
+ self.period = 60
+ self.running = True
+ self.retries = 0
+ self.retry_time = 0
+
+ @inlineCallbacks
+ def start(self):
+ self.running = True
+ index, rec = yield self._get_with_retry(self.key, None,
+ timeout=self.timeout)
+ self.index = str(index)
+
+ @inlineCallbacks
+ def _get(key, deferred):
+ try:
+ index, rec = yield self._get_with_retry(key, None,
+ timeout=self.timeout,
+ index=self.index)
+ self.index = str(index)
+ if not deferred.called:
+ log.debug('got-result-cancelling-deferred')
+ deferred.callback((self.index, rec))
+ except Exception as e:
+ log.exception('got-exception', e=e)
+
+ while self.running:
+ try:
+ rcvd = DeferredWithTimeout(timeout=self.period)
+ _get(self.key, rcvd)
+ try:
+ # Update index for next watch iteration
+ index, rec = yield rcvd
+ log.debug('event-received', index=index, rec=rec)
+ # Notify client of key change event
+ if rec is None:
+ # Key has been deleted
+ self._send_event(Event(Event.DELETE, self.key, None))
+ else:
+ self._send_event(Event(Event.PUT, rec['Key'], rec['Value']))
+ except TimeOutError as e:
+ log.debug('no-events-over-watch-period', key=self.key)
+ except Exception as e:
+ log.exception('exception', e=e)
+ except Exception as e:
+ log.exception('exception', e=e)
+
+ log.debug('close-watch', key=self.key)
+
+ def stop(self):
+ self.running = False
+ self.callback = None
+
+ @inlineCallbacks
+ def _get_with_retry(self, key, value, timeout, *args, **kw):
+ log.debug('watch-period', key=key, period=self.period, timeout=timeout, args=args, kw=kw)
+ err = None
+ result = None
+ while True:
+ try:
+ result = yield self.client.kv.get(key, **kw)
+ self._clear_backoff()
+ break
+ except ConsulException as ex:
+ err = ex
+ if 'ConnectionRefusedError' in ex.message:
+ self._send_event(Event(Event.CONNECTION_DOWN, self.key, None))
+ log.exception('comms-exception', ex=ex)
+ yield self._backoff('consul-not-up')
+ else:
+ log.error('consul-specific-exception', ex=ex)
+ except Exception as ex:
+ err = ex
+ log.error('consul-exception', ex=ex)
+
+ if timeout > 0 and self.retry_time > timeout:
+ err = 'operation-timed-out'
+ if err is not None:
+ self._clear_backoff()
+ break
+
+ returnValue(result)
+
+ def _send_event(self, event):
+ if self.callback is not None:
+ self.callback(event)
+
+ def _backoff(self, msg):
+ wait_time = RETRY_BACKOFF[min(self.retries, len(RETRY_BACKOFF) - 1)]
+ self.retry_time += wait_time
+ self.retries += 1
+ log.error(msg, next_retry_in_secs=wait_time,
+ total_delay_in_secs = self.retry_time,
+ retries=self.retries)
+ return asleep(wait_time)
+
+ def _clear_backoff(self):
+ if self.retries:
+ log.debug('reconnected-to-kv', after_retries=self.retries)
+ self.retries = 0
+ self.retry_time = 0
diff --git a/python/adapters/common/kvstore/etcd_client.py b/python/adapters/common/kvstore/etcd_client.py
new file mode 100644
index 0000000..e1850e7
--- /dev/null
+++ b/python/adapters/common/kvstore/etcd_client.py
@@ -0,0 +1,240 @@
+# Copyright 2018-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+#
+# Most of the txaioetcd methods provide a timeout parameter. This parameter
+# is likely intended to limit the amount of time spent by any one method
+# waiting for a response from the etcd server. However, if the server is
+# down, the method immediately throws a ConnectionRefusedError exception;
+# it does not perform any retries. The timeout parameter provided by the
+# methods in EtcdClient cover this contingency.
+#
+################################################################################
+
+from kv_client import DEFAULT_TIMEOUT, Event, KVClient, KVPair
+from structlog import get_logger
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
+from twisted.internet.error import ConnectionRefusedError
+from txaioetcd import Client, CompVersion, Failed, KeySet, OpGet, OpSet, Transaction
+
+log = get_logger()
+
+class EtcdClient(KVClient):
+
+ def __init__(self, kv_host, kv_port):
+ KVClient.__init__(self, kv_host, kv_port)
+ self.url = u'http://' + kv_host + u':' + str(kv_port)
+ self.client = Client(reactor, self.url)
+
+ @inlineCallbacks
+ def watch(self, key, key_change_callback, timeout=DEFAULT_TIMEOUT):
+ self.key_watches[key] = key_change_callback
+ result = yield self._op_with_retry('WATCH', key, None, timeout, callback=self.key_changed)
+ returnValue(result)
+
+ def key_changed(self, kv):
+ key = kv.key
+ value = kv.value
+ log.debug('key-changed', key=key, value=value)
+ # Notify client of key change event
+ if value is not None:
+ evt = Event(Event.PUT, key, value)
+ else:
+ evt = Event(Event.DELETE, key, None)
+ if key in self.key_watches:
+ self.key_watches[key](evt)
+
+ def close_watch(self, key, timeout=DEFAULT_TIMEOUT):
+ log.debug('close-watch', key=key)
+ if key in self.key_watches:
+ self.key_watches.pop(key)
+
+ @inlineCallbacks
+ def _op_with_retry(self, operation, key, value, timeout, *args, **kw):
+ log.debug('kv-op', operation=operation, key=key, timeout=timeout, args=args, kw=kw)
+ err = None
+ result = None
+ if type(key) == str:
+ key = bytes(key)
+ if value is not None:
+ value = bytes(value)
+ while True:
+ try:
+ if operation == 'GET':
+ result = yield self._get(key)
+ elif operation == 'LIST':
+ result, err = yield self._list(key)
+ elif operation == 'PUT':
+ # Put returns an object of type Revision
+ result = yield self.client.set(key, value, **kw)
+ elif operation == 'DELETE':
+ # Delete returns an object of type Deleted
+ result = yield self.client.delete(key)
+ elif operation == 'RESERVE':
+ result, err = yield self._reserve(key, value, **kw)
+ elif operation == 'RENEW':
+ result, err = yield self._renew_reservation(key)
+ elif operation == 'RELEASE':
+ result, err = yield self._release_reservation(key)
+ elif operation == 'RELEASE-ALL':
+ err = yield self._release_all_reservations()
+ elif operation == 'WATCH':
+ for name, val in kw.items():
+ if name == 'callback':
+ callback = val
+ break
+ result = self.client.watch([KeySet(key, prefix=True)], callback)
+ self._clear_backoff()
+ break
+ except ConnectionRefusedError as ex:
+ log.error('comms-exception', ex=ex)
+ yield self._backoff('etcd-not-up')
+ except Exception as ex:
+ log.error('etcd-exception', ex=ex)
+ err = ex
+
+ if timeout > 0 and self.retry_time > timeout:
+ err = 'operation-timed-out'
+ if err is not None:
+ self._clear_backoff()
+ break
+
+ returnValue((result, err))
+
+ @inlineCallbacks
+ def _get(self, key):
+ kvp = None
+ resp = yield self.client.get(key)
+ if resp.kvs is not None and len(resp.kvs) == 1:
+ kv = resp.kvs[0]
+ kvp = KVPair(kv.key, kv.value, kv.mod_revision)
+ returnValue(kvp)
+
+ @inlineCallbacks
+ def _list(self, key):
+ err = None
+ list = []
+ resp = yield self.client.get(KeySet(key, prefix=True))
+ if resp.kvs is not None and len(resp.kvs) > 0:
+ for kv in resp.kvs:
+ list.append(KVPair(kv.key, kv.value, kv.mod_revision))
+ returnValue((list, err))
+
+ @inlineCallbacks
+ def _reserve(self, key, value, **kw):
+ for name, val in kw.items():
+ if name == 'ttl':
+ ttl = val
+ break
+ reserved = False
+ err = 'reservation-failed'
+ owner = None
+
+ # Create a lease
+ lease = yield self.client.lease(ttl)
+
+ # Create a transaction
+ txn = Transaction(
+ compare=[ CompVersion(key, '==', 0) ],
+ success=[ OpSet(key, bytes(value), lease=lease) ],
+ failure=[ OpGet(key) ]
+ )
+ newly_acquired = False
+ try:
+ result = yield self.client.submit(txn)
+ except Failed as failed:
+ log.debug('key-already-present', key=key)
+ if len(failed.responses) > 0:
+ response = failed.responses[0]
+ if response.kvs is not None and len(response.kvs) > 0:
+ kv = response.kvs[0]
+ log.debug('key-already-present', value=kv.value)
+ if kv.value == value:
+ reserved = True
+ log.debug('key-already-reserved', key = kv.key, value=kv.value)
+ else:
+ newly_acquired = True
+ log.debug('key-was-absent', key=key, result=result)
+
+ # Check if reservation succeeded
+ resp = yield self.client.get(key)
+ if resp.kvs is not None and len(resp.kvs) == 1:
+ owner = resp.kvs[0].value
+ if owner == value:
+ if newly_acquired:
+ log.debug('key-reserved', key=key, value=value, ttl=ttl,
+ lease_id=lease.lease_id)
+ reserved = True
+ # Add key to reservation list
+ self.key_reservations[key] = lease
+ else:
+ log.debug("reservation-still-held")
+ else:
+ log.debug('reservation-held-by-another', value=owner)
+
+ if reserved:
+ err = None
+ returnValue((owner, err))
+
+ @inlineCallbacks
+ def _renew_reservation(self, key):
+ result = None
+ err = None
+ if key not in self.key_reservations:
+ err = 'key-not-reserved'
+ else:
+ lease = self.key_reservations[key]
+ # A successfully refreshed lease returns an object of type Header
+ result = yield lease.refresh()
+ if result is None:
+ err = 'lease-refresh-failed'
+ returnValue((result, err))
+
+ @inlineCallbacks
+ def _release_reservation(self, key):
+ err = None
+ if key not in self.key_reservations:
+ err = 'key-not-reserved'
+ else:
+ lease = self.key_reservations[key]
+ time_left = yield lease.remaining()
+ # A successfully revoked lease returns an object of type Header
+ log.debug('release-reservation', key=key, lease_id=lease.lease_id,
+ time_left_in_secs=time_left)
+ result = yield lease.revoke()
+ if result is None:
+ err = 'lease-revoke-failed'
+ self.key_reservations.pop(key)
+ returnValue((result, err))
+
+ @inlineCallbacks
+ def _release_all_reservations(self):
+ err = None
+ keys_to_delete = []
+ for key in self.key_reservations:
+ lease = self.key_reservations[key]
+ time_left = yield lease.remaining()
+ # A successfully revoked lease returns an object of type Header
+ log.debug('release-reservation', key=key, lease_id=lease.lease_id,
+ time_left_in_secs=time_left)
+ result = yield lease.revoke()
+ if result is None:
+ err = 'lease-revoke-failed'
+ log.debug('lease-revoke', result=result)
+ keys_to_delete.append(key)
+ for key in keys_to_delete:
+ self.key_reservations.pop(key)
+ returnValue(err)
diff --git a/python/adapters/common/kvstore/kv_client.py b/python/adapters/common/kvstore/kv_client.py
new file mode 100644
index 0000000..f6486f3
--- /dev/null
+++ b/python/adapters/common/kvstore/kv_client.py
@@ -0,0 +1,206 @@
+# Copyright 2018-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.common.utils.asleep import asleep
+from structlog import get_logger
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+log = get_logger()
+
+class KVPair():
+ def __init__(self, key, value, index):
+ self.key = key
+ self.value = value
+ self.index = index
+
+class Event():
+ PUT = 0
+ DELETE = 1
+ CONNECTION_DOWN = 2
+
+ def __init__(self, event_type, key, value):
+ self.event_type = event_type
+ self.key = key
+ self.value = value
+
+RETRY_BACKOFF = [0.05, 0.1, 0.2, 0.5, 1, 2, 5]
+DEFAULT_TIMEOUT = 0.0
+for i in range(len(RETRY_BACKOFF)):
+ DEFAULT_TIMEOUT += RETRY_BACKOFF[i]
+
+class KVClient():
+
+ def __init__(self, kv_host, kv_port):
+ self.host = kv_host
+ self.port = kv_port
+ self.key_reservations = {}
+ self.key_watches = {}
+ self.retries = 0
+ self.retry_time = 0
+
+ @inlineCallbacks
+ def get(self, key, timeout=DEFAULT_TIMEOUT):
+ '''
+ This method returns the value of the given key in KV store.
+
+ :param key: The key whose value is requested
+ :param timeout: The length of time in seconds the method will wait for a response
+ :return: (KVPair, error) where KVPair is None if an error occurred
+ '''
+ result = yield self._op_with_retry('GET', key, None, timeout)
+ returnValue(result)
+
+ @inlineCallbacks
+ def list(self, key, timeout=DEFAULT_TIMEOUT):
+ '''
+ The list method returns an array of key-value pairs all of which
+ share the same key prefix.
+
+ :param key: The key prefix
+ :param timeout: The length of time in seconds the method will wait for a response
+ :return: ([]KVPair, error) where []KVPair is a list of KVPair objects
+ '''
+ result = yield self._op_with_retry('LIST', key, None, timeout)
+ returnValue(result)
+
+ @inlineCallbacks
+ def put(self, key, value, timeout=DEFAULT_TIMEOUT):
+ '''
+ The put method writes a value to the given key in KV store.
+ Do NOT modify a reserved key in an etcd store; doing so seems
+ to nullify the TTL of the key. In other words, the key lasts
+ forever.
+
+ :param key: The key to be written to
+ :param value: The value of the key
+ :param timeout: The length of time in seconds the method will wait for a response
+ :return: error, which is set to None for a successful write
+ '''
+ _, err = yield self._op_with_retry('PUT', key, value, timeout)
+ returnValue(err)
+
+ @inlineCallbacks
+ def delete(self, key, timeout=DEFAULT_TIMEOUT):
+ '''
+ The delete method removes a key from the KV store.
+
+ :param key: The key to be deleted
+ :param timeout: The length of time in seconds the method will wait for a response
+ :return: error, which is set to None for a successful deletion
+ '''
+ _, err = yield self._op_with_retry('DELETE', key, None, timeout)
+ returnValue(err)
+
+ @inlineCallbacks
+ def reserve(self, key, value, ttl, timeout=DEFAULT_TIMEOUT):
+ '''
+ This method acts essentially like a semaphore. The underlying mechanism
+ differs depending on the KV store: etcd uses a test-and-set transaction;
+ consul uses an acquire lock. If using etcd, do NOT write to the key
+ subsequent to the initial reservation; the TTL functionality may become
+ impaired (i.e. the reservation never expires).
+
+ :param key: The key under reservation
+ :param value: The reservation owner
+ :param ttl: The time-to-live (TTL) for the reservation. The key is unreserved
+ by the KV store when the TTL expires.
+ :param timeout: The length of time in seconds the method will wait for a response
+ :return: (key_value, error) If the key is acquired, then the value returned will
+ be the value passed in. If the key is already acquired, then the value assigned
+ to that key will be returned.
+ '''
+ result = yield self._op_with_retry('RESERVE', key, value, timeout, ttl=ttl)
+ returnValue(result)
+
+ @inlineCallbacks
+ def renew_reservation(self, key, timeout=DEFAULT_TIMEOUT):
+ '''
+ This method renews the reservation for a given key. A reservation expires
+ after the TTL (Time To Live) period specified when reserving the key.
+
+ :param key: The reserved key
+ :param timeout: The length of time in seconds the method will wait for a response
+ :return: error, which is set to None for a successful renewal
+ '''
+ result, err = yield self._op_with_retry('RENEW', key, None, timeout)
+ returnValue(err)
+
+ @inlineCallbacks
+ def release_reservation(self, key, timeout=DEFAULT_TIMEOUT):
+ '''
+ The release_reservation method cancels the reservation for a given key.
+
+ :param key: The reserved key
+ :param timeout: The length of time in seconds the method will wait for a response
+ :return: error, which is set to None for a successful cancellation
+ '''
+ result, err = yield self._op_with_retry('RELEASE', key, None, timeout)
+ returnValue(err)
+
+ @inlineCallbacks
+ def release_all_reservations(self, timeout=DEFAULT_TIMEOUT):
+ '''
+ This method cancels all key reservations made previously
+ using the reserve API.
+
+ :param timeout: The length of time in seconds the method will wait for a response
+ :return: error, which is set to None for a successful cancellation
+ '''
+ result, err = yield self._op_with_retry('RELEASE-ALL', None, None, timeout)
+ returnValue(err)
+
+ @inlineCallbacks
+ def watch(self, key, key_change_callback, timeout=DEFAULT_TIMEOUT):
+ '''
+ This method provides a watch capability for the given key. If the value of the key
+ changes or the key is deleted, then an event indicating the change is passed to
+ the given callback function.
+
+ :param key: The key to be watched
+ :param key_change_callback: The function invoked whenever the key changes
+ :param timeout: The length of time in seconds the method will wait for a response
+ :return: There is no return; key change events are passed to the callback function
+ '''
+ raise NotImplementedError('Method not implemented')
+
+ @inlineCallbacks
+ def close_watch(self, key, timeout=DEFAULT_TIMEOUT):
+ '''
+ This method closes the watch on the given key. Once the watch is closed, key
+ change events are no longer passed to the key change callback function.
+
+ :param key: The key under watch
+ :param timeout: The length of time in seconds the method will wait for a response
+ :return: There is no return
+ '''
+ raise NotImplementedError('Method not implemented')
+
+ @inlineCallbacks
+ def _op_with_retry(self, operation, key, value, timeout, *args, **kw):
+ raise NotImplementedError('Method not implemented')
+
+ def _backoff(self, msg):
+ wait_time = RETRY_BACKOFF[min(self.retries, len(RETRY_BACKOFF) - 1)]
+ self.retry_time += wait_time
+ self.retries += 1
+ log.error(msg, next_retry_in_secs=wait_time,
+ total_delay_in_secs = self.retry_time,
+ retries=self.retries)
+ return asleep(wait_time)
+
+ def _clear_backoff(self):
+ if self.retries:
+ log.debug('reset-backoff', after_retries=self.retries)
+ self.retries = 0
+ self.retry_time = 0
\ No newline at end of file
diff --git a/python/adapters/common/kvstore/kvstore.py b/python/adapters/common/kvstore/kvstore.py
new file mode 100644
index 0000000..ed7f246
--- /dev/null
+++ b/python/adapters/common/kvstore/kvstore.py
@@ -0,0 +1,31 @@
+# Copyright 2018-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from consul_client import ConsulClient
+from etcd_client import EtcdClient
+
+def create_kv_client(kv_store, host, port):
+ '''
+ Factory for creating a client interface to a KV store
+
+ :param kv_store: Specify either 'etcd' or 'consul'
+ :param host: Name or IP address of host serving the KV store
+ :param port: Port number (integer) of the KV service
+ :return: Reference to newly created client interface
+ '''
+ if kv_store == 'etcd':
+ return EtcdClient(host, port)
+ elif kv_store == 'consul':
+ return ConsulClient(host, port)
+ return None
diff --git a/python/adapters/common/pon_resource_manager/__init__.py b/python/adapters/common/pon_resource_manager/__init__.py
new file mode 100644
index 0000000..2d104e0
--- /dev/null
+++ b/python/adapters/common/pon_resource_manager/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/common/pon_resource_manager/resource_kv_store.py b/python/adapters/common/pon_resource_manager/resource_kv_store.py
new file mode 100644
index 0000000..a1a5c14
--- /dev/null
+++ b/python/adapters/common/pon_resource_manager/resource_kv_store.py
@@ -0,0 +1,107 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Resource KV store - interface between Resource Manager and backend store."""
+import structlog
+
+from voltha.core.config.config_backend import ConsulStore
+from voltha.core.config.config_backend import EtcdStore
+
+# KV store uses this prefix to store resource info
+PATH_PREFIX = 'resource_manager/{}'
+
+
+class ResourceKvStore(object):
+ """Implements apis to store/get/remove resource in backend store."""
+
+ def __init__(self, technology, device_id, backend, host, port):
+ """
+ Create ResourceKvStore object.
+
+ Based on backend ('consul' and 'etcd' use the host and port
+ to create the respective object.
+
+ :param technology: PON technology
+ :param device_id: OLT device id
+ :param backend: Type of backend storage (etcd or consul)
+ :param host: host ip info for backend storage
+ :param port: port for the backend storage
+ :raises exception when invalid backend store passed as an argument
+ """
+ # logger
+ self._log = structlog.get_logger()
+
+ path = PATH_PREFIX.format(technology)
+ try:
+ if backend == 'consul':
+ self._kv_store = ConsulStore(host, port, path)
+ elif backend == 'etcd':
+ self._kv_store = EtcdStore(host, port, path)
+ else:
+ self._log.error('Invalid-backend')
+ raise Exception("Invalid-backend-for-kv-store")
+ except Exception as e:
+ self._log.exception("exception-in-init")
+ raise Exception(e)
+
+ def update_to_kv_store(self, path, resource):
+ """
+ Update resource.
+
+ :param path: path to update the resource
+ :param resource: updated resource
+ """
+ try:
+ self._kv_store[path] = str(resource)
+ self._log.debug("Resource-updated-in-kv-store", path=path)
+ return True
+ except BaseException:
+ self._log.exception("Resource-update-in-kv-store-failed",
+ path=path, resource=resource)
+ return False
+
+ def get_from_kv_store(self, path):
+ """
+ Get resource.
+
+ :param path: path to get the resource
+ """
+ resource = None
+ try:
+ resource = self._kv_store[path]
+ self._log.debug("Got-resource-from-kv-store", path=path)
+ except KeyError:
+ self._log.info("Resource-not-found-updating-resource",
+ path=path)
+ except BaseException:
+ self._log.exception("Getting-resource-from-kv-store-failed",
+ path=path)
+ return resource
+
+ def remove_from_kv_store(self, path):
+ """
+ Remove resource.
+
+ :param path: path to remove the resource
+ """
+ try:
+ del self._kv_store[path]
+ self._log.debug("Resource-deleted-in-kv-store", path=path)
+ return True
+ except BaseException:
+ self._log.exception("Resource-delete-in-kv-store-failed",
+ path=path)
+ return False
diff --git a/python/adapters/common/pon_resource_manager/resource_manager.py b/python/adapters/common/pon_resource_manager/resource_manager.py
new file mode 100644
index 0000000..17b2871
--- /dev/null
+++ b/python/adapters/common/pon_resource_manager/resource_manager.py
@@ -0,0 +1,677 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Resource Manager will be unique for each OLT device.
+
+It exposes APIs to create/free alloc_ids/onu_ids/gemport_ids. Resource Manager
+uses a KV store in backend to ensure resiliency of the data.
+"""
+import json
+import structlog
+from bitstring import BitArray
+from ast import literal_eval
+import shlex
+from argparse import ArgumentParser, ArgumentError
+
+from common.pon_resource_manager.resource_kv_store import ResourceKvStore
+
+
+# Used to parse extra arguments to OpenOlt adapter from the NBI
+class OltVendorArgumentParser(ArgumentParser):
+ # Must override the exit command to prevent it from
+ # calling sys.exit(). Return exception instead.
+ def exit(self, status=0, message=None):
+ raise Exception(message)
+
+
+class PONResourceManager(object):
+ """Implements APIs to initialize/allocate/release alloc/gemport/onu IDs."""
+
+ # Constants to identify resource pool
+ ONU_ID = 'ONU_ID'
+ ALLOC_ID = 'ALLOC_ID'
+ GEMPORT_ID = 'GEMPORT_ID'
+
+ # The resource ranges for a given device vendor_type should be placed
+ # at 'resource_manager/<technology>/resource_ranges/<olt_vendor_type>'
+ # path on the KV store.
+ # If Resource Range parameters are to be read from the external KV store,
+ # they are expected to be stored in the following format.
+ # Note: All parameters are MANDATORY for now.
+ '''
+ {
+ "onu_id_start": 1,
+ "onu_id_end": 127,
+ "alloc_id_start": 1024,
+ "alloc_id_end": 2816,
+ "gemport_id_start": 1024,
+ "gemport_id_end": 8960,
+ "pon_ports": 16
+ }
+
+ '''
+ # constants used as keys to reference the resource range parameters from
+ # and external KV store.
+ ONU_START_IDX = "onu_id_start"
+ ONU_END_IDX = "onu_id_end"
+ ALLOC_ID_START_IDX = "alloc_id_start"
+ ALLOC_ID_END_IDX = "alloc_id_end"
+ GEM_PORT_ID_START_IDX = "gemport_id_start"
+ GEM_PORT_ID_END_IDX = "gemport_id_end"
+ NUM_OF_PON_PORT = "pon_ports"
+
+ # PON Resource range configuration on the KV store.
+ # Format: 'resource_manager/<technology>/resource_ranges/<olt_vendor_type>'
+ # The KV store backend is initialized with a path prefix and we need to
+ # provide only the suffix.
+ PON_RESOURCE_RANGE_CONFIG_PATH = 'resource_ranges/{}'
+
+ # resource path suffix
+ ALLOC_ID_POOL_PATH = '{}/alloc_id_pool/{}'
+ GEMPORT_ID_POOL_PATH = '{}/gemport_id_pool/{}'
+ ONU_ID_POOL_PATH = '{}/onu_id_pool/{}'
+
+ # Path on the KV store for storing list of alloc IDs for a given ONU
+ # Format: <device_id>/<(pon_intf_id, onu_id)>/alloc_ids
+ ALLOC_ID_RESOURCE_MAP_PATH = '{}/{}/alloc_ids'
+
+ # Path on the KV store for storing list of gemport IDs for a given ONU
+ # Format: <device_id>/<(pon_intf_id, onu_id)>/gemport_ids
+ GEMPORT_ID_RESOURCE_MAP_PATH = '{}/{}/gemport_ids'
+
+ # Constants for internal usage.
+ PON_INTF_ID = 'pon_intf_id'
+ START_IDX = 'start_idx'
+ END_IDX = 'end_idx'
+ POOL = 'pool'
+
+ def __init__(self, technology, extra_args, device_id,
+ backend, host, port):
+ """
+ Create PONResourceManager object.
+
+ :param technology: PON technology
+ :param: extra_args: This string contains extra arguments passed during
+ pre-provisioning of OLT and specifies the OLT Vendor type
+ :param device_id: OLT device id
+ :param backend: backend store
+ :param host: ip of backend store
+ :param port: port on which backend store listens
+ :raises exception when invalid backend store passed as an argument
+ """
+ # logger
+ self._log = structlog.get_logger()
+
+ try:
+ self.technology = technology
+ self.extra_args = extra_args
+ self.device_id = device_id
+ self.backend = backend
+ self.host = host
+ self.port = port
+ self.olt_vendor = None
+ self._kv_store = ResourceKvStore(technology, device_id, backend,
+ host, port)
+ # Below attribute, pon_resource_ranges, should be initialized
+ # by reading from KV store.
+ self.pon_resource_ranges = dict()
+ except Exception as e:
+ self._log.exception("exception-in-init")
+ raise Exception(e)
+
+ def init_resource_ranges_from_kv_store(self):
+ """
+ Initialize PON resource ranges with config fetched from kv store.
+
+ :return boolean: True if PON resource ranges initialized else false
+ """
+ self.olt_vendor = self._get_olt_vendor()
+ # Try to initialize the PON Resource Ranges from KV store based on the
+ # OLT vendor key, if available
+ if self.olt_vendor is None:
+ self._log.info("olt-vendor-unavailable--not-reading-from-kv-store")
+ return False
+
+ path = self.PON_RESOURCE_RANGE_CONFIG_PATH.format(self.olt_vendor)
+ try:
+ # get resource from kv store
+ result = self._kv_store.get_from_kv_store(path)
+
+ if result is None:
+ self._log.debug("resource-range-config-unavailable-on-kvstore")
+ return False
+
+ resource_range_config = result
+
+ if resource_range_config is not None:
+ self.pon_resource_ranges = json.loads(resource_range_config)
+ self._log.debug("Init-resource-ranges-from-kvstore-success",
+ pon_resource_ranges=self.pon_resource_ranges,
+ path=path)
+ return True
+
+ except Exception as e:
+ self._log.exception("error-initializing-resource-range-from-kv-store",
+ e=e)
+ return False
+
+ def init_default_pon_resource_ranges(self, onu_start_idx=1,
+ onu_end_idx=127,
+ alloc_id_start_idx=1024,
+ alloc_id_end_idx=2816,
+ gem_port_id_start_idx=1024,
+ gem_port_id_end_idx=8960,
+ num_of_pon_ports=16):
+ """
+ Initialize default PON resource ranges
+
+ :param onu_start_idx: onu id start index
+ :param onu_end_idx: onu id end index
+ :param alloc_id_start_idx: alloc id start index
+ :param alloc_id_end_idx: alloc id end index
+ :param gem_port_id_start_idx: gemport id start index
+ :param gem_port_id_end_idx: gemport id end index
+ :param num_of_pon_ports: number of PON ports
+ """
+ self._log.info("initialize-default-resource-range-values")
+ self.pon_resource_ranges[
+ PONResourceManager.ONU_START_IDX] = onu_start_idx
+ self.pon_resource_ranges[PONResourceManager.ONU_END_IDX] = onu_end_idx
+ self.pon_resource_ranges[
+ PONResourceManager.ALLOC_ID_START_IDX] = alloc_id_start_idx
+ self.pon_resource_ranges[
+ PONResourceManager.ALLOC_ID_END_IDX] = alloc_id_end_idx
+ self.pon_resource_ranges[
+ PONResourceManager.GEM_PORT_ID_START_IDX] = gem_port_id_start_idx
+ self.pon_resource_ranges[
+ PONResourceManager.GEM_PORT_ID_END_IDX] = gem_port_id_end_idx
+ self.pon_resource_ranges[
+ PONResourceManager.NUM_OF_PON_PORT] = num_of_pon_ports
+
+ def init_device_resource_pool(self):
+ """
+ Initialize resource pool for all PON ports.
+ """
+ i = 0
+ while i < self.pon_resource_ranges[PONResourceManager.NUM_OF_PON_PORT]:
+ self.init_resource_id_pool(
+ pon_intf_id=i,
+ resource_type=PONResourceManager.ONU_ID,
+ start_idx=self.pon_resource_ranges[
+ PONResourceManager.ONU_START_IDX],
+ end_idx=self.pon_resource_ranges[
+ PONResourceManager.ONU_END_IDX])
+
+ i += 1
+
+ # TODO: ASFvOLT16 platform requires alloc and gemport ID to be unique
+ # across OLT. To keep it simple, a single pool (POOL 0) is maintained
+ # for both the resource types. This may need to change later.
+ self.init_resource_id_pool(
+ pon_intf_id=0,
+ resource_type=PONResourceManager.ALLOC_ID,
+ start_idx=self.pon_resource_ranges[
+ PONResourceManager.ALLOC_ID_START_IDX],
+ end_idx=self.pon_resource_ranges[
+ PONResourceManager.ALLOC_ID_END_IDX])
+
+ self.init_resource_id_pool(
+ pon_intf_id=0,
+ resource_type=PONResourceManager.GEMPORT_ID,
+ start_idx=self.pon_resource_ranges[
+ PONResourceManager.GEM_PORT_ID_START_IDX],
+ end_idx=self.pon_resource_ranges[
+ PONResourceManager.GEM_PORT_ID_END_IDX])
+
+ def clear_device_resource_pool(self):
+ """
+ Clear resource pool of all PON ports.
+ """
+ i = 0
+ while i < self.pon_resource_ranges[PONResourceManager.NUM_OF_PON_PORT]:
+ self.clear_resource_id_pool(
+ pon_intf_id=i,
+ resource_type=PONResourceManager.ONU_ID,
+ )
+ i += 1
+
+ self.clear_resource_id_pool(
+ pon_intf_id=0,
+ resource_type=PONResourceManager.ALLOC_ID,
+ )
+
+ self.clear_resource_id_pool(
+ pon_intf_id=0,
+ resource_type=PONResourceManager.GEMPORT_ID,
+ )
+
+ def init_resource_id_pool(self, pon_intf_id, resource_type, start_idx,
+ end_idx):
+ """
+ Initialize Resource ID pool for a given Resource Type on a given PON Port
+
+ :param pon_intf_id: OLT PON interface id
+ :param resource_type: String to identify type of resource
+ :param start_idx: start index for onu id pool
+ :param end_idx: end index for onu id pool
+ :return boolean: True if resource id pool initialized else false
+ """
+ status = False
+ path = self._get_path(pon_intf_id, resource_type)
+ if path is None:
+ return status
+
+ try:
+ # In case of adapter reboot and reconciliation resource in kv store
+ # checked for its presence if not kv store update happens
+ resource = self._get_resource(path)
+
+ if resource is not None:
+ self._log.info("Resource-already-present-in-store", path=path)
+ status = True
+ else:
+ resource = self._format_resource(pon_intf_id, start_idx,
+ end_idx)
+ self._log.info("Resource-initialized", path=path)
+
+ # Add resource as json in kv store.
+ result = self._kv_store.update_to_kv_store(path, resource)
+ if result is True:
+ status = True
+
+ except Exception as e:
+ self._log.exception("error-initializing-resource-pool", e=e)
+
+ return status
+
+ def get_resource_id(self, pon_intf_id, resource_type, num_of_id=1):
+ """
+ Create alloc/gemport/onu id for given OLT PON interface.
+
+ :param pon_intf_id: OLT PON interface id
+ :param resource_type: String to identify type of resource
+ :param num_of_id: required number of ids
+ :return list/int/None: list, int or None if resource type is
+ alloc_id/gemport_id, onu_id or invalid type
+ respectively
+ """
+ result = None
+
+ # TODO: ASFvOLT16 platform requires alloc and gemport ID to be unique
+ # across OLT. To keep it simple, a single pool (POOL 0) is maintained
+ # for both the resource types. This may need to change later.
+ # Override the incoming pon_intf_id to PON0
+ if resource_type == PONResourceManager.GEMPORT_ID or \
+ resource_type == PONResourceManager.ALLOC_ID:
+ pon_intf_id = 0
+
+ path = self._get_path(pon_intf_id, resource_type)
+ if path is None:
+ return result
+
+ try:
+ resource = self._get_resource(path)
+ if resource is not None and resource_type == \
+ PONResourceManager.ONU_ID:
+ result = self._generate_next_id(resource)
+ elif resource is not None and (
+ resource_type == PONResourceManager.GEMPORT_ID or
+ resource_type == PONResourceManager.ALLOC_ID):
+ result = list()
+ while num_of_id > 0:
+ result.append(self._generate_next_id(resource))
+ num_of_id -= 1
+ else:
+ raise Exception("get-resource-failed")
+
+ self._log.debug("Get-" + resource_type + "-success", result=result,
+ path=path)
+ # Update resource in kv store
+ self._update_resource(path, resource)
+
+ except Exception as e:
+ self._log.exception("Get-" + resource_type + "-id-failed",
+ path=path, e=e)
+ return result
+
+ def free_resource_id(self, pon_intf_id, resource_type, release_content):
+ """
+ Release alloc/gemport/onu id for given OLT PON interface.
+
+ :param pon_intf_id: OLT PON interface id
+ :param resource_type: String to identify type of resource
+ :param release_content: required number of ids
+ :return boolean: True if all IDs in given release_content released
+ else False
+ """
+ status = False
+
+ # TODO: ASFvOLT16 platform requires alloc and gemport ID to be unique
+ # across OLT. To keep it simple, a single pool (POOL 0) is maintained
+ # for both the resource types. This may need to change later.
+ # Override the incoming pon_intf_id to PON0
+ if resource_type == PONResourceManager.GEMPORT_ID or \
+ resource_type == PONResourceManager.ALLOC_ID:
+ pon_intf_id = 0
+
+ path = self._get_path(pon_intf_id, resource_type)
+ if path is None:
+ return status
+
+ try:
+ resource = self._get_resource(path)
+ if resource is not None and resource_type == \
+ PONResourceManager.ONU_ID:
+ self._release_id(resource, release_content)
+ elif resource is not None and (
+ resource_type == PONResourceManager.ALLOC_ID or
+ resource_type == PONResourceManager.GEMPORT_ID):
+ for content in release_content:
+ self._release_id(resource, content)
+ else:
+ raise Exception("get-resource-failed")
+
+ self._log.debug("Free-" + resource_type + "-success", path=path)
+
+ # Update resource in kv store
+ status = self._update_resource(path, resource)
+
+ except Exception as e:
+ self._log.exception("Free-" + resource_type + "-failed",
+ path=path, e=e)
+ return status
+
+ def clear_resource_id_pool(self, pon_intf_id, resource_type):
+ """
+ Clear Resource Pool for a given Resource Type on a given PON Port.
+
+ :return boolean: True if removed else False
+ """
+ path = self._get_path(pon_intf_id, resource_type)
+ if path is None:
+ return False
+
+ try:
+ result = self._kv_store.remove_from_kv_store(path)
+ if result is True:
+ self._log.debug("Resource-pool-cleared",
+ device_id=self.device_id,
+ path=path)
+ return True
+ except Exception as e:
+ self._log.exception("error-clearing-resource-pool", e=e)
+
+ self._log.error("Clear-resource-pool-failed", device_id=self.device_id,
+ path=path)
+ return False
+
+ def init_resource_map(self, pon_intf_onu_id):
+ """
+ Initialize resource map
+
+ :param pon_intf_onu_id: reference of PON interface id and onu id
+ """
+ # initialize pon_intf_onu_id tuple to alloc_ids map
+ alloc_id_path = PONResourceManager.ALLOC_ID_RESOURCE_MAP_PATH.format(
+ self.device_id, str(pon_intf_onu_id)
+ )
+ alloc_ids = list()
+ self._kv_store.update_to_kv_store(
+ alloc_id_path, json.dumps(alloc_ids)
+ )
+
+ # initialize pon_intf_onu_id tuple to gemport_ids map
+ gemport_id_path = PONResourceManager.GEMPORT_ID_RESOURCE_MAP_PATH.format(
+ self.device_id, str(pon_intf_onu_id)
+ )
+ gemport_ids = list()
+ self._kv_store.update_to_kv_store(
+ gemport_id_path, json.dumps(gemport_ids)
+ )
+
+ def remove_resource_map(self, pon_intf_onu_id):
+ """
+ Remove resource map
+
+ :param pon_intf_onu_id: reference of PON interface id and onu id
+ """
+ # remove pon_intf_onu_id tuple to alloc_ids map
+ alloc_id_path = PONResourceManager.ALLOC_ID_RESOURCE_MAP_PATH.format(
+ self.device_id, str(pon_intf_onu_id)
+ )
+ self._kv_store.remove_from_kv_store(alloc_id_path)
+
+ # remove pon_intf_onu_id tuple to gemport_ids map
+ gemport_id_path = PONResourceManager.GEMPORT_ID_RESOURCE_MAP_PATH.format(
+ self.device_id, str(pon_intf_onu_id)
+ )
+ self._kv_store.remove_from_kv_store(gemport_id_path)
+
+ def get_current_alloc_ids_for_onu(self, pon_intf_onu_id):
+ """
+ Get currently configured alloc ids for given pon_intf_onu_id
+
+ :param pon_intf_onu_id: reference of PON interface id and onu id
+ """
+ path = PONResourceManager.ALLOC_ID_RESOURCE_MAP_PATH.format(
+ self.device_id,
+ str(pon_intf_onu_id))
+ value = self._kv_store.get_from_kv_store(path)
+ if value is not None:
+ alloc_id_list = json.loads(value)
+ if len(alloc_id_list) > 0:
+ return alloc_id_list
+
+ return None
+
+ def get_current_gemport_ids_for_onu(self, pon_intf_onu_id):
+ """
+ Get currently configured gemport ids for given pon_intf_onu_id
+
+ :param pon_intf_onu_id: reference of PON interface id and onu id
+ """
+
+ path = PONResourceManager.GEMPORT_ID_RESOURCE_MAP_PATH.format(
+ self.device_id,
+ str(pon_intf_onu_id))
+ value = self._kv_store.get_from_kv_store(path)
+ if value is not None:
+ gemport_id_list = json.loads(value)
+ if len(gemport_id_list) > 0:
+ return gemport_id_list
+
+ return None
+
+ def update_alloc_ids_for_onu(self, pon_intf_onu_id, alloc_ids):
+ """
+ Update currently configured alloc ids for given pon_intf_onu_id
+
+ :param pon_intf_onu_id: reference of PON interface id and onu id
+ """
+ path = PONResourceManager.ALLOC_ID_RESOURCE_MAP_PATH.format(
+ self.device_id, str(pon_intf_onu_id)
+ )
+ self._kv_store.update_to_kv_store(
+ path, json.dumps(alloc_ids)
+ )
+
+ def update_gemport_ids_for_onu(self, pon_intf_onu_id, gemport_ids):
+ """
+ Update currently configured gemport ids for given pon_intf_onu_id
+
+ :param pon_intf_onu_id: reference of PON interface id and onu id
+ """
+ path = PONResourceManager.GEMPORT_ID_RESOURCE_MAP_PATH.format(
+ self.device_id, str(pon_intf_onu_id)
+ )
+ self._kv_store.update_to_kv_store(
+ path, json.dumps(gemport_ids)
+ )
+
+ def _get_olt_vendor(self):
+ """
+ Get olt vendor variant
+
+ :return: type of olt vendor
+ """
+ olt_vendor = None
+ if self.extra_args and len(self.extra_args) > 0:
+ parser = OltVendorArgumentParser(add_help=False)
+ parser.add_argument('--olt_vendor', '-o', action='store',
+ choices=['default', 'asfvolt16', 'cigolt24'],
+ default='default')
+ try:
+ args = parser.parse_args(shlex.split(self.extra_args))
+ self._log.debug('parsing-extra-arguments', args=args)
+ olt_vendor = args.olt_vendor
+ except ArgumentError as e:
+ self._log.exception('invalid-arguments: {}', e=e)
+ except Exception as e:
+ self._log.exception('option-parsing-error: {}', e=e)
+
+ return olt_vendor
+
+ def _generate_next_id(self, resource):
+ """
+ Generate unique id having OFFSET as start index.
+
+ :param resource: resource used to generate ID
+ :return int: generated id
+ """
+ pos = resource[PONResourceManager.POOL].find('0b0')
+ resource[PONResourceManager.POOL].set(1, pos)
+ return pos[0] + resource[PONResourceManager.START_IDX]
+
+ def _release_id(self, resource, unique_id):
+ """
+ Release unique id having OFFSET as start index.
+
+ :param resource: resource used to release ID
+ :param unique_id: id need to be released
+ """
+ pos = ((int(unique_id)) - resource[PONResourceManager.START_IDX])
+ resource[PONResourceManager.POOL].set(0, pos)
+
+ def _get_path(self, pon_intf_id, resource_type):
+ """
+ Get path for given resource type.
+
+ :param pon_intf_id: OLT PON interface id
+ :param resource_type: String to identify type of resource
+ :return: path for given resource type
+ """
+ path = None
+ if resource_type == PONResourceManager.ONU_ID:
+ path = self._get_onu_id_resource_path(pon_intf_id)
+ elif resource_type == PONResourceManager.ALLOC_ID:
+ path = self._get_alloc_id_resource_path(pon_intf_id)
+ elif resource_type == PONResourceManager.GEMPORT_ID:
+ path = self._get_gemport_id_resource_path(pon_intf_id)
+ else:
+ self._log.error("invalid-resource-pool-identifier")
+ return path
+
+ def _get_alloc_id_resource_path(self, pon_intf_id):
+ """
+ Get alloc id resource path.
+
+ :param pon_intf_id: OLT PON interface id
+ :return: alloc id resource path
+ """
+ return PONResourceManager.ALLOC_ID_POOL_PATH.format(
+ self.device_id, pon_intf_id)
+
+ def _get_gemport_id_resource_path(self, pon_intf_id):
+ """
+ Get gemport id resource path.
+
+ :param pon_intf_id: OLT PON interface id
+ :return: gemport id resource path
+ """
+ return PONResourceManager.GEMPORT_ID_POOL_PATH.format(
+ self.device_id, pon_intf_id)
+
+ def _get_onu_id_resource_path(self, pon_intf_id):
+ """
+ Get onu id resource path.
+
+ :param pon_intf_id: OLT PON interface id
+ :return: onu id resource path
+ """
+ return PONResourceManager.ONU_ID_POOL_PATH.format(
+ self.device_id, pon_intf_id)
+
+ def _update_resource(self, path, resource):
+ """
+ Update resource in resource kv store.
+
+ :param path: path to update resource
+ :param resource: resource need to be updated
+ :return boolean: True if resource updated in kv store else False
+ """
+ resource[PONResourceManager.POOL] = \
+ resource[PONResourceManager.POOL].bin
+ result = self._kv_store.update_to_kv_store(path, json.dumps(resource))
+ if result is True:
+ return True
+ return False
+
+ def _get_resource(self, path):
+ """
+ Get resource from kv store.
+
+ :param path: path to get resource
+ :return: resource if resource present in kv store else None
+ """
+ # get resource from kv store
+ result = self._kv_store.get_from_kv_store(path)
+ if result is None:
+ return result
+ self._log.info("dumping resource", result=result)
+ resource = result
+
+ if resource is not None:
+ # decode resource fetched from backend store to dictionary
+ resource = json.loads(resource)
+
+ # resource pool in backend store stored as binary string whereas to
+ # access the pool to generate/release IDs it need to be converted
+ # as BitArray
+ resource[PONResourceManager.POOL] = \
+ BitArray('0b' + resource[PONResourceManager.POOL])
+
+ return resource
+
+ def _format_resource(self, pon_intf_id, start_idx, end_idx):
+ """
+ Format resource as json.
+
+ :param pon_intf_id: OLT PON interface id
+ :param start_idx: start index for id pool
+ :param end_idx: end index for id pool
+ :return dictionary: resource formatted as dictionary
+ """
+ # Format resource as json to be stored in backend store
+ resource = dict()
+ resource[PONResourceManager.PON_INTF_ID] = pon_intf_id
+ resource[PONResourceManager.START_IDX] = start_idx
+ resource[PONResourceManager.END_IDX] = end_idx
+
+ # resource pool stored in backend store as binary string
+ resource[PONResourceManager.POOL] = BitArray(end_idx).bin
+
+ return json.dumps(resource)
diff --git a/python/adapters/extensions/__init__.py b/python/adapters/extensions/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/alarms/README.md b/python/adapters/extensions/alarms/README.md
new file mode 100644
index 0000000..2ac971a
--- /dev/null
+++ b/python/adapters/extensions/alarms/README.md
@@ -0,0 +1,62 @@
+# VOLTHA Alarm Library
+
+This directory provides a common library for the creation of Alarms by adapters within VOLTHA
+and should be used to insure that published alarms from different adapters use the same format
+
+## Alarm Manager Creation
+
+Each device handler should create an instance of the **AdapterAlarms** alarm manager shortly after
+initial activation. This alarm manager is responsible for the formatting and sending of alarms
+by the adapters.
+
+## Raising and Clearing Alarms
+
+To create a specific alarm, create an instance of the specific alarm you wish to publish
+(such as **OnuDiscoveryAlarms** for newly discovered ONUs) and pass in alarm specific information
+to the initialize.
+
+Once constructed, you can call the alarm's **_raise_alarm()_** method to format and send an active
+alarm, or the **_clear_alarm()_** to clear it.
+
+# Basic Alarm Format
+
+Here is an JSON example of a current alarm published on the kafka bus under the
+_voltha.alarms_ topic:
+
+```json
+{
+ "id": "voltha.adtran_olt.000198f9c4d2ae80.Discovery",
+ "description": "adtran_olt.000198f9c4d2ae80 - ONU DISCOVERY Alarm - DISCOVERY - Raised",
+ "logical_device_id": "0001112233445566",
+ "state": "RAISED",
+ "category": "PON",
+ "severity": "CRITICAL",
+ "resource_id": "0",
+ "type": "EQUIPMENT",
+ "reported_ts": 1532031872.0,
+ "raised_ts": 1532031872.0,
+ "changed_ts": 0.0,
+ "context": {
+ "serial-number": "ADTN17230031",
+ "pon-id": "0"
+ }
+}
+```
+
+# Remaining Work Items
+This initial code is only a prelimenary sample. The following tasks need to be
+added to the VOLTHA JIRA or performed in the SEBA group.
+
+- Get a list from SEBA/VOLTHA on required alarms
+
+- Provide example JSON output and verify that it meets SEBA's requirements
+
+- Get feedback from other OLT/ONU developers on any needed changes
+
+- For the logical_device_id, this is reported in the format that the device adapter has which
+ includes the vcore number (often 0001) in the first four nibble. Should this be normalized to
+ all zeros?
+
+- Support alarm_suppression capability (via IAdapter call). Needs investigation
+
+- TODO: Probably a few more. Look through code for more 'TODO' Notes
diff --git a/python/adapters/extensions/alarms/__init__.py b/python/adapters/extensions/alarms/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/alarms/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/alarms/adapter_alarms.py b/python/adapters/extensions/alarms/adapter_alarms.py
new file mode 100644
index 0000000..b24113f
--- /dev/null
+++ b/python/adapters/extensions/alarms/adapter_alarms.py
@@ -0,0 +1,205 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import structlog
+import arrow
+from voltha.protos.events_pb2 import AlarmEventType, AlarmEventSeverity,\
+ AlarmEventState, AlarmEventCategory
+log = structlog.get_logger()
+
+
+# TODO: In the device adapter, the following alarms are still TBD
+# (Taken from openolt_alarms)
+# onu_alarm_ind
+# onu_startup_failure_indication
+# onu_signal_degrade_indication
+# onu_drift_of_window_ind
+# onu_loss_omci_ind
+# onu_signals_fail_ind
+# onu_tiwi_ind
+# onu_activation_fail_ind
+# onu_processing_error_ind
+
+
+class AdapterAlarms:
+ """
+ Class for managing Alarms within a given Device Handler instance
+ """
+ def __init__(self, adapter_agent, device_id, logical_device_id):
+ """
+ Adapter alarm manager initializer
+
+ :param adapter_agent: (AdapterAgent) Adapter agent reference
+ :param device_id: (str) Device handler's unique device id
+ :param logical_device_id: (str) Logical Device that the device is a member of
+ """
+ self.log = structlog.get_logger(device_id=device_id)
+ self.adapter_agent = adapter_agent
+ self.device_id = device_id
+ self.logical_device_id = logical_device_id
+ self.adapter_name = adapter_agent.adapter_name
+ self.lc = None
+
+ def format_id(self, alarm):
+ """
+ Format the Unique Alarm ID for this alarm. This is provided in the alarms
+ 'id' field
+
+ :param alarm: (str) The name of the alarm such as 'Discover' or 'LOS'
+
+ :return: (str) Alarm ID
+ """
+ return 'voltha.{}.{}.{}'.format(self.adapter_name,
+ self.device_id,
+ alarm)
+
+ def format_description(self, _object, alarm, status):
+ """
+ Format the textual description field of this alarm
+
+ :param _object: ()
+ :param alarm: (str) The name of the alarm such as 'Discover' or 'LOS'
+ :param status: (bool) If True, the alarm is active (it is being raised)
+
+ :return: (str) Alarm description
+ """
+ return '{} Alarm - {} - {}'.format(_object.upper(),
+ alarm.upper(),
+ 'Raised' if status else 'Cleared')
+
+ def send_alarm(self, context_data, alarm_data):
+ """
+ Send the alarm to the event bus
+
+ :param context_data: (dict) Alarm specific context data
+ :param alarm_data: (dict) Common Alarm information dictionary
+ """
+ try:
+ current_context = {}
+ if isinstance(context_data, dict):
+ for key, value in context_data.iteritems():
+ current_context[key] = str(value)
+ ser_num = None
+ device = self.adapter_agent.get_device(device_id=self.device_id)
+ ser_num = device.serial_number
+
+
+ """
+ Only put in the onu serial numbers since the OLT does not currently have a serial number and the
+ value is the ip:port address.
+ """
+ if isinstance(context_data, dict) and '_onu' in device.type.lower():
+ current_context["onu_serial_number"] = ser_num
+ alarm_event = self.adapter_agent.create_alarm(
+ id=alarm_data.get('id', 'voltha.{}.{}.olt'.format(self.adapter_name,
+ self.device_id)),
+ resource_id=str(alarm_data.get('resource_id', self.device_id)),
+ description="{}.{} - {}".format(self.adapter_name, self.device_id,
+ alarm_data.get('description')),
+ type=alarm_data.get('type'),
+ category=alarm_data.get('category'),
+ severity=alarm_data.get('severity'),
+ state=alarm_data.get('state'),
+ raised_ts=alarm_data.get('ts', 0),
+ context=current_context,
+ logical_device_id=self.logical_device_id,
+ alarm_type_name=alarm_data.get('alarm_type_name')
+ )
+ self.adapter_agent.submit_alarm(self.device_id, alarm_event)
+
+ except Exception as e:
+ self.log.exception('failed-to-send-alarm', e=e)
+ raise
+
+
+class AlarmBase(object):
+ """Base class for alarms"""
+ def __init__(self, alarm_mgr, object_type, alarm,
+ alarm_category,
+ resource_id=None,
+ alarm_type=AlarmEventType.EQUIPMENT,
+ alarm_severity=AlarmEventSeverity.CRITICAL):
+ """
+ Initializer for the Alarm base class
+
+ :param alarm_mgr: (AdapterAlarms) Reference to the device handler's Adapter
+ Alarm manager
+ :param object_type: (str) Type of device generating the alarm such as 'olt' or 'onu'
+ :param alarm: (str) A textual name for the alarm such as 'HeartBeat' or 'Discovery'
+ :param alarm_category: (AlarmEventCategory) Refers to functional category of
+ the alarm
+ :param resource_id: (str) Identifier of the originating resource of the alarm
+ :param alarm_type: (AlarmEventType) Refers to the area of the system impacted
+ by the alarm
+ :param alarm_severity: (AlarmEventSeverity) Overall impact of the alarm on the
+ system
+ """
+ self._alarm_mgr = alarm_mgr
+ self._object_type = object_type
+ self._alarm = alarm
+ self._alarm_category = alarm_category
+ self._alarm_type = alarm_type
+ self._alarm_severity = alarm_severity
+ self._resource_id = resource_id
+
+ def get_alarm_data(self, status):
+ """
+ Get the alarm specific data and format it into a dictionary. When the alarm
+ is being sent to the event bus, this dictionary provides a majority of the
+ fields for the alarms.
+
+ :param status: (bool) True if the alarm is active/raised
+ :return: (dict) Alarm data
+ """
+ data = {
+ 'ts': arrow.utcnow().timestamp,
+ 'description': self._alarm_mgr.format_description(self._object_type,
+ self._alarm,
+ status),
+ 'id': self._alarm_mgr.format_id(self._alarm),
+ 'type': self._alarm_type,
+ 'category': self._alarm_category,
+ 'severity': self._alarm_severity,
+ 'state': AlarmEventState.RAISED if status else AlarmEventState.CLEARED,
+ 'alarm_type_name': self._alarm
+ }
+ if self._resource_id is not None:
+ data['resource_id'] = self._resource_id
+ return data
+
+ def get_context_data(self):
+ """
+ Get alarm specific context data. If an alarm has specific data to specify, it is
+ included in the context field in the published event
+
+ :return: (dict) Dictionary with alarm specific context data
+ """
+ return {} # NOTE: You should override this if needed
+
+ def raise_alarm(self):
+ """
+ Called to set the state of an alarm to active and to send it to the event bus
+ """
+ alarm_data = self.get_alarm_data(True)
+ context_data = self.get_context_data()
+ self._alarm_mgr.send_alarm(context_data, alarm_data)
+
+ def clear_alarm(self):
+ """
+ Called to set the state of an alarm to inactive and to send it to the event bus
+ """
+ alarm_data = self.get_alarm_data(False)
+ context_data = self.get_context_data()
+ self._alarm_mgr.send_alarm(context_data, alarm_data)
diff --git a/python/adapters/extensions/alarms/heartbeat_alarm.py b/python/adapters/extensions/alarms/heartbeat_alarm.py
new file mode 100644
index 0000000..4f5f4f4
--- /dev/null
+++ b/python/adapters/extensions/alarms/heartbeat_alarm.py
@@ -0,0 +1,28 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from adapter_alarms import AlarmBase
+
+
+class HeartbeatAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, object_type='olt', heartbeat_misses=0):
+ super(HeartbeatAlarm, self).__init__(alarm_mgr, object_type,
+ alarm='Heartbeat',
+ alarm_category=AlarmEventCategory.PON,
+ alarm_type=AlarmEventType.EQUIPMENT,
+ alarm_severity=AlarmEventSeverity.CRITICAL)
+ self._misses = heartbeat_misses
+
+ def get_context_data(self):
+ return {'heartbeats-missed': self._misses}
diff --git a/python/adapters/extensions/alarms/olt/__init__.py b/python/adapters/extensions/alarms/olt/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/alarms/olt/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/alarms/olt/olt_los_alarm.py b/python/adapters/extensions/alarms/olt/olt_los_alarm.py
new file mode 100644
index 0000000..c8666a3
--- /dev/null
+++ b/python/adapters/extensions/alarms/olt/olt_los_alarm.py
@@ -0,0 +1,32 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OltLosAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, intf_id, port_type_name):
+ super(OltLosAlarm, self).__init__(alarm_mgr, object_type='olt LOS',
+ alarm='OLT_LOS',
+ alarm_category=AlarmEventCategory.OLT,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ # Added port type to indicate if alarm was on NNI or PON
+ self._intf_id = intf_id
+ self._port_type_name = port_type_name
+
+ def get_context_data(self):
+ return {'olt-intf-id:': self._intf_id,
+ 'olt-port-type-name': self._port_type_name}
diff --git a/python/adapters/extensions/alarms/onu/__init__.py b/python/adapters/extensions/alarms/onu/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/alarms/onu/onu_activation_fail_alarm.py b/python/adapters/extensions/alarms/onu/onu_activation_fail_alarm.py
new file mode 100644
index 0000000..2bf054e
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_activation_fail_alarm.py
@@ -0,0 +1,30 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuActivationFailAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuActivationFailAlarm, self).__init__(alarm_mgr, object_type='onu ACTIVATION FAIL',
+ alarm='ONU_ACTIVATION_FAIL',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_active_alarm.py b/python/adapters/extensions/alarms/onu/onu_active_alarm.py
new file mode 100644
index 0000000..a139875
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_active_alarm.py
@@ -0,0 +1,50 @@
+# 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.extensions.alarms.adapter_alarms import AlarmBase
+from voltha.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+
+
+class OnuActiveAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, device_id, pon_id, onu_serial_number,
+ reg_id, olt_serial_number, ipv4_address=None):
+ super(OnuActiveAlarm, self).__init__(alarm_mgr, object_type='ONU',
+ alarm='ONU_ACTIVATED',
+ alarm_category=AlarmEventCategory.PON,
+ resource_id=pon_id,
+ alarm_type=AlarmEventType.EQUIPMENT,
+ alarm_severity=AlarmEventSeverity.CRITICAL)
+ self._pon_id = pon_id
+ self._onu_serial_number = onu_serial_number
+ self._device_id = device_id
+ self._olt_serial_number = olt_serial_number
+ self._host = ipv4_address
+ self._reg_id = reg_id
+
+ def get_context_data(self):
+ data = {
+ 'pon-id': self._pon_id,
+ 'serial-number': self._onu_serial_number,
+ 'olt_serial_number': self._olt_serial_number,
+ 'device_id': self._device_id,
+ 'registration_id': self._reg_id
+ }
+ if self._host is not None:
+ data['host'] = self._host
+
+ return data
+
+ def clear_alarm(self):
+ raise NotImplementedError('ONU Active Alarms are auto-clear')
+
diff --git a/python/adapters/extensions/alarms/onu/onu_discovery_alarm.py b/python/adapters/extensions/alarms/onu/onu_discovery_alarm.py
new file mode 100644
index 0000000..c7da2bc
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_discovery_alarm.py
@@ -0,0 +1,36 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuDiscoveryAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, pon_id, serial_number):
+ super(OnuDiscoveryAlarm, self).__init__(alarm_mgr, object_type='ONU Discovery',
+ alarm='ONU_DISCOVERY',
+ alarm_category=AlarmEventCategory.PON,
+ resource_id=pon_id,
+ alarm_type=AlarmEventType.EQUIPMENT,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._pon_id = pon_id
+ self._serial_number = serial_number
+
+ def get_context_data(self):
+ return {
+ 'pon-id': self._pon_id,
+ 'serial-number': self._serial_number
+ }
+
+ def clear_alarm(self):
+ raise NotImplementedError('ONU Discovery Alarms are auto-clear')
diff --git a/python/adapters/extensions/alarms/onu/onu_dying_gasp_alarm.py b/python/adapters/extensions/alarms/onu/onu_dying_gasp_alarm.py
new file mode 100644
index 0000000..52b6850
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_dying_gasp_alarm.py
@@ -0,0 +1,33 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuDyingGaspAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuDyingGaspAlarm, self).__init__(alarm_mgr, object_type='onu DYING_GASP',
+ alarm='ONU_DYING_GASP',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {
+ 'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id
+ }
diff --git a/python/adapters/extensions/alarms/onu/onu_equipment_alarm.py b/python/adapters/extensions/alarms/onu/onu_equipment_alarm.py
new file mode 100644
index 0000000..e7e3a7a
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_equipment_alarm.py
@@ -0,0 +1,45 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuEquipmentAlarm(AlarmBase):
+ """
+ The ONU Equipment Alarm is reported by both the CircuitPack (ME #6) and
+ the ONT-G (ME # 256) to indicate failure on an internal interface or
+ failed self-test.
+
+ For CircuitPack equipment alarms, the intf_id reported is that of the
+ UNI's logical port number
+
+ For ONT-G equipment alarms, the intf_id reported is that of the PON/ANI
+ physical port number
+
+ Note: Some ONUs may use this alarm to report a self-test failure or may
+ may report it with a different alarm number specifically for a
+ self-test failure.
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuEquipmentAlarm, self).__init__(alarm_mgr, object_type='onu equipment',
+ alarm='ONU_EQUIPMENT',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.EQUIPTMENT,
+ alarm_severity=AlarmEventSeverity.CRITICAL)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_high_rx_optical_power_alarm.py b/python/adapters/extensions/alarms/onu/onu_high_rx_optical_power_alarm.py
new file mode 100644
index 0000000..7b59d55
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_high_rx_optical_power_alarm.py
@@ -0,0 +1,37 @@
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from voltha.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuHighRxOpticalAlarm(AlarmBase):
+ """
+ The ONU High Tx Optical Power Alarm is reported by the ANI-G (ME # 263) to
+ indicate that the received downstream optical power above threshold..
+
+ For ANI-G equipment alarms, the intf_id reported is that of the PON/ANI
+ physical port number
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuHighRxOpticalAlarm, self).__init__(alarm_mgr, object_type='onu high rx optical power',
+ alarm='ONU_HIGH_RX_OPTICAL',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_high_tx_optical_power_alarm.py b/python/adapters/extensions/alarms/onu/onu_high_tx_optical_power_alarm.py
new file mode 100644
index 0000000..64caefe
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_high_tx_optical_power_alarm.py
@@ -0,0 +1,37 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuHighTxOpticalAlarm(AlarmBase):
+ """
+ The ONU High Tx Optical Power Alarm is reported by the ANI-G (ME # 263) to
+ indicate that the received downstream optical power above upper threshold.
+
+ For ANI-G equipment alarms, the intf_id reported is that of the PON/ANI
+ physical port number
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuHighTxOpticalAlarm, self).__init__(alarm_mgr, object_type='onu high tx optical power',
+ alarm='ONU_HIGH_TX_OPTICAL',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
\ No newline at end of file
diff --git a/python/adapters/extensions/alarms/onu/onu_laser_bias_current_alarm.py b/python/adapters/extensions/alarms/onu/onu_laser_bias_current_alarm.py
new file mode 100644
index 0000000..8daf5a6
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_laser_bias_current_alarm.py
@@ -0,0 +1,38 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuLaserBiasAlarm(AlarmBase):
+ """
+ The ONU Laser Bias Current Alarm is reported by the ANI-G (ME # 263) to
+ indicate that the laser bias current above threshold determined by
+ vendor and that laser end of life is pending
+
+ For ANI-G equipment alarms, the intf_id reported is that of the PON/ANI
+ physical port number
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuLaserBiasAlarm, self).__init__(alarm_mgr, object_type='onu laser bias current',
+ alarm='ONU_LASER_BIAS_CURRENT',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.EQUIPTMENT,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_laser_eol_alarm.py b/python/adapters/extensions/alarms/onu/onu_laser_eol_alarm.py
new file mode 100644
index 0000000..fa5039c
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_laser_eol_alarm.py
@@ -0,0 +1,36 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuLaserEolAlarm(AlarmBase):
+ """
+ The ONU Laser End-of-Lifer Alarm is reported by both the CircuitPack (ME #6)
+ to indicate that failure of transmit laser imminent
+
+ The intf_id reported is that of the UNI's logical port number
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuLaserEolAlarm, self).__init__(alarm_mgr, object_type='onu laser EOL',
+ alarm='ONU_LASER_EOL',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.EQUIPTMENT,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_lob_alarm.py b/python/adapters/extensions/alarms/onu/onu_lob_alarm.py
new file mode 100644
index 0000000..e595211
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_lob_alarm.py
@@ -0,0 +1,30 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuLobAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuLobAlarm, self).__init__(alarm_mgr, object_type='onu LOB',
+ alarm='ONU_LOB',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_lopc_mic_error_alarm.py b/python/adapters/extensions/alarms/onu/onu_lopc_mic_error_alarm.py
new file mode 100644
index 0000000..cc05cb0
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_lopc_mic_error_alarm.py
@@ -0,0 +1,33 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuLopcMicErrorAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuLopcMicErrorAlarm, self).__init__(alarm_mgr, object_type='onu LOPC_MIC_ERROR',
+ alarm='ONU_LOPC_MIC_ERROR',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {
+ 'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id
+ }
diff --git a/python/adapters/extensions/alarms/onu/onu_lopc_miss_alarm.py b/python/adapters/extensions/alarms/onu/onu_lopc_miss_alarm.py
new file mode 100644
index 0000000..af695ca
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_lopc_miss_alarm.py
@@ -0,0 +1,33 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuLopcMissAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuLopcMissAlarm, self).__init__(alarm_mgr, object_type='onu LOPC_MISS',
+ alarm='ONU_LOPC_MISS',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {
+ 'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id
+ }
diff --git a/python/adapters/extensions/alarms/onu/onu_los_alarm.py b/python/adapters/extensions/alarms/onu/onu_los_alarm.py
new file mode 100644
index 0000000..d2ebb7f
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_los_alarm.py
@@ -0,0 +1,30 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuLosAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuLosAlarm, self).__init__(alarm_mgr, object_type='onu LOS',
+ alarm='ONU_LOS',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_low_rx_optical_power_alarm.py b/python/adapters/extensions/alarms/onu/onu_low_rx_optical_power_alarm.py
new file mode 100644
index 0000000..ee6f4d2
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_low_rx_optical_power_alarm.py
@@ -0,0 +1,37 @@
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from voltha.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuLowRxOpticalAlarm(AlarmBase):
+ """
+ The ONU Low Rx Optical Power Alarm is reported by the ANI-G (ME # 263) to
+ indicate that the received downstream optical power below threshold.
+
+ For ANI-G equipment alarms, the intf_id reported is that of the PON/ANI
+ physical port number
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuLowRxOpticalAlarm, self).__init__(alarm_mgr, object_type='onu low rx optical power',
+ alarm='ONU_LOW_RX_OPTICAL',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_low_tx_optical_power_alarm.py b/python/adapters/extensions/alarms/onu/onu_low_tx_optical_power_alarm.py
new file mode 100644
index 0000000..e28a556
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_low_tx_optical_power_alarm.py
@@ -0,0 +1,37 @@
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from voltha.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuLowTxOpticalAlarm(AlarmBase):
+ """
+ The ONU Low Tx Optical Power Alarm is reported by the ANI-G (ME # 263) to
+ indicate that the transmit optical power below lower threshold
+
+ For ANI-G equipment alarms, the intf_id reported is that of the PON/ANI
+ physical port number
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuLowTxOpticalAlarm, self).__init__(alarm_mgr, object_type='onu low tx optical power',
+ alarm='ONU_LOW_TX_OPTICAL',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_selftest_failure_alarm.py b/python/adapters/extensions/alarms/onu/onu_selftest_failure_alarm.py
new file mode 100644
index 0000000..c742762
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_selftest_failure_alarm.py
@@ -0,0 +1,44 @@
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from voltha.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuSelfTestFailureAlarm(AlarmBase):
+ """
+ The ONU Self Test Failure Alarm is reported by both the CircuitPack (ME #6)
+ and the ONT-G (ME # 256) to indicate failure a failed autonomous self-test.
+
+ For CircuitPack equipment alarms, the intf_id reported is that of the
+ UNI's logical port number
+
+ For ONT-G equipment alarms, the intf_id reported is that of the PON/ANI
+ physical port number
+
+ Note: Some ONUs may use this alarm to report a self-test failure or may
+ may report it with the ONU Equipment Alarm which can also cover a
+ self-test failure.
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuSelfTestFailureAlarm, self).__init__(alarm_mgr, object_type='onu self-test failure',
+ alarm='ONU_SELF_TEST_FAIL',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.EQUIPTMENT,
+ alarm_severity=AlarmEventSeverity.CRITICAL)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_signal_degrade_alarm.py b/python/adapters/extensions/alarms/onu/onu_signal_degrade_alarm.py
new file mode 100644
index 0000000..4861f75
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_signal_degrade_alarm.py
@@ -0,0 +1,33 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuSignalDegradeAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, onu_id, intf_id,
+ inverse_bit_error_rate):
+ super(OnuSignalDegradeAlarm, self).__init__(alarm_mgr, object_type='onu SIGNAL DEGRADE',
+ alarm='ONU_SIGNAL_DEGRADE',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+ self._inverse_bit_error_rate=inverse_bit_error_rate
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id,
+ 'inverse-bit-error-rate': self._inverse_bit_error_rate}
diff --git a/python/adapters/extensions/alarms/onu/onu_signal_fail_alarm.py b/python/adapters/extensions/alarms/onu/onu_signal_fail_alarm.py
new file mode 100644
index 0000000..bcc629a
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_signal_fail_alarm.py
@@ -0,0 +1,39 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+"""
+ OnuSignalsFailureIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+ fixed32 inverse_bit_error_rate = 4;
+"""
+
+class OnuSignalFailAlarm(AlarmBase):
+ def __init__(self, alarm_mgr, onu_id, intf_id, inverse_bit_error_rate):
+ super(OnuSignalFailAlarm, self).__init__(alarm_mgr, object_type='onu SIGNAL FAIL',
+ alarm='ONU_SIGNAL_FAIL',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+ self._inverse_bit_error_rate = inverse_bit_error_rate
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id,
+ 'inverse-bit-error-rate': self._inverse_bit_error_rate}
diff --git a/python/adapters/extensions/alarms/onu/onu_startup_alarm.py b/python/adapters/extensions/alarms/onu/onu_startup_alarm.py
new file mode 100644
index 0000000..9960f03
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_startup_alarm.py
@@ -0,0 +1,39 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+"""
+ message OnuStartupFailureIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+}
+
+"""
+
+class OnuStartupAlarm(AlarmBase):
+
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuStartupAlarm, self).__init__(alarm_mgr, object_type='onu STARTUP FAIL',
+ alarm='ONU_STARTUP_FAIL',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_temp_red_alarm.py b/python/adapters/extensions/alarms/onu/onu_temp_red_alarm.py
new file mode 100644
index 0000000..bfa1623
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_temp_red_alarm.py
@@ -0,0 +1,42 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuTempRedAlarm(AlarmBase):
+ """
+ The ONU Temperature Yellow Alarm is reported by both the CircuitPack
+ (ME #6) and the ONT-G (ME # 256) to indicate no service has been shut
+ down to avoid equipment damage. The operational state of the affected
+ PPTPs indicates the affected services.
+
+ For CircuitPack equipment alarms, the intf_id reported is that of the
+ UNI's logical port number
+
+ For ONT-G equipment alarms, the intf_id reported is that of the PON/ANI
+ physical port number
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuTempRedAlarm, self).__init__(alarm_mgr, object_type='onu temperature red',
+ alarm='ONU_TEMP_RED',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.ENVIRONMENT,
+ alarm_severity=AlarmEventSeverity.CRITICAL)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_temp_yellow_alarm.py b/python/adapters/extensions/alarms/onu/onu_temp_yellow_alarm.py
new file mode 100644
index 0000000..7a28f81
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_temp_yellow_alarm.py
@@ -0,0 +1,41 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuTempYellowAlarm(AlarmBase):
+ """
+ The ONU Temperature Yellow Alarm is reported by both the CircuitPack
+ (ME #6) and the ONT-G (ME # 256) to indicate no service shutdown at
+ present, but the circuit pack is operating beyond its recommended range.
+
+ For CircuitPack equipment alarms, the intf_id reported is that of the
+ UNI's logical port number
+
+ For ONT-G equipment alarms, the intf_id reported is that of the PON/ANI
+ physical port number
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuTempYellowAlarm, self).__init__(alarm_mgr, object_type='onu temperature yellow',
+ alarm='ONU_TEMP_YELLOW',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.ENVIRONMENT,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_voltage_red_alarm.py b/python/adapters/extensions/alarms/onu/onu_voltage_red_alarm.py
new file mode 100644
index 0000000..506351c
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_voltage_red_alarm.py
@@ -0,0 +1,39 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuVoltageRedAlarm(AlarmBase):
+ """
+ The ONU Voltage Red Alarm is reported by the ONT-G (ME # 256) to
+ indicate some services have been shut down to avoid power collapse.
+ The operational state of the affected PPTPs indicates the affected
+ services.
+
+ For ONT-G equipment alarms, the intf_id reported is that of the PON/ANI
+ physical port number
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuVoltageRedAlarm, self).__init__(alarm_mgr, object_type='onu voltage red',
+ alarm='ONU_VOLTAGE_RED',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.CRITICAL)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_voltage_yellow_alarm.py b/python/adapters/extensions/alarms/onu/onu_voltage_yellow_alarm.py
new file mode 100644
index 0000000..1bb3ef6
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_voltage_yellow_alarm.py
@@ -0,0 +1,39 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuVoltageYellowAlarm(AlarmBase):
+ """
+ The ONU Voltage Red Alarm is reported by the ONT-G (ME # 256) to
+ indicate some services have been shut down to avoid power collapse.
+ The operational state of the affected PPTPs indicates the affected
+ services.
+
+ For ONT-G equipment alarms, the intf_id reported is that of the PON/ANI
+ physical port number
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id):
+ super(OnuVoltageYellowAlarm, self).__init__(alarm_mgr, object_type='onu voltage yellow',
+ alarm='ONU_VOLTAGE_YELLOW',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id}
diff --git a/python/adapters/extensions/alarms/onu/onu_window_drift_alarm.py b/python/adapters/extensions/alarms/onu/onu_window_drift_alarm.py
new file mode 100644
index 0000000..32d677d
--- /dev/null
+++ b/python/adapters/extensions/alarms/onu/onu_window_drift_alarm.py
@@ -0,0 +1,43 @@
+# 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.protos.events_pb2 import AlarmEventType, AlarmEventSeverity, AlarmEventCategory
+from voltha.extensions.alarms.adapter_alarms import AlarmBase
+
+
+class OnuWindowDriftAlarm(AlarmBase):
+ """
+ OnuDriftOfWindowIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+ fixed32 drift = 4;
+ fixed32 new_eqd = 5;
+ }
+ """
+ def __init__(self, alarm_mgr, onu_id, intf_id, drift, new_eqd):
+ super(OnuWindowDriftAlarm, self).__init__(alarm_mgr, object_type='onu WINDOW DRIFT',
+ alarm='ONU_WINDOW_DRIFT',
+ alarm_category=AlarmEventCategory.ONU,
+ alarm_type=AlarmEventType.COMMUNICATION,
+ alarm_severity=AlarmEventSeverity.MAJOR)
+ self._onu_id = onu_id
+ self._intf_id = intf_id
+ self._drift = drift
+ self._new_eqd = new_eqd
+
+ def get_context_data(self):
+ return {'onu-id': self._onu_id,
+ 'onu-intf-id': self._intf_id,
+ 'drift': self._drift,
+ 'new-eqd': self._new_eqd}
diff --git a/python/adapters/extensions/alarms/simulator/README.md b/python/adapters/extensions/alarms/simulator/README.md
new file mode 100644
index 0000000..1333ed7
--- /dev/null
+++ b/python/adapters/extensions/alarms/simulator/README.md
@@ -0,0 +1 @@
+TODO
diff --git a/python/adapters/extensions/alarms/simulator/__init__.py b/python/adapters/extensions/alarms/simulator/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/alarms/simulator/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/alarms/simulator/simulate_alarms.py b/python/adapters/extensions/alarms/simulator/simulate_alarms.py
new file mode 100644
index 0000000..4dfee37
--- /dev/null
+++ b/python/adapters/extensions/alarms/simulator/simulate_alarms.py
@@ -0,0 +1,77 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from voltha.extensions.alarms.olt.olt_los_alarm import OltLosAlarm
+from voltha.extensions.alarms.onu.onu_dying_gasp_alarm import OnuDyingGaspAlarm
+from voltha.extensions.alarms.onu.onu_los_alarm import OnuLosAlarm
+from voltha.extensions.alarms.onu.onu_lopc_miss_alarm import OnuLopcMissAlarm
+from voltha.extensions.alarms.onu.onu_lopc_mic_error_alarm import OnuLopcMicErrorAlarm
+from voltha.extensions.alarms.onu.onu_lob_alarm import OnuLobAlarm
+
+from voltha.extensions.alarms.onu.onu_startup_alarm import OnuStartupAlarm
+from voltha.extensions.alarms.onu.onu_signal_degrade_alarm import OnuSignalDegradeAlarm
+from voltha.extensions.alarms.onu.onu_signal_fail_alarm import OnuSignalFailAlarm
+from voltha.extensions.alarms.onu.onu_window_drift_alarm import OnuWindowDriftAlarm
+from voltha.extensions.alarms.onu.onu_activation_fail_alarm import OnuActivationFailAlarm
+
+from voltha.extensions.alarms.onu.onu_discovery_alarm import OnuDiscoveryAlarm
+
+class AdapterAlarmSimulator(object):
+ def __init__(self, adapter_alarms):
+ self.adapter_alarms = adapter_alarms
+
+ def simulate_alarm(self, alarm):
+ if alarm.indicator == "los":
+ alarm_obj = OltLosAlarm(self.adapter_alarms, intf_id=alarm.intf_id, port_type_name=alarm.port_type_name)
+ elif alarm.indicator == "dying_gasp":
+ alarm_obj = OnuDyingGaspAlarm(self.adapter_alarms, onu_id=alarm.onu_device_id, intf_id=alarm.intf_id)
+ elif alarm.indicator == "onu_los":
+ alarm_obj = OnuLosAlarm(self.adapter_alarms, onu_id=alarm.onu_device_id, intf_id=alarm.intf_id)
+ elif alarm.indicator == "onu_lopc_miss":
+ alarm_obj = OnuLopcMissAlarm(self.adapter_alarms, onu_id=alarm.onu_device_id, intf_id=alarm.intf_id)
+ elif alarm.indicator == "onu_lopc_mic":
+ alarm_obj = OnuLopcMicErrorAlarm(self.adapter_alarms, onu_id=alarm.onu_device_id, intf_id=alarm.intf_id)
+ elif alarm.indicator == "onu_lob":
+ alarm_obj = OnuLobAlarm(self.adapter_alarms, onu_id=alarm.onu_device_id, intf_id=alarm.intf_id)
+ elif alarm.indicator == "onu_startup":
+ alarm_obj = OnuStartupAlarm(self.adapter_alarms, intf_id=alarm.intf_id, onu_id=alarm.onu_device_id)
+ elif alarm.indicator == "onu_signal_degrade":
+ alarm_obj = OnuSignalDegradeAlarm(self.adapter_alarms, intf_id=alarm.intf_id, onu_id=alarm.onu_device_id,
+ inverse_bit_error_rate=alarm.inverse_bit_error_rate)
+ elif alarm.indicator == "onu_drift_of_window":
+ alarm_obj = OnuWindowDriftAlarm(self.adapter_alarms, intf_id=alarm.intf_id,
+ onu_id=alarm.onu_device_id,
+ drift=alarm.drift,
+ new_eqd=alarm.new_eqd)
+ elif alarm.indicator == "onu_signal_fail":
+ alarm_obj = OnuSignalFailAlarm(self.adapter_alarms, intf_id=alarm.intf_id,
+ onu_id=alarm.onu_device_id,
+ inverse_bit_error_rate=alarm.inverse_bit_error_rate)
+ elif alarm.indicator == "onu_activation":
+ alarm_obj = OnuActivationFailAlarm(self.adapter_alarms, intf_id=alarm.intf_id,
+ onu_id=alarm.onu_device_id)
+ elif alarm.indicator == "onu_discovery":
+ alarm_obj = OnuDiscoveryAlarm(self.adapter_alarms, pon_id=alarm.intf_id,
+ serial_number=alarm.onu_serial_number)
+ else:
+ raise Exception("Unknown alarm indicator %s" % alarm.indicator)
+
+ if alarm.operation == alarm.RAISE:
+ alarm_obj.raise_alarm()
+ elif alarm.operation == alarm.CLEAR:
+ alarm_obj.clear_alarm()
+ else:
+ # This shouldn't happen
+ raise Exception("Unknown alarm operation")
diff --git a/python/adapters/extensions/kpi/README.md b/python/adapters/extensions/kpi/README.md
new file mode 100644
index 0000000..a55c7d2
--- /dev/null
+++ b/python/adapters/extensions/kpi/README.md
@@ -0,0 +1,286 @@
+# VOLTHA Performance Monitoring/KPI Library
+
+This directory provides a common library for the creation of Performance Monitoring groups
+within VOLTHA and should be used to insure that KPI information from different adapters use
+the same format.
+
+The original KpiEvent protobuf message is still supported for adapters that wish to use theprevious format but device adapter developers are encouraged to support the new format and
+make use of this shared library.
+
+**Also**, please read the **Remaining Work Item** sections of each README.md file. Some additional
+work items as well as existing/related JIRA items are highlighted in this section.
+
+## KPI Manager Creation
+
+Currently, each device adapter is required to follow the following steps to create and
+register PM Metric manager. This is typically performed in the device handler's
+'activate' method (called in response to the device handler first being enabled)
+
+1. Create an instance of a derived **AdapterPmMetrics** manager object. This is currently an
+ **OltPmMetrics** object for an _OLT_ adapter, or an **OnuPmMetrics** adapter for an
+ _ONU_ adapter. If you have additional device specific metrics to report, you can
+ derive your own manager object from one of these two derived classes. In order to
+ inherit (or modify) the metrics defined in those classes as well as support any new
+ metrics specific to your device.
+
+ This call takes a number of device adapter specific arguments and these are detailed
+ in the pydoc headers for the appropriate **AdapterPmMetrics** _\_\_init___() method.
+
+2. Create the ProtoBuf message for your metrics by calling the newly created _manager's_
+ **_make_proto_**() method.
+
+3. Register the ProtoBuf message configuration with the adapter agent via the
+ _update_device_pm_config_() method with the optional init parameter set to **True**.
+
+4. Request the manager to schedule the first PM collection interval by calling the
+ manager's _start_collector_() method. You may wish to do this after a short pause
+ depending on how your adapter is designed.
+
+**NOTE:** Currently there is only a single collection frequency for all metrics for
+a given device adapter. In the future, individual collection intervals on a per-metric/metric-group
+will be supported by the shared library.
+
+The next two subsections provides examples of these steps for both an OLT and an ONU
+device adapter
+
+### OLT Device Adapters PM Manager setup
+
+```python
+ # Create the OLT PM Manager object
+ kwargs = {
+ 'nni-ports': self.northbound_ports.values(),
+ 'pon-ports': self.southbound_ports.values()
+ }
+ self.pm_metrics = OltPmMetrics(self.adapter_agent, self.device_id, self.logical_device_id,
+ grouped=True, freq_override=False,
+ **kwargs)
+
+ # Create the protobuf message configuration
+ pm_config = self.pm_metrics.make_proto()
+ self.log.debug("initial-pm-config", pm_config=pm_config)
+
+ # Create the PM information in the adapter agent
+ self.adapter_agent.update_device_pm_config(pm_config, init=True)
+
+ # Start collecting stats from the device after a brief pause
+ reactor.callLater(10, self.pm_metrics.start_collector)
+```
+
+### ONU Device Adapters PM Manager Setup
+
+For ONU devices, if you wish to include OpenOMCI 15-minute historical interval
+intervals, you will need to register the PM Metrics OpenOMCI Interval PM class
+with OpenOMCI. This ties in the OpenOMCI PM Interval State Machine with the KPI
+shared library.
+
+```python
+
+ # Create the OLT PM Manager object
+ kwargs = {
+ 'heartbeat': self.heartbeat,
+ 'omci-cc': self.openomci.omci_cc
+ }
+ self.pm_metrics = OnuPmMetrics(self.adapter_agent, self.device_id, self.logical_device_id,
+ grouped=True, freq_override=False,
+ **kwargs)
+
+ # Create the protobuf message configuration
+ pm_config = self.pm_metrics.make_proto()
+
+ # Register the OMCI history intervals with OpenOMCI
+ self.openomci.set_pm_config(self.pm_metrics.omci_pm.openomci_interval_pm)
+
+ # Create the PM information in the adapter agent
+ self.adapter_agent.update_device_pm_config(pm_config, init=True)
+
+ # Start collecting stats from the device after a brief pause
+ reactor.callLater(30, self.pm_metrics.start_collector)
+```
+
+### How metrics are currently collected
+
+Currently, the default behaviour is to collect KPI information on a single periodic
+interval that can be adjusted via the NBI/CLI of VOLTHA. It collects data by extracting
+it from an object provided during the collection request and this object should either
+provide attributes or a property method that matches the metric to be collected.
+For instance, assume that you have an NNI metric called 'tx_packets'. You would pass
+an object during collection that should have one of the two following;
+
+- a _tx_packets_ attribute/member name defined for the object that has the requested
+ value already set (via background poll)
+
+- a _tx_packets_ **property** method that accesses an internal variable with the value
+ already set (via background poll) or that calculates/extracts the value without blockin
+ the call.
+
+### Known Issues in collection
+
+Note that a future story will be created to allow for collection to be requested for
+a metric/metric-group on demand so that background polling of KPI information is not
+required for all reported metrics.
+
+Note that a future story will be created to allow KPI information to be collected on
+per-group/metric intervals.
+
+# Basic KPI Format (**KpiEvent2**)
+
+The KPI information is published on the kafka bus under the _voltha.kpi_ topic. For
+VOLTHA PM information, the kafka key is empty and the value is a JSON message composed
+of the following key-value pairs.
+
+| key | value | Notes |
+| :--------: | :----- | :---- |
+| type | string | "slice" or "ts". A "slice" is a set of path/metric data for the same time-stamp. A "ts" is a time-series: array of data for same metric |
+| ts | float | UTC time-stamp of when the KpiEvent2 was created (seconds since the epoch of January 1, 1970) |
+| slice_data | list | One or more sets of metrics composed of a _metadata_ section and a _metrics_ section. |
+
+**NOTE**: Time-series metrics and corresponding protobuf messages have not been defined.
+
+## Slice Data Format
+
+For KPI slice KPI messages, the _slice_data_ portion of the **KpiEvent2** is composed of a _metadata_
+section and a _metrics_ section.
+
+### _metadata_ Section Format
+
+The metadata section is used to:
+ - Define which metric/metric-group is being reported (The _title_ field)
+ - Provide some common fields required by all metrics (_title_, _timestamp_, _device ID_, ...)
+ - Provide metric/metric-group specific context (the _context_ fields)
+
+| key | value | Notes |
+| :--------: | :----- | :---- |
+| title | string | "slice" or "ts". A "slice" is a set of path/metric data for the same time-stamp. A "ts" is a time-series: array of data for same metric |
+| ts | float | UTC time-stamp of data at the time of collection (seconds since the epoch of January 1, 1970) |
+| logical_device_id | string | The logical ID that the device belongs to. This is equivalent to the DPID reported in ONOS for the VOLTHA logical device with the 'of:' prefix removed. |
+| device_id | string | The physical device ID that is reporting the metric. |
+| serial_number | string | The reported serial number for the physical device reporting the metric. |
+| context | map | A key-value map of metric/metric-group specific information.|
+
+The context map is composed of key-value pairs where the key (string) is the label for the context
+specific value and the value (string) is the corresponding context value. While most values may be
+better represented as a float/integer, there may be some that are better represented as text. For
+this reason, values are always represented as strings to allow the ProtoBuf message format to be
+as simple as possible.
+
+Here is an JSON _example_ of a current KPI published on the kafka bus under the
+_voltha.kpi_ topic.
+
+```json
+{
+ "type": "slice",
+ "ts": 1534440704.0,
+ "slice_data": [
+ {
+ "metadata": {
+ "title": "Ethernet",
+ "ts": 1534440704.0,
+ "logical_device_id": "000000139521a269",
+ "device_id": "000115929ed71696",
+ "serial_no": "dummy_sn2209199",
+ "context": {
+ "port_no": "1"
+ }
+ },
+ "metrics": {
+ "tx_dropped": 0.0, # A COUNTER
+ "rx_packets": 0.0,
+ "rx_bytes": 0.0,
+ "rx_mcast_packets": 0.0,
+ "tx_mcast_packets": 16.0,
+ "rx_bcast_packets": 0.0,
+ "oper_status": 4.0, # A STATE
+ "admin_state": 3.0,
+ "rx_errors": 0.0,
+ "tx_bytes": 1436.0,
+ "rx_dropped": 0.0,
+ "tx_packets": 16.0,
+ "tx_bcast": 0.0
+ }
+ },
+ {
+ "metadata": {
+ "title": "PON",
+ "logical_device_id": "000000139521a269",
+ "device_id": "000115929ed71696",
+ "serial_no": "dummy_sn2209199",
+ "ts": 1534440704.0,
+ "context": {
+ "port_no": "5",
+ "pon_id": "0"
+ },
+ },
+ "metrics": {
+ "rx_packets": 0.0,
+ "in_service_onus": 0.0, # A GAUGE
+ "rx_bytes": 0.0,
+ "closest_onu_distance": -1.0,
+ "tx_bip_errors": 0.0,
+ "oper_status": 4.0,
+ "admin_state": 3.0,
+ "tx_bytes": 0.0,
+ "tx_packets": 0.0
+ }
+ },
+ ...
+}
+
+```
+
+For OpenOMCI historical intervals, the name is derived from the Managed Entity class:
+
+```json
+{
+ "type": "slice",
+ "ts": 1532372864.0,
+ "prefixes": {
+ "voltha.adtran_onu.0001b8c505090b5b.EthernetFrameExtendedPerformanceMonitoring": {
+ "metrics": {
+ "entity_id": 2.0,
+ "class_id": 334.0,
+ "packets": 0.0,
+ "octets": 0.0,
+ "interval_end_time": 0.0,
+ "crc_errored_packets": 0.0,
+ "broadcast_packets": 0.0,
+ "64_octets": 0.0,
+ "65_to_127_octets": 0.0,
+ "128_to_255_octets": 0.0,
+ "256_to_511_octets": 0.0,
+ "undersize_packets": 0.0,
+ "drop_events": 0.0,
+ "multicast_packets": 0.0,
+ "oversize_packets": 0.0
+ }
+ }
+ }
+}
+```
+More information on the OpenOMCI ONU Historical Intervals is detailed in the _IntervalMetrics.md_
+file in the _onu/_ subdirectory.
+
+# Remaining Work Items
+
+This initial code is only a preliminary sample. The following tasks need to be
+added to the VOLTHA JIRA or performed in the SEBA group:
+
+- Get feedback from other OLT/ONU developers on any needed changes
+
+- Allow PM groups to have different collection times
+
+- Support calling a 'get-data' method before collect the metrics. Currently metrics are collected
+ in a device adapter independent way and the PM just updates what the attributes happen to have.
+ This would provide an asynchronous request and upon successful completion, the KPI metric/group
+ would be published on the Kafka bus.
+
+- [VOL-931](https://jira.opencord.org/browse/VOL-931) Support for retrieval of PM measurements
+ on-demaind. Would be best implemented after the previous async (get-data) work item.
+
+- For statistics groups that have more than one instance, do we need to be able to
+ enable/disable specific instances? Major refactor of code if so (database work, ...)
+
+- [VOL-930](https://jira.opencord.org/browse/VOL-930) PM Collection Format. This format may
+ fit better with the time-series KPI collection as it requests ability for start/stop times.
+ It could possibly be done at a higher layer but the intent may be to have a greater number
+ of samples on a specific metric instance for a defined period of time. Need clarification
+ from the JIRA author.
diff --git a/python/adapters/extensions/kpi/__init__.py b/python/adapters/extensions/kpi/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/kpi/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/kpi/adapter_pm_metrics.py b/python/adapters/extensions/kpi/adapter_pm_metrics.py
new file mode 100644
index 0000000..fe14fee
--- /dev/null
+++ b/python/adapters/extensions/kpi/adapter_pm_metrics.py
@@ -0,0 +1,217 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import structlog
+import arrow
+from twisted.internet.task import LoopingCall
+from voltha.protos.events_pb2 import KpiEvent2, KpiEventType, MetricInformation, MetricMetaData
+from voltha.protos.device_pb2 import PmConfig
+
+
+class AdapterPmMetrics(object):
+ """
+ Base class for Device Adapter PM Metrics Manager
+
+ Device specific (OLT, ONU, OpenOMCI, ...) will derive groups of PM information
+ and this base class is primarily used to provide a consistent interface to configure,
+ start, and stop statistics collection.
+ """
+ DEFAULT_FREQUENCY_KEY = 'default-collection-frequency'
+ DEFAULT_COLLECTION_FREQUENCY = 15 * 10 # 1/10ths of a second
+
+ # If the collection object has a property of the following name, it will be used
+ # to retrieve the UTC Collection Timestamp (UTC seconds since epoch). If the collection
+ # object does not support this attribute, the current time will be used. If the attribute
+ # is supported, but returns None, this signals that no metrics are currently available
+ # for collection.
+ TIMESTAMP_ATTRIBUTE = 'timestamp'
+
+ def __init__(self, adapter_agent, device_id, logical_device_id,
+ grouped=False, freq_override=False, **kwargs):
+ """
+ Initializer for shared Device Adapter PM metrics manager
+
+ :param adapter_agent: (AdapterAgent) Adapter agent for the device
+ :param device_id: (str) Device ID
+ :param logical_device_id: (str) VOLTHA Logical Device ID
+ :param grouped: (bool) Flag indicating if statistics are managed as a group
+ :param freq_override: (bool) Flag indicating if frequency collection can be specified
+ on a per group basis
+ :param kwargs: (dict) Device Adapter specific values
+ """
+ self.log = structlog.get_logger(device_id=device_id)
+ self.device_id = device_id
+ self.adapter_agent = adapter_agent
+ self.name = adapter_agent.adapter_name
+ # Sanitize the vcore ID in the logical device ID
+ self.logical_device_id = '0000' + logical_device_id[4:]
+ device = self.adapter_agent.get_device(self.device_id)
+ self.serial_number = device.serial_number
+
+ self.default_freq = kwargs.get(AdapterPmMetrics.DEFAULT_FREQUENCY_KEY,
+ AdapterPmMetrics.DEFAULT_COLLECTION_FREQUENCY)
+ self.grouped = grouped
+ self.freq_override = grouped and freq_override
+ self.lc = None
+ self.pm_group_metrics = dict() # name -> PmGroupConfig
+
+ def update(self, pm_config):
+ # TODO: Move any common steps into base class
+ raise NotImplementedError('Your derived class should override this method')
+
+ def make_proto(self, pm_config=None):
+ raise NotImplementedError('Your derived class should override this method')
+
+ def start_collector(self, callback=None):
+ """
+ Start the collection loop for an adapter if the frequency > 0
+
+ :param callback: (callable) Function to call to collect PM data
+ """
+ self.log.info("starting-pm-collection", device_name=self.name)
+ if callback is None:
+ callback = self.collect_and_publish_metrics
+
+ if self.lc is None:
+ self.lc = LoopingCall(callback)
+
+ if self.default_freq > 0:
+ self.lc.start(interval=self.default_freq / 10)
+
+ def stop_collector(self):
+ """ Stop the collection loop"""
+ if self.lc is not None and self.default_freq > 0:
+ self.lc.stop()
+
+ def collect_group_metrics(self, group_name, group, names, config):
+ """
+ Collect the metrics for a specific PM group.
+
+ This common collection method expects that the 'group object' provide as the second
+ parameter supports an attribute or property with the name of the value to
+ retrieve.
+
+ :param group_name: (str) The unique collection name. The name should not contain spaces.
+ :param group: (object) The object to query for the value of various attributes (PM names)
+ :param names: (set) A collection of PM names that, if implemented as a property in the object,
+ will return a value to store in the returned PM dictionary
+ :param config: (PMConfig) PM Configuration settings. The enabled flag is examined to determine
+ if the data associated with a PM Name will be collected.
+
+ :return: (MetricInformation) collected metrics
+ """
+ assert ' ' not in group_name, 'Spaces are not allowed in metric titles, use an underscore'
+
+ if group is None:
+ return None
+
+ metrics = dict()
+ context = dict()
+
+ now = getattr(group, AdapterPmMetrics.TIMESTAMP_ATTRIBUTE) \
+ if hasattr(group, AdapterPmMetrics.TIMESTAMP_ATTRIBUTE) \
+ else arrow.utcnow().float_timestamp
+
+ if now is None:
+ return None # No metrics available at this time for collection
+
+ for (metric, t) in names:
+ if config[metric].type == PmConfig.CONTEXT and hasattr(group, metric):
+ context[metric] = str(getattr(group, metric))
+
+ elif config[metric].type in (PmConfig.COUNTER, PmConfig.GAUGE, PmConfig.STATE):
+ if config[metric].enabled and hasattr(group, metric):
+ metrics[metric] = getattr(group, metric)
+
+ # Check length of metric data. Will be zero if if/when individual group
+ # metrics can be disabled and all are (or or not supported by the
+ # underlying adapter)
+ if len(metrics) == 0:
+ return None
+
+ return MetricInformation(metadata=MetricMetaData(title=group_name,
+ ts=now,
+ logical_device_id=self.logical_device_id,
+ serial_no=self.serial_number,
+ device_id=self.device_id,
+ context=context),
+ metrics=metrics)
+
+ def collect_metrics(self, data=None):
+ """
+ Collect metrics for this adapter.
+
+ The adapter type (OLT, ONU, ..) should provide a derived class where this
+ method iterates through all metrics and collects them up in a dictionary with
+ the group/metric name as the key, and the metric values as the contents.
+
+ The data collected (or passed in) is a list of pairs/tuples. Each
+ pair is composed of a MetricMetaData metadata-portion and list of MetricValuePairs
+ that contains a single individual metric or list of metrics if this is a
+ group metric.
+
+ This method is called for each adapter at a fixed frequency.
+ TODO: Currently all group metrics are collected on a single timer tick.
+ This needs to be fixed as independent group or instance collection is
+ desirable.
+
+ :param data: (list) Existing list of collected metrics (MetricInformation).
+ This is provided to allow derived classes to call into
+ further encapsulated classes.
+
+ :return: (list) metadata and metrics pairs - see description above
+ """
+ raise NotImplementedError('Your derived class should override this method')
+
+ def collect_and_publish_metrics(self):
+ """ Request collection of all enabled metrics and publish them """
+ try:
+ data = self.collect_metrics()
+ self.publish_metrics(data)
+
+ except Exception as e:
+ self.log.exception('failed-to-collect-kpis', e=e)
+
+ def publish_metrics(self, data):
+ """
+ Publish the metrics during a collection.
+
+ The data collected (or passed in) is a list of dictionary pairs/tuple. Each
+ pair is composed of a metadata-portion and a metrics-portion that contains
+ information for a specific instance of an individual metric or metric group.
+
+ :param data: (list) Existing list of collected metrics (MetricInformation)
+ to convert to a KPIEvent and publish
+ """
+ self.log.debug('publish-metrics')
+
+ if len(data):
+ try:
+ # TODO: Existing adapters use the KpiEvent, if/when all existing
+ # adapters use the shared KPI library, we may want to
+ # deprecate the KPIEvent
+ kpi_event = KpiEvent2(
+ type=KpiEventType.slice,
+ ts=arrow.utcnow().float_timestamp,
+ slice_data=data
+ )
+ self.adapter_agent.submit_kpis(kpi_event)
+
+ except Exception as e:
+ self.log.exception('failed-to-submit-kpis', e=e)
+
+ # TODO: Need to support on-demand counter update if provided by the PM 'group'.
+ # Currently we expect PM data to be periodically polled by a separate
+ # mechanism. The on-demand counter update should be optional in case the
+ # particular device adapter group of data is polled anyway for other reasons.
diff --git a/python/adapters/extensions/kpi/olt/README.md b/python/adapters/extensions/kpi/olt/README.md
new file mode 100644
index 0000000..e0012b8
--- /dev/null
+++ b/python/adapters/extensions/kpi/olt/README.md
@@ -0,0 +1,179 @@
+# OLT PM Metrics
+
+
+**THESE ARE PRELIMINARY METRIC GROUPS**, Work is needed by the VOLTHA community to reach a consensus on the
+actual metrics that will be provided. **Also**, please read the **Remaining Work Item** sections of each
+README file.
+
+
+
+This document outlines the metrics reported by VOLTHA OLTs. These are currently collected
+from OLT Device Adapter which is responsible for polling the hardware for information. A future
+version of the Performance Monitoring Library will allow for collection on-demand.
+
+## Format on the Kafka bus
+
+The format of the OLT KPI Events is detailed in the [Basic KPI Format (**KpiEvent2**)](../README.md)
+section of this documents parent directory for wire format on the bus. This document primarily provides
+the group metric information for OLT PKIs and associated metadata context information.
+
+**All** metric values reported by the library are reported as *float*s. The context and metric tables
+listed in the sections below report the type as initially collected by the OLT Device Adapters.
+
+#OLT PM Metric Groups
+
+The following sections outline the KPI metrics gathered by most OLT Device adapters. If an OLT does not
+support a specific metric in a group, it will not report that metric. This is preferred to reporting a
+metric and it always having a value of 0.0 (which could be misleading).
+
+## Admin and Oper State/status
+
+Various interfaces will provide a numeric (integer) value for the current Admin State and Operation
+Status of the interface. These map to the following states:
+
+**Admin State**
+
+| State | Value | Notes |
+| ----------------: | :---: | :---- |
+| UNKNOWN | 0 | The administrative state of the device is unknown |
+| DISABLED | 2 | The device is disabled and shall not perform its intended forwarding functions other than being available for re-activation. |
+| PREPROVISIONED | 1 | The device is pre-provisioned into Voltha, but not contacted by it |
+| ENABLED | 3 | The device is enabled for activation and operation |
+| DOWNLOADING_IMAGE | 4 | The device is in the state of image download |
+
+**Operational Status**
+
+| State | Value | Notes |
+| ---------: | :---: | :---- |
+| UNKNOWN | 0 | The status of the device is unknown at this point |
+| DISCOVERED | 1 | The device has been discovered, but not yet activated |
+| ACTIVATING | 2 | The device is being activated (booted, rebooted, upgraded, etc.) |
+| TESTING | 3 | Service impacting tests are being conducted |
+| ACTIVE | 4 | The device is up and active |
+| FAILED | 5 | The device has failed and cannot fulfill its intended role |
+
+## NNI KPI Metrics
+
+This metric provides metrics for a specific NNI Port of an OLT
+
+**Metadata Context items**
+
+| key | value | Notes |
+| :---------: | :------ | :---- |
+| intf_id | integer | Physical device interface port number for this NNI port |
+
+**Metrics**
+
+| key | type / size | Notes |
+| :--------------: | :----------- | :---- |
+| admin_state | state | See _Admin State_ section above |
+| oper_status | state | See _Operational Status_ section above |
+| rx_bytes | int, 64-bits | TODO: add definition here... |
+| rx_packets | int, 64-bits | TODO: add definition here... |
+| rx_ucast_packets | int, 64-bits | TODO: add definition here... |
+| rx_mcast_packets | int, 64-bits | TODO: add definition here... |
+| rx_bcast_packets | int, 64-bits | TODO: add definition here... |
+| rx_error_packets | int, 64-bits | TODO: add definition here... |
+| tx_bytes | int, 64-bits | TODO: add definition here... |
+| tx_packets | int, 64-bits | TODO: add definition here... |
+| tx_ucast_packets | int, 64-bits | TODO: add definition here... |
+| tx_mcast_packets | int, 64-bits | TODO: add definition here... |
+| tx_bcast_packets | int, 64-bits | TODO: add definition here... |
+| tx_error_packets | int, 64-bits | TODO: add definition here... |
+| rx_crc_errors | int, 64-bits | TODO: add definition here... |
+| bip_errors | int, 64-bits | TODO: add definition here... |
+
+## PON KPI Metrics
+
+The OLT PON Port metrics
+
+**Metadata Context items**
+
+| key | value | Notes |
+| :---------: | :------ | :---- |
+| intf_id | integer | Physical device interface port number for this NNI port |
+| pon_id | integer | PON ID (0..n) |
+
+**Metrics**
+
+| key | type / size | Notes |
+| :------------------: | :----------- | :---- |
+| admin_state | state | See _Admin State_ section above |
+| oper_status | state | See _Operational Status_ section above |
+| rx_packets | int, 64-bits | Sum of all the RX Packets of GEM ports that are not base TCONT's |
+| rx_bytes | int, 64-bits | Sum of all the RX Octets of GEM ports that are not base TCONT's |
+| tx_packets | int, 64-bits | Sum of all the TX Packets of GEM ports that are not base TCONT's |
+| tx_bytes | int, 64-bits | Sum of all the TX Octets of GEM ports that are not base TCONT's |
+| tx_bip_errors | int, 32-bits | Sum of all the TX ONU bip errors to get TX BIP's per PON |
+| in_service_onus | int | The number of activated ONUs on this pon |
+| closest_onu_distance | float | Distance to the closest ONU, units=kM w/granularity in the thousandths |
+
+## ONU KPI Metrics
+
+The OLT metrics for each activated ONUs
+
+**Metadata Context items**
+
+| key | value | Notes |
+| :---------: | :------ | :---- |
+| intf_id | integer | Physical device interface port number for this NNI port |
+| pon_id | integer | PON ID (0..n) |
+| onu_id | integer | ONU ID |
+
+**Metrics**
+
+| key | type / size | Notes |
+| :----------------: | :----------- | :---- |
+| fiber_length | float | Distance to ONU, units=kM w/granularity in the thousandths |
+| equalization_delay | int, 32-bits | Equalization delay |
+| rssi | int, 32-bits | The received signal strength indication of the ONU. |
+
+**TODO**: How about the following as well?
+ - rx_packets - int, 32-bits - Rx packets received on all GEM ports
+ - rx_bytes - int, 64-bits - Rx octets received on all GEM ports
+ - tx_packets - int, 32-bits - Tx packets transmitted on all GEM ports
+ - tx_bytes - int, 64-bits - Rx packets transmitted on all GEM ports
+ - tx_bip_errors - int, 32-bits - Sum of all the TX ONU bip errors to get TX BIP's on all GEM ports
+
+## GEM Port KPI Metrics
+
+The GEM Port metrics for each activated ONUs
+
+**Metadata Context items**
+
+| key | value | Notes |
+| :---------: | :------ | :---- |
+| intf_id | integer | Physical device interface port number for this NNI port |
+| pon_id | integer | PON ID (0..n) |
+| onu_id | integer | ONU ID |
+| gem_id | integer | GEM Port ID |
+
+**Metrics**
+
+| key | type / size | Notes |
+| :---------: | :----------- | :---- |
+| alloc_id | int, 16-bits | TODO: add definition here... |
+| rx_packets | int, 32-bits | Rx packets received |
+| rx_bytes | int, 64-bits | Rx octets received |
+| tx_packets | int, 32-bits | Tx packets transmitted |
+| tx_bytes | int, 64-bits | Rx packets transmitted |
+
+# Remaining Work Items
+
+This initial code is only a preliminary work. See the [Remaining Work Items](../README.md)
+section of this document's parent directory for a list of remaining tasks.
+
+- [VOL-932](https://jira.opencord.org/browse/VOL-932) PM Interval collection on the OLT. Need
+ to consult OLT device adapter vendors and operators for which KPIs would best fit in the
+ interval groups. Intervals differ from other metric groups as they are defined to collect on
+ a specific interval (15-minutes most common) and at the start of the interval, the counters
+ should be set to zero so that the accumulation during the interval is what is reported. See
+ also [VOL-933](https://jira.opencord.org/browse/VOL-932),
+ [VOL-934](https://jira.opencord.org/browse/VOL-934),
+ [VOL-935](https://jira.opencord.org/browse/VOL-935),
+ [VOL-938](https://jira.opencord.org/browse/VOL-938),
+ [VOL-939](https://jira.opencord.org/browse/VOL-939),
+ [VOL-940](https://jira.opencord.org/browse/VOL-940).
+ **NOTE**: A couple of the ones above are for the ONU
+
+TODO: For each group, list if the default is enabled/disabled
\ No newline at end of file
diff --git a/python/adapters/extensions/kpi/olt/__init__.py b/python/adapters/extensions/kpi/olt/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/kpi/olt/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/kpi/olt/olt_pm_metrics.py b/python/adapters/extensions/kpi/olt/olt_pm_metrics.py
new file mode 100644
index 0000000..ea2e0c8
--- /dev/null
+++ b/python/adapters/extensions/kpi/olt/olt_pm_metrics.py
@@ -0,0 +1,300 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from voltha.protos.device_pb2 import PmConfig, PmConfigs, PmGroupConfig
+from voltha.extensions.kpi.adapter_pm_metrics import AdapterPmMetrics
+
+
+class OltPmMetrics(AdapterPmMetrics):
+ """
+ Shared OL Device Adapter PM Metrics Manager
+
+ This class specifically addresses ONU general PM (health, ...) area
+ specific PM (OMCI, PON, UNI) is supported in encapsulated classes accessible
+ from this object
+ """
+ def __init__(self, adapter_agent, device_id, logical_device_id,
+ grouped=False, freq_override=False, **kwargs):
+ """
+ Initializer for shared ONU Device Adapter PM metrics
+
+ :param adapter_agent: (AdapterAgent) Adapter agent for the device
+ :param device_id: (str) Device ID
+ :param logical_device_id: (str) VOLTHA Logical Device ID
+ :param grouped: (bool) Flag indicating if statistics are managed as a group
+ :param freq_override: (bool) Flag indicating if frequency collection can be specified
+ on a per group basis
+ :param kwargs: (dict) Device Adapter specific values. For an ONU Device adapter, the
+ expected key-value pairs are listed below. If not provided, the
+ associated PM statistics are not gathered:
+
+ 'nni-ports': List of objects that provide NNI (northbound) port statistics
+ 'pon-ports': List of objects that provide PON port statistics
+ """
+ super(OltPmMetrics, self).__init__(adapter_agent, device_id, logical_device_id,
+ grouped=grouped, freq_override=freq_override,
+ **kwargs)
+
+ # PM Config Types are COUNTER, GAUGE, and STATE
+ self.nni_pm_names = {
+ ('intf_id', PmConfig.CONTEXT), # Physical device interface ID/Port number
+
+ ('admin_state', PmConfig.STATE),
+ ('oper_status', PmConfig.STATE),
+
+ ('rx_bytes', PmConfig.COUNTER),
+ ('rx_packets', PmConfig.COUNTER),
+ ('rx_ucast_packets', PmConfig.COUNTER),
+ ('rx_mcast_packets', PmConfig.COUNTER),
+ ('rx_bcast_packets', PmConfig.COUNTER),
+ ('rx_error_packets', PmConfig.COUNTER),
+
+ ('tx_bytes', PmConfig.COUNTER),
+ ('tx_packets', PmConfig.COUNTER),
+ ('tx_ucast_packets', PmConfig.COUNTER),
+ ('tx_mcast_packets', PmConfig.COUNTER),
+ ('tx_bcast_packets', PmConfig.COUNTER),
+ ('tx_error_packets', PmConfig.COUNTER),
+ ('rx_crc_errors', PmConfig.COUNTER),
+ ('bip_errors', PmConfig.COUNTER),
+ }
+ self.pon_pm_names = {
+ ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
+ ('pon_id', PmConfig.CONTEXT), # PON ID (0..n)
+
+ ('admin_state', PmConfig.STATE),
+ ('oper_status', PmConfig.STATE),
+ ('rx_packets', PmConfig.COUNTER),
+ ('rx_bytes', PmConfig.COUNTER),
+ ('tx_packets', PmConfig.COUNTER),
+ ('tx_bytes', PmConfig.COUNTER),
+ ('tx_bip_errors', PmConfig.COUNTER),
+ ('in_service_onus', PmConfig.GAUGE),
+ ('closest_onu_distance', PmConfig.GAUGE)
+ }
+ self.onu_pm_names = {
+ ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
+ ('pon_id', PmConfig.CONTEXT),
+ ('onu_id', PmConfig.CONTEXT),
+
+ ('fiber_length', PmConfig.GAUGE),
+ ('equalization_delay', PmConfig.GAUGE),
+ ('rssi', PmConfig.GAUGE),
+ }
+ self.gem_pm_names = {
+ ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
+ ('pon_id', PmConfig.CONTEXT),
+ ('onu_id', PmConfig.CONTEXT),
+ ('gem_id', PmConfig.CONTEXT),
+
+ ('alloc_id', PmConfig.GAUGE),
+ ('rx_packets', PmConfig.COUNTER),
+ ('rx_bytes', PmConfig.COUNTER),
+ ('tx_packets', PmConfig.COUNTER),
+ ('tx_bytes', PmConfig.COUNTER),
+ }
+ self.nni_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in self.nni_pm_names}
+ self.pon_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in self.pon_pm_names}
+ self.onu_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in self.onu_pm_names}
+ self.gem_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in self.gem_pm_names}
+
+ self._nni_ports = kwargs.pop('nni-ports', None)
+ self._pon_ports = kwargs.pop('pon-ports', None)
+
+ def update(self, pm_config):
+ try:
+ # TODO: Test frequency override capability for a particular group
+ if self.default_freq != pm_config.default_freq:
+ # Update the callback to the new frequency.
+ self.default_freq = pm_config.default_freq
+ self.lc.stop()
+ self.lc.start(interval=self.default_freq / 10)
+
+ if pm_config.grouped:
+ for group in pm_config.groups:
+ group_config = self.pm_group_metrics.get(group.group_name)
+ if group_config is not None:
+ group_config.enabled = group.enabled
+ else:
+ msg = 'There are no independent OLT metrics, only group metrics at this time'
+ raise NotImplemented(msg)
+
+ except Exception as e:
+ self.log.exception('update-failure', e=e)
+ raise
+
+ def make_proto(self, pm_config=None):
+ if pm_config is None:
+ pm_config = PmConfigs(id=self.device_id, default_freq=self.default_freq,
+ grouped=self.grouped,
+ freq_override=self.freq_override)
+ metrics = set()
+ have_nni = self._nni_ports is not None and len(self._nni_ports) > 0
+ have_pon = self._pon_ports is not None and len(self._pon_ports) > 0
+
+ if self.grouped:
+ if have_nni:
+ pm_ether_stats = PmGroupConfig(group_name='Ethernet',
+ group_freq=self.default_freq,
+ enabled=True)
+ self.pm_group_metrics[pm_ether_stats.group_name] = pm_ether_stats
+
+ else:
+ pm_ether_stats = None
+
+ if have_pon:
+ pm_pon_stats = PmGroupConfig(group_name='PON',
+ group_freq=self.default_freq,
+ enabled=True)
+
+ pm_onu_stats = PmGroupConfig(group_name='ONU',
+ group_freq=self.default_freq,
+ enabled=True)
+
+ pm_gem_stats = PmGroupConfig(group_name='GEM',
+ group_freq=self.default_freq,
+ enabled=True)
+
+ self.pm_group_metrics[pm_pon_stats.group_name] = pm_pon_stats
+ self.pm_group_metrics[pm_onu_stats.group_name] = pm_onu_stats
+ self.pm_group_metrics[pm_gem_stats.group_name] = pm_gem_stats
+ else:
+ pm_pon_stats = None
+ pm_onu_stats = None
+ pm_gem_stats = None
+
+ else:
+ pm_ether_stats = pm_config if have_nni else None
+ pm_pon_stats = pm_config if have_pon else None
+ pm_onu_stats = pm_config if have_pon else None
+ pm_gem_stats = pm_config if have_pon else None
+
+ if have_nni:
+ for m in sorted(self.nni_metrics_config):
+ pm = self.nni_metrics_config[m]
+ if not self.grouped:
+ if pm.name in metrics:
+ continue
+ metrics.add(pm.name)
+ pm_ether_stats.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+ if have_pon:
+ for m in sorted(self.pon_metrics_config):
+ pm = self.pon_metrics_config[m]
+ if not self.grouped:
+ if pm.name in metrics:
+ continue
+ metrics.add(pm.name)
+ pm_pon_stats.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+
+ for m in sorted(self.onu_metrics_config):
+ pm = self.onu_metrics_config[m]
+ if not self.grouped:
+ if pm.name in metrics:
+ continue
+ metrics.add(pm.name)
+ pm_onu_stats.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+
+ for m in sorted(self.gem_metrics_config):
+ pm = self.gem_metrics_config[m]
+ if not self.grouped:
+ if pm.name in metrics:
+ continue
+ metrics.add(pm.name)
+ pm_gem_stats.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+ if self.grouped:
+ pm_config.groups.extend([stats for stats in
+ self.pm_group_metrics.itervalues()])
+
+ return pm_config
+
+ def collect_metrics(self, data=None):
+ """
+ Collect metrics for this adapter.
+
+ The data collected (or passed in) is a list of pairs/tuples. Each
+ pair is composed of a MetricMetaData metadata-portion and list of MetricValuePairs
+ that contains a single individual metric or list of metrics if this is a
+ group metric.
+
+ This method is called for each adapter at a fixed frequency.
+ TODO: Currently all group metrics are collected on a single timer tick.
+ This needs to be fixed as independent group or instance collection is
+ desirable.
+
+ :param data: (list) Existing list of collected metrics (MetricInformation).
+ This is provided to allow derived classes to call into
+ further encapsulated classes.
+
+ :return: (list) metadata and metrics pairs - see description above
+ """
+ if data is None:
+ data = list()
+
+ group_name = 'Ethernet'
+ if self.pm_group_metrics[group_name].enabled:
+ for port in self._nni_ports:
+ group_data = self.collect_group_metrics(group_name,
+ port,
+ self.nni_pm_names,
+ self.nni_metrics_config)
+ if group_data is not None:
+ data.append(group_data)
+
+ for port in self._pon_ports:
+ group_name = 'PON'
+ if self.pm_group_metrics[group_name].enabled:
+ group_data = self.collect_group_metrics(group_name,
+ port,
+ self.pon_pm_names,
+ self.pon_metrics_config)
+ if group_data is not None:
+ data.append(group_data)
+
+ for onu_id in port.onu_ids:
+ onu = port.onu(onu_id)
+ if onu is not None:
+ group_name = 'ONU'
+ if self.pm_group_metrics[group_name].enabled:
+ group_data = self.collect_group_metrics(group_name,
+ onu,
+ self.onu_pm_names,
+ self.onu_metrics_config)
+ if group_data is not None:
+ data.append(group_data)
+
+ group_name = 'GEM'
+ if self.pm_group_metrics[group_name].enabled:
+ for gem in onu.gem_ports:
+ if not gem.multicast:
+ group_data = self.collect_group_metrics(group_name,
+ onu,
+ self.gem_pm_names,
+ self.gem_metrics_config)
+ if group_data is not None:
+ data.append(group_data)
+
+ # TODO: Do any multicast GEM PORT metrics here...
+ return data
diff --git a/python/adapters/extensions/kpi/onu/IntervalMetrics.md b/python/adapters/extensions/kpi/onu/IntervalMetrics.md
new file mode 100644
index 0000000..31169ae
--- /dev/null
+++ b/python/adapters/extensions/kpi/onu/IntervalMetrics.md
@@ -0,0 +1,302 @@
+# ONU OMCI Historical Interval PM Groups
+
+This document outlines the 15-minute interval groups currently supported by the
+**OnuPmIntervalMetrics** _onu_pm_interval_metrics.py_ file. These groups
+cover a 15-minute interval.
+
+## Performance Interval State Machine
+
+At OpenOMCI startup within an ONU Device Adapter, as soon as the OpenOMCI ME database has
+been declared to be in-sync ONU's ME Database, the Performance Interval State Machine is
+started for the ONU. The first task it performs is to synchronize the ONU's (hardware) time with
+the ONU Device Handler's (Linux container) so that a 15-minute interval is established.
+
+The OpenOMCI PM interval state machine then examines managed elements created by the
+ONU autonomously or created by OpenOMCI in response to a OMCI request from an ONU
+adapter to determine if an appropriate 15-Minute historical PM ME needs to be attached. The
+state machine also registers for notification of any create/delete requests at that
+point so that it can add/remove 15-minute historical PM MEs as services are applied or
+removed.
+
+Before adding a 15-minute historical PM ME, the ME capabilities of the ONU is
+examined to insure that it can support that particular ME. This is important as the
+Ethernet Frame historical intervals are actually supported by up to 4 different MEs
+reporting the basically the same data. This is detailed below in the _Ethernet Frame
+Performance Monitoring MEs_ section.
+
+## Timezone
+
+The ONU will be synchronized to the Linux Container running the ONU Device handler's
+time in UTC upon startup. Not all ONUs have the capability to set their calendar
+date (month, day, year) to that of the ONU's Device Handler, but it will set the
+clock to that date. For reporting of 15-minute intervals, only an accurate 15-minute
+boundary is really of any great importance.
+
+## Interval Reporting
+
+After the ONU time synchronization request is made, the first reported interval is
+schedule to occur in the next 15-minute boundry. For example, if the OpenOMCI
+state machine synchronizes the ONU's time at
+
+## Common Elements for All Reported MEs
+
+In addition to counter elements (attributes) reported in each ME, every reported
+historical interval report the following Elements as context values in the KPI
+Event metadata field. Each value is reported as a _string_ per the Protobuf structure
+but are actually integer/floats.
+
+| Label | Type | Description |
+| ------------------: | :----------: | :---------- |
+| class_id | int, 16-bits | The ME Class ID of the PM Interval ME |
+| entity_id | int, 16-bits | The OMCI Entity Instance of the particular PM Interval ME |
+| interval_end_time | int, 8-bits | Identifies the most recently finished 15 minute. This attribute is set to zero when a synchronize time request is performed by OpenOMCI. This counter rolls over from 255 to 0 upon saturation. |
+| interval_start_time | int, 64-bits | The UTC timestamp (seconds since epoch) rounded down to the start time of the specific interval |
+
+# Supported 15-Minute Historical Performance Monitoring MEs
+
+The following 15-minute historical performance monitoring MEs currently supported are detailed
+in the sections below
+
+## Ethernet Frame Performance Monitoring MEs
+
+The OMCI Ethernet PM supported by OpenOMCI includes 4 possible MEs. These MEs are attached to
+the MAC Bridge Port Configuration MEs for the ONU. For downstream data, the ME is placed on the
+MAC Bridge Port Configuration ME closest to the ANI Port. For upstream data, the ME is placed
+on the MAC Bridge Port Configuration ME closest to the associated UNI.
+
+The OpenOMCI will first attempt to use the Extended Performance Monitoring MEs if they are
+supported by the ONU. First the 64-bit counter version will be attempted and then the 32-bit
+counters as a fallback. If of the Extended Performance Monitoring MEs are supported, the
+appropriate Upstream or DownStream Monitoring ME will be used.
+
+### ME Information
+
+The table below describes the four Ethernet Frame Performance Monitoring MEs and provides their
+counter width (in bytes) and ME Class ID.
+
+| ME Name | Class ID | Counter Width |
+| ----------------------------------------------------------: | :------: | :---: |
+| Ethernet Frame Extended Performance Monitoring64Bit | 426 | 64-bit |
+| Ethernet Frame Extended Performance Monitoring | 334 | 32-bit |
+| Ethernet Frame Upstream Performance MonitoringHistoryData | 322 | 32-bit |
+| Ethernet Frame Downstream Performance MonitoringHistoryData | 321 | 32-bit |
+
+**Metric Group Name**: Ethernet_Bridge_Port_History
+**Default Collection**: True
+**Default Interval**: 15 minutes & aligned to wall-clock. Read-Only
+
+### Counter Information
+
+Each of the Ethernet Frame PM MEs contain the following counters
+
+| Attribute Name | Description |
+| ------------------: | :-----------|
+| drop_events | The total number of events in which packets were dropped due to a lack of resources. This is not necessarily the number of packets dropped; it is the number of times this event was detected. |
+| octets | The total number of upstream octets received, including those in bad packets, excluding framing bits, but including FCS. |
+| packets | The total number of upstream packets received, including bad packets, broadcast packets and multicast packets. |
+| broadcast_packets | The total number of upstream good packets received that were directed to the broadcast address. This does not include multicast packets. |
+| multicast_packets | The total number of upstream good packets received that were directed to a multicast address. This does not include broadcast packets. |
+| crc_errored_packets | The total number of upstream packets received that had a length (excluding framing bits, but including FCS octets) of between 64 octets and 1518 octets, inclusive, but had either a bad FCS with an integral number of octets (FCS error) or a bad FCS with a non-integral number of octets (alignment error). |
+| undersize_packets | The total number of upstream packets received that were less than 64 octets long, but were otherwise well formed (excluding framing bits, but including FCS). |
+| oversize_packets | The total number of upstream packets received that were longer than 1518 octets (excluding framing bits, but including FCS) and were otherwise well formed. NOTE 2 – If 2 000 byte Ethernet frames are supported, counts in this performance parameter are not necessarily errors. |
+| 64_octets | The total number of upstream received packets (including bad packets) that were 64 octets long, excluding framing bits but including FCS. |
+| 65_to_127_octets | The total number of upstream received packets (including bad packets) that were 65..127 octets long, excluding framing bits but including FCS. |
+| 128_to_255_octets | The total number of upstream packets (including bad packets) received that were 128..255 octets long, excluding framing bits but including FCS. |
+| 256_to_511_octets | The total number of upstream packets (including bad packets) received that were 256..511 octets long, excluding framing bits but including FCS. |
+| 512_to_1023_octets | The total number of upstream packets (including bad packets) received that were 512..1 023 octets long, excluding framing bits but including FCS. |
+| 1024_to_1518_octets | The total number of upstream packets (including bad packets) received that were 1024..1518 octets long, excluding framing bits, but including FCS. |
+
+## Ethernet PM Monitoring History Data (Class ID 24)
+
+This managed entity collects some of the performance monitoring data for a physical
+Ethernet interface. Instances of this managed entity are created and deleted by the OLT.
+
+**Metric Group Name**: Ethernet_UNI_History
+**Default Collection**: True
+**Default Interval**: 15 minutes & aligned to wall-clock. Read-Only
+
+### Application
+
+For performance monitoring of Ethernet UNI.
+
+### Relationships
+
+An instance of this managed entity is associated with an instance of the physical path
+termination point Ethernet UNI.
+
+### Attributes
+All counters are 32-bits wide.
+
+| Attribute Name | Description |
+| ------------------: | :-----------|
+| fcs_errors | This attribute counts frames received on a particular interface that were an integral number of octets in length but failed the frame check sequence (FCS) check. The count is incremented when the MAC service returns the frameCheckError status to the link layer control (LLC) or other MAC user. Received frames for which multiple error conditions are obtained are counted according to the error status presented to the LLC. |
+| excessive_collision_counter | This attribute counts frames whose transmission failed due to excessive collisions. |
+| late_collision_counter | This attribute counts the number of times that a collision was detected later than 512 bit times into the transmission of a packet. |
+| frames_too_long | This attribute counts received frames that exceeded the maximum permitted frame size. The count is incremented when the MAC service returns the frameTooLong status to the LLC. |
+| buffer_overflows_on_rx | This attribute counts the number of times that the receive buffer overflowed. |
+| buffer_overflows_on_tx | This attribute counts the number of times that the transmit buffer overflowed. |
+| single_collision_frame_counter | This attribute counts successfully transmitted frames whose transmission was delayed by exactly one collision. |
+| multiple_collisions_frame_counter | This attribute counts successfully transmitted frames whose transmission was delayed by more than one collision. |
+| sqe_counter | This attribute counts the number of times that the SQE test error message was generated by the PLS sublayer. |
+| deferred_tx_counter | This attribute counts frames whose first transmission attempt was delayed because the medium was busy. The count does not include frames involved in collisions. |
+| internal_mac_tx_error_counter | This attribute counts frames whose transmission failed due to an internal MAC sublayer transmit error. |
+| carrier_sense_error_counter | This attribute counts the number of times that carrier sense was lost or never asserted when attempting to transmit a frame. |
+| alignment_error_counter | This attribute counts received frames that were not an integral number of octets in length and did not pass the FCS check. |
+| internal_mac_rx_error_counter | This attribute counts frames whose reception failed due to an internal MAC sublayer receive error. |
+
+## FEC Performance Monitoring History Data (Class ID 312)
+
+This managed entity collects performance monitoring data associated with PON downstream FEC
+counters. Instances of this managed entity are created and deleted by the OLT.
+
+**Metric Group Name**: FEC_History
+**Default Collection**: True
+**Default Interval**: 15 minutes & aligned to wall-clock. Read-Only
+
+### Application
+This managed entity collects performance monitoring data associated with PON downstream FEC
+counters.
+
+### Relationships
+An instance of this managed entity is associated with an instance of the ANI-G managed entity.
+
+### Attributes
+
+| Attribute Name | Counter Width | Description |
+| -----------------------: | :-----: | :-----------|
+| corrected_bytes | 32-bits | This attribute counts the number of bytes that were corrected by the FEC function. |
+| corrected_code_words | 32-bits | This attribute counts the code words that were corrected by the FEC function. |
+| uncorrectable_code_words | 32-bits | This attribute counts errored code words that could not be corrected by the FEC function. |
+| total_code_words | 32-bits | This attribute counts the total received code words. |
+| fec_seconds | 16-bits | This attribute counts seconds during which there was a forward error correction anomaly. |
+
+
+## GEM Port Network CTP Monitoring History Data (Class ID 341)
+
+This managed entity collects GEM frame performance monitoring data associated with a GEM port
+network CTP. Instances of this managed entity are created and deleted by the OLT.
+
+Note 1: One might expect to find some form of impaired or discarded frame count associated with
+a GEM port. However, the only impairment that might be detected at the GEM frame level would be
+a corrupted GEM frame header. In this case, no part of the header could be considered reliable
+including the port ID. For this reason, there is no impaired or discarded frame count in this ME.
+
+Note 2: This managed entity replaces the GEM port performance history data managed entity and
+is preferred for new implementations.
+
+**Metric Group Name**: GEM_Port_History
+**Default Collection**: False
+**Default Interval**: 15 minutes & aligned to wall-clock. Read-Only
+
+### Relationships
+
+An instance of this managed entity is associated with an instance of the GEM port network CTP
+managed entity.
+
+### Attributes
+
+| Attribute Name | Counter Width | Description |
+| ------------------------: | :-----: | :-----------|
+| transmitted_gem_frames | 32-bits | This attribute counts GEM frames transmitted on the monitored GEM port. |
+| received_gem_frames | 32-bits | This attribute counts GEM frames received correctly on the monitored GEM port. A correctly received GEM frame is one that does not contain uncorrectable errors and has a valid HEC. |
+| received_payload_bytes | 64-bits | This attribute counts user payload bytes received on the monitored GEM port. |
+| transmitted_payload_bytes | 64-bits | This attribute counts user payload bytes transmitted on the monitored GEM port. |
+| encryption_key_errors | 32-bits | This attribute is defined in ITU-T G.987 systems only. It counts GEM frames with erroneous encryption key indexes. If the GEM port is not encrypted, this attribute counts any frame with a key index not equal to 0. If the GEM port is encrypted, this attribute counts any frame whose key index specifies a key that is not known to the ONU. |
+
+Note 3: GEM PM ignores idle GEM frames.
+
+Note 4: GEM PM counts each non-idle GEM frame, whether it contains an entire user frame or only
+a fragment of a user frame.
+
+## XgPon TC Performance Monitoring History Data (Class ID 344)
+
+This managed entity collects performance monitoring data associated with the XG-PON
+transmission convergence layer, as defined in ITU-T G.987.3.
+
+**Metric Group Name**: xgPON_TC_History
+**Default Collection**: False
+**Default Interval**: 15 minutes & aligned to wall-clock. Read-Only
+
+### Relationships
+An instance of this managed entity is associated with an ANI-G.
+
+### Attributes
+
+All counters are 32-bits wide.
+
+| Attribute Name | Description |
+| ------------------------: | :-----------|
+| psbd_hec_error_count | This attribute counts HEC errors in any of the fields of the downstream physical sync block. |
+| xgtc_hec_error_count | This attribute counts HEC errors detected in the XGTC header. |
+| unknown_profile_count | This attribute counts the number of grants received whose specified profile was not known to the ONU. |
+| transmitted_xgem_frames | This attribute counts the number of non-idle XGEM frames transmitted. If an SDU is fragmented, each fragment is an XGEM frame and is counted as such. |
+| fragment_xgem_frames | This attribute counts the number of XGEM frames that represent fragmented SDUs, as indicated by the LF bit = 0. |
+| xgem_hec_lost_words_count | This attribute counts the number of four-byte words lost because of an XGEM frame HEC error. In general, all XGTC payload following the error is lost, until the next PSBd event. |
+| xgem_key_errors | This attribute counts the number of downstream XGEM frames received with an invalid key specification. The key may be invalid for several reasons. |
+| xgem_hec_error_count | This attribute counts the number of instances of an XGEM frame HEC error. |
+
+## XgPon Downstream Performance Monitoring History Data (Class ID 345)
+
+This managed entity collects performance monitoring data associated with the XG-PON
+transmission convergence layer, as defined in ITU-T G.987.3. It collects counters associated with
+downstream PLOAM and OMCI messages.
+
+**Metric Group Name**: xgPON_Downstream_History
+**Default Collection**: False
+**Default Interval**: 15 minutes & aligned to wall-clock. Read-Only
+
+### Relationships
+
+An instance of this managed entity is associated with an ANI-G.
+
+### Attributes
+
+All counters are 32-bits wide.
+
+| Attribute Name | Description |
+| --------------------------------------: | :-----------|
+| ploam_mic_error_count | This attribute counts MIC errors detected in downstream PLOAM messages, either directed to this ONU or broadcast to all ONUs. |
+| downstream_ploam_messages_count | This attribute counts PLOAM messages received, either directed to this ONU or broadcast to all ONUs. |
+| profile_messages_received | This attribute counts the number of profile messages received, either directed to this ONU or broadcast to all ONUs. |
+| ranging_time_messages_received | This attribute counts the number of ranging_time messages received, either directed to this ONU or broadcast to all ONUs. |
+| deactivate_onu_id_messages_received | This attribute counts the number of deactivate_ONU-ID messages received, either directed to this ONU or broadcast to all ONUs. Deactivate_ONU-ID messages do not reset this counter. |
+| disable_serial_number_messages_received | This attribute counts the number of disable_serial_number messages received, whose serial number specified this ONU. |
+| request_registration_messages_received | This attribute counts the number request_registration messages received. |
+| assign_alloc_id_messages_received | This attribute counts the number of assign_alloc-ID messages received. |
+| key_control_messages_received | This attribute counts the number of key_control messages received, either directed to this ONU or broadcast to all ONUs. |
+| sleep_allow_messages_received | This attribute counts the number of sleep_allow messages received, either directed to this ONU or broadcast to all ONUs. |
+| baseline_omci_messages_received_count | This attribute counts the number of OMCI messages received in the baseline message format. |
+| extended_omci_messages_received_count | This attribute counts the number of OMCI messages received in the extended message format. |
+| assign_onu_id_messages_received | This attribute counts the number of assign_ONU-ID messages received since the last re-boot. |
+| omci_mic_error_count | This attribute counts MIC errors detected in OMCI messages directed to this ONU. |
+
+## XgPon Upstream Performance Monitoring History Data (Class ID 346)
+
+This managed entity collects performance monitoring data associated with the XG-PON
+transmission convergence layer, as defined in ITU-T G.987.3. It counts upstream PLOAM
+messages transmitted by the ONU.
+
+**Metric Group Name**: xgPON_Upstream_History
+**Default Collection**: False
+**Default Interval**: 15 minutes & aligned to wall-clock. Read-Only
+
+###Relationships
+
+An instance of this managed entity is associated with an ANI-G.
+
+### Attributes
+
+All counters are 32-bits wide.
+
+| Attribute Name | Description |
+| ------------------------------: | :-----------|
+| upstream_ploam_message_count | This attribute counts PLOAM messages transmitted upstream, excluding acknowledge messages. |
+| serial_number_onu_message_count | This attribute counts Serial_number_ONU PLOAM messages transmitted. |
+| registration_message_count | This attribute counts registration PLOAM messages transmitted. |
+| key_report_message_count | This attribute counts key_report PLOAM messages transmitted. |
+| acknowledge_message_count | This attribute counts acknowledge PLOAM messages transmitted. It includes all forms of acknowledgement, including those transmitted in response to a PLOAM grant when the ONU has nothing to send. |
+| sleep_request_message_count | This attribute counts sleep_request PLOAM messages transmitted. |
+
+# Remaining Work Items
+
+- The enable/disable of a PM group (CLI/NBI) should control whether or not a PM interval ME is created and collected.
\ No newline at end of file
diff --git a/python/adapters/extensions/kpi/onu/README.md b/python/adapters/extensions/kpi/onu/README.md
new file mode 100644
index 0000000..4b9798e
--- /dev/null
+++ b/python/adapters/extensions/kpi/onu/README.md
@@ -0,0 +1,123 @@
+# ONU PM Metrics
+
+
+**THESE ARE PRELIMINARY METRIC GROUPS**, Work is needed by the VOLTHA community to reach a consensus on the
+actual metrics that will be provided. **Also**, please read the **Remaining Work Item** sections of each
+README file.
+
+
+This document outlines the non-interval metrics collected for the ONU by the OpenOMCI code. These
+are primarily collected from one of the many OMCI Managed Entities
+
+## Format on the Kafka bus
+
+The format of the ONU KPI Events is detailed in the [Basic KPI Format (**KpiEvent2**)](../README.md)
+section of this documents parent directory for wire format on the bus. This document primarily provides
+the group metric information for OLT PKIs and associated metadata context information.
+
+**All** metric values reported by the library are reported as *float*s. The context and metric tables
+listed in the sections below report the type as initially collected by the OLT Device Adapters.
+
+#ONU PM Metric Groups
+
+The following sections outline the KPI metrics gathered by OpenOMCI on behalf of the ONU. If an ONU
+does not support a specific metric in a group, it will not report that metric. This is preferred to
+reporting a metric and it always having a value of 0.0 (which could be misleading).
+
+**Note**: Currently all metric groups are collected and reported at one time (only one collection timer)
+and this value is controlled by the VOLTHA shared kpi library's PM_Config default_freq value and will
+be set to _60 seconds_. This single-collection deficiency will be corrected in the near future.
+
+## ANI Optical KPI Metrics
+
+This group reports the ONU's Optical Power metrics for each PON physical port as reported by the
+OMCI Managed Entity ANI-G (_Class ID #263_).
+
+**Metric Group Name**: PON_Optical
+**Default Collection**: True
+**Default Interval**: 15 minutes
+
+**Metadata Context items**
+
+| key | value | Notes |
+| :-----: | :------ | :---- |
+| intf_id | integer | Physical device interface port ID for this PON/ANI port |
+
+The port ID is extracted from the lower 8-bits of the ANI-G Managed Entity ID and indicates
+the physical position of the PON interface.
+
+**Metrics**
+
+| key | type / size | Notes |
+| :---------------------: | :----------- | :---- |
+| transmit_power | int, 16-bits | This attribute reports the current measurement of mean optical launch power. Its value is a 2s complement integer referred to 1 mW (i.e., dBm), with 0.002 dB granularity |
+| receive_power | int, 16-bits | This attribute reports the current measurement of the total downstream optical signal level. Its value is a 2s complement integer referred to 1 mW (i.e., dBm), with 0.002 dB granularity. |
+
+**NOTE**: The following metrics were also requested for the PON interface in
+[VOL-935](https://jira.opencord.org/browse/VOL-935) but they are not available through
+the OpenOMCI set of Managed Entities. However there are alarms available that relate to
+these items available through the ANI-G ME:
+
+ - ONT Optical module/transceiver temperature
+ - ONT Optical module/transceiver voltage
+ - ONT Laser bias current
+
+TR-287 does reference mechanisms to perform OLT and ONU Optical Link monitoring to cover these
+three items but interfaces are not yet available in VOLTHA and retrieval of these values from
+an ONU may be difficult as the only defined interface to retrieve data is OMCI.
+
+## UNI KPI Metrics
+
+This group reports metrics associated with the customer facing UNI port of the ONU
+and is collected from OMCI Physical Path Termination Point Ethernet UNI (_Class ID #11_)
+and the UNI-G (_Class iD #264_).
+
+**Metric Group Name**: UNI_Status
+**Default Collection**: True
+**Default Interval**: 15 minutes
+
+**Metadata Context items**
+
+| key | value | Notes |
+| :-----: | :------ | :---- |
+| intf_id | integer | Physical device interface port ID for this UNI port |
+
+The port ID is extracted from the UNI-G Managed Entity ID and indicates the
+physical position of the UNI interface. This ID is implicitly linked to the
+associated PPTP Ethernet UNI ME.
+
+**Metrics**
+
+| key | type / size | From | Notes |
+| :--------------: | :---------- | :---- | :---- |
+| ethernet_type | int, gauge | PPTP | This attribute represents the sensed interface type as defined in the table below |
+| oper_status | boolean | PPTP | Link status/Operational Status: Link up (1), Link down (0) |
+| pptp_admin_state | boolean | PPTP | Administrative state: Locked/disabled (1), Unlocked/enabled (0) |
+| uni_admin_state | boolean | UNI-G | Administrative state: Locked/disabled (1), Unlocked/enabled (0) |
+
+**Sensed Ethernet Type Table**
+
+| value | Rate | Duplex |
+| ----: | :--------------: | :--- |
+| 0x00 | Unknown | n/a |
+| 0x01 | 10BASE-T | full |
+| 0x02 | 100BASE-T | full |
+| 0x03 | Gigabit Ethernet | full |
+| 0x04 | 10Gb/s Ethernet | full |
+| 0x05 | 2.5Gb/s Ethernet | full |
+| 0x06 | 5Gb/s Ethernet | full |
+| 0x07 | 25Gb/s Ethernet | full |
+| 0x08 | 40Gb/s Ethernet | full |
+| 0x11 | 10BASE-T | half |
+| 0x12 | 100BASE-T | half |
+| 0x13 | Gigabit Ethernet | half |
+
+# Remaining Work Items
+
+This initial code is only a preliminary work. See the [Remaining Work Items](../README.md)
+section of this document's parent directory for a list of remaining tasks. In addition to these
+work items, the interval statistics [README](./IntervalMetrics.md) may have additional work
+items remaining.
+
+
+TODO: For each group, list if the default is enabled/disabled
\ No newline at end of file
diff --git a/python/adapters/extensions/kpi/onu/__init__.py b/python/adapters/extensions/kpi/onu/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/kpi/onu/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/kpi/onu/onu_omci_pm.py b/python/adapters/extensions/kpi/onu/onu_omci_pm.py
new file mode 100644
index 0000000..c186bbb
--- /dev/null
+++ b/python/adapters/extensions/kpi/onu/onu_omci_pm.py
@@ -0,0 +1,317 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import arrow
+from voltha.protos.device_pb2 import PmConfig, PmGroupConfig
+from voltha.protos.events_pb2 import MetricInformation, MetricMetaData
+from voltha.extensions.kpi.adapter_pm_metrics import AdapterPmMetrics
+from voltha.extensions.kpi.onu.onu_pm_interval_metrics import OnuPmIntervalMetrics
+from voltha.extensions.omci.omci_entities import UniG
+from voltha.extensions.omci.omci_entities import PptpEthernetUni
+
+
+class OnuOmciPmMetrics(AdapterPmMetrics):
+ """ ONU OMCI related metrics """
+
+ # Metric default settings
+ #
+ # Frequency values are in 1/10ths of a second
+ #
+ OMCI_DEV_KEY = 'omci-onu-dev'
+ OMCI_CC_GROUP_NAME = 'OMCI_CC'
+ DEFAULT_OMCI_CC_ENABLED = False
+ DEFAULT_OMCI_CC_FREQUENCY = (2 * 60) * 10
+
+ OPTICAL_GROUP_NAME = 'PON_Optical'
+ DEFAULT_OPTICAL_ENABLED = True
+ DEFAULT_OPTICAL_FREQUENCY = (15 * 60 * 10)
+
+ UNI_STATUS_GROUP_NAME = 'UNI_Status'
+ DEFAULT_UNI_STATUS_ENABLED = True
+ DEFAULT_UNI_STATUS_FREQUENCY = (15 * 60 * 10)
+
+ def __init__(self, adapter_agent, device_id, logical_device_id,
+ grouped=False, freq_override=False, **kwargs):
+ """
+ Initializer for shared ONU Device Adapter OMCI CC PM metrics
+
+ :param adapter_agent: (AdapterAgent) Adapter agent for the device
+ :param device_id: (str) Device ID
+ :param logical_device_id: (str) VOLTHA Logical Device ID
+ :param grouped: (bool) Flag indicating if statistics are managed as a group
+ :param freq_override: (bool) Flag indicating if frequency collection can be specified
+ on a per group basis
+ :param kwargs: (dict) Device Adapter specific values. For an ONU Device adapter, the
+ expected key-value pairs are listed below. If not provided, the
+ associated PM statistics are not gathered:
+
+ 'omci-onu-dev': Reference to the OMCI OnuDeviceEtnry object for
+ retrieval of OpenOMCI Communications channel statistics
+ and retrieval of polled statistics.
+ """
+ super(OnuOmciPmMetrics, self).__init__(adapter_agent, device_id, logical_device_id,
+ grouped=grouped, freq_override=freq_override,
+ **kwargs)
+
+ self._omci_onu_device = kwargs.pop(OnuOmciPmMetrics.OMCI_DEV_KEY, None)
+ self._omci_cc = self._omci_onu_device.omci_cc if self._omci_onu_device is not None else None
+
+ self.omci_cc_pm_names = {
+ ('tx_frames', PmConfig.COUNTER),
+ ('tx_errors', PmConfig.COUNTER),
+ ('rx_frames', PmConfig.COUNTER),
+ ('rx_unknown_tid', PmConfig.COUNTER),
+ ('rx_onu_frames', PmConfig.COUNTER), # Rx ONU autonomous messages
+ ('rx_unknown_me', PmConfig.COUNTER), # Managed Entities without a decode definition
+ ('rx_timeouts', PmConfig.COUNTER),
+ ('rx_late', PmConfig.COUNTER),
+ ('consecutive_errors', PmConfig.COUNTER),
+ ('reply_min', PmConfig.GAUGE), # Milliseconds
+ ('reply_max', PmConfig.GAUGE), # Milliseconds
+ ('reply_average', PmConfig.GAUGE), # Milliseconds
+ ('hp_tx_queue_len', PmConfig.GAUGE),
+ ('lp_tx_queue_len', PmConfig.GAUGE),
+ ('max_hp_tx_queue', PmConfig.GAUGE),
+ ('max_lp_tx_queue', PmConfig.GAUGE),
+ }
+ self.omci_cc_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in self.omci_cc_pm_names}
+
+ self.omci_optical_pm_names = {
+ ('intf_id', PmConfig.CONTEXT),
+
+ ('transmit_power', PmConfig.GAUGE),
+ ('receive_power', PmConfig.GAUGE),
+ }
+ self.omci_optical_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in self.omci_optical_pm_names}
+
+ self.omci_uni_pm_names = {
+ ('intf_id', PmConfig.CONTEXT),
+
+ ('ethernet_type', PmConfig.GAUGE), # PPTP Ethernet ME
+ ('oper_status', PmConfig.GAUGE), # PPTP Ethernet ME
+ ('pptp_admin_state', PmConfig.GAUGE), # PPTP Ethernet ME
+ ('uni_admin_state', PmConfig.GAUGE), # UNI-G ME
+ }
+ self.omci_uni_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in self.omci_uni_pm_names}
+
+ self.openomci_interval_pm = OnuPmIntervalMetrics(adapter_agent, device_id, logical_device_id)
+
+ def update(self, pm_config):
+ # TODO: Test frequency override capability for a particular group
+ if self.default_freq != pm_config.default_freq:
+ # Update the callback to the new frequency.
+ self.default_freq = pm_config.default_freq
+ self.lc.stop()
+ self.lc.start(interval=self.default_freq / 10)
+
+ if pm_config.grouped:
+ for group in pm_config.groups:
+ group_config = self.pm_group_metrics.get(group.group_name)
+ if group_config is not None:
+ group_config.enabled = group.enabled
+ else:
+ msg = 'There are on independent OMCI metrics, only group metrics at this time'
+ raise NotImplemented(msg)
+
+ self.openomci_interval_pm.update(pm_config)
+
+ def make_proto(self, pm_config=None):
+ assert pm_config is not None
+
+ # OMCI only supports grouped metrics
+ if self._omci_onu_device is None or not self.grouped:
+ return pm_config
+
+ pm_omci_cc_stats = PmGroupConfig(group_name=OnuOmciPmMetrics.OMCI_CC_GROUP_NAME,
+ group_freq=OnuOmciPmMetrics.DEFAULT_OMCI_CC_FREQUENCY,
+ enabled=OnuOmciPmMetrics.DEFAULT_OMCI_CC_ENABLED)
+ self.pm_group_metrics[pm_omci_cc_stats.group_name] = pm_omci_cc_stats
+
+ pm_omci_optical_stats = PmGroupConfig(group_name=OnuOmciPmMetrics.OPTICAL_GROUP_NAME,
+ group_freq=OnuOmciPmMetrics.DEFAULT_OPTICAL_FREQUENCY,
+ enabled=OnuOmciPmMetrics.DEFAULT_OPTICAL_ENABLED)
+ self.pm_group_metrics[pm_omci_optical_stats.group_name] = pm_omci_optical_stats
+
+ pm_omci_uni_stats = PmGroupConfig(group_name=OnuOmciPmMetrics.UNI_STATUS_GROUP_NAME,
+ group_freq=OnuOmciPmMetrics.DEFAULT_UNI_STATUS_FREQUENCY,
+ enabled=OnuOmciPmMetrics.DEFAULT_UNI_STATUS_ENABLED)
+ self.pm_group_metrics[pm_omci_uni_stats.group_name] = pm_omci_uni_stats
+
+ stats_and_config = [(pm_omci_cc_stats, self.omci_cc_metrics_config),
+ (pm_omci_optical_stats, self.omci_optical_metrics_config),
+ (pm_omci_uni_stats, self.omci_cc_metrics_config)]
+
+ for stats, config in stats_and_config:
+ for m in sorted(config):
+ pm = config[m]
+ stats.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+ pm_config.groups.extend([stats])
+
+ # Also create OMCI Interval PM configs
+ return self.openomci_interval_pm.make_proto(pm_config)
+
+ def collect_metrics(self, data=None):
+ """
+ Collect metrics for this adapter.
+
+ The data collected (or passed in) is a list of pairs/tuples. Each
+ pair is composed of a MetricMetaData metadata-portion and list of MetricValuePairs
+ that contains a single individual metric or list of metrics if this is a
+ group metric.
+
+ This method is called for each adapter at a fixed frequency.
+ TODO: Currently all group metrics are collected on a single timer tick.
+ This needs to be fixed as independent group or instance collection is
+ desirable.
+
+ :param data: (list) Existing list of collected metrics (MetricInformation).
+ This is provided to allow derived classes to call into
+ further encapsulated classes.
+
+ :return: (list) metadata and metrics pairs - see description above
+ """
+ if data is None:
+ data = list()
+
+ # Note: Interval PM is collection done autonomously, not through this method
+
+ if self._omci_cc is not None:
+ group_name = OnuOmciPmMetrics.OMCI_CC_GROUP_NAME
+ if self.pm_group_metrics[group_name].enabled:
+ group_data = self.collect_group_metrics(group_name,
+ self._omci_cc,
+ self.omci_cc_pm_names,
+ self.omci_cc_metrics_config)
+ if group_data is not None:
+ data.append(group_data)
+
+ # Optical and UNI data is collected on a per-port basis
+ data.extend(self.collect_optical_metrics())
+ data.extend(self.collect_uni_status_metrics())
+
+ return data
+
+ def collect_optical_metrics(self):
+ """
+ Collect the metrics for optical information from all ANI/PONs
+
+ :return: (list) collected metrics (MetricInformation)
+ """
+ now = self._omci_onu_device.timestamp
+
+ group_name = OnuOmciPmMetrics.OPTICAL_GROUP_NAME
+ if now is None or not self.pm_group_metrics[group_name].enabled:
+ return []
+
+ # Scan all ANI-G ports
+ ani_g_entities = self._omci_onu_device.configuration.ani_g_entities
+ ani_g_entities_ids = ani_g_entities.keys() if ani_g_entities is not None else None
+ metrics_info = []
+
+ if ani_g_entities_ids is not None and len(ani_g_entities_ids):
+ from voltha.extensions.omci.omci_entities import AniG
+ ani_g_items = ['optical_signal_level', 'transmit_optical_level']
+
+ for entity_id in ani_g_entities_ids:
+ metrics = dict()
+ data = self._omci_onu_device.query_mib(class_id=AniG.class_id,
+ instance_id=entity_id,
+ attributes=ani_g_items)
+ if len(data):
+ if 'optical_signal_level' in data:
+ metrics['receive_power'] = data['optical_signal_level']
+
+ if 'transmit_optical_level' in data:
+ metrics['transmit_power'] = data['transmit_optical_level']
+
+ if len(metrics):
+ metric_data = MetricInformation(metadata=MetricMetaData(title=group_name,
+ ts=now,
+ logical_device_id=self.logical_device_id,
+ serial_no=self.serial_number,
+ device_id=self.device_id,
+ context={
+ 'intf_id': str(entity_id)
+ }),
+ metrics=metrics)
+ metrics_info.append(metric_data)
+
+ return metrics_info
+
+ def collect_uni_status_metrics(self):
+ """
+ Collect the metrics for optical information from all ANI/PONs
+
+ :return: (list) collected metrics (MetricInformation)
+ """
+ now = self._omci_onu_device.timestamp
+
+ group_name = OnuOmciPmMetrics.UNI_STATUS_GROUP_NAME
+ if now is None or not self.pm_group_metrics[group_name].enabled:
+ return []
+
+ # Scan all UNI-G and PPTP ports
+ uni_g_entities = self._omci_onu_device.configuration.uni_g_entities
+ uni_g_entities_ids = uni_g_entities.keys() if uni_g_entities is not None else None
+ pptp_entities = self._omci_onu_device.configuration.pptp_entities
+ pptp_entities_ids = pptp_entities.keys() if pptp_entities is not None else None
+
+ metrics_info = []
+
+ if uni_g_entities_ids and pptp_entities_ids and len(uni_g_entities_ids) and \
+ len(uni_g_entities_ids) <= len(pptp_entities_ids):
+
+ uni_g_items = ['administrative_state']
+ pptp_items = ['administrative_state', 'operational_state', 'sensed_type']
+
+ for entity_id in pptp_entities_ids:
+ metrics = dict()
+ data = self._omci_onu_device.query_mib(class_id=UniG.class_id,
+ instance_id=entity_id,
+ attributes=uni_g_items)
+ if len(data):
+ if 'administrative_state' in data:
+ metrics['uni_admin_state'] = data['administrative_state']
+
+ data = self._omci_onu_device.query_mib(class_id=PptpEthernetUni.class_id,
+ instance_id=entity_id,
+ attributes=pptp_items)
+ if len(data):
+ if 'administrative_state' in data:
+ metrics['pptp_admin_state'] = data['administrative_state']
+
+ if 'operational_state' in data:
+ metrics['oper_status'] = data['operational_state']
+
+ if 'sensed_type' in data:
+ metrics['ethernet_type'] = data['sensed_type']
+
+ if len(metrics):
+ metric_data = MetricInformation(metadata=MetricMetaData(title=group_name,
+ ts=now,
+ logical_device_id=self.logical_device_id,
+ serial_no=self.serial_number,
+ device_id=self.device_id,
+ context={
+ 'intf_id': str(entity_id & 0xFF)
+ }),
+ metrics=metrics)
+ metrics_info.append(metric_data)
+
+ return metrics_info
diff --git a/python/adapters/extensions/kpi/onu/onu_pm_interval_metrics.py b/python/adapters/extensions/kpi/onu/onu_pm_interval_metrics.py
new file mode 100644
index 0000000..5629e7c
--- /dev/null
+++ b/python/adapters/extensions/kpi/onu/onu_pm_interval_metrics.py
@@ -0,0 +1,383 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import arrow
+from voltha.protos.device_pb2 import PmConfig, PmGroupConfig
+from voltha.protos.events_pb2 import KpiEvent2, MetricInformation, MetricMetaData, KpiEventType
+from voltha.extensions.kpi.adapter_pm_metrics import AdapterPmMetrics
+from voltha.extensions.omci.omci_entities import \
+ EthernetFrameUpstreamPerformanceMonitoringHistoryData, \
+ EthernetFrameDownstreamPerformanceMonitoringHistoryData, \
+ EthernetFrameExtendedPerformanceMonitoring, \
+ EthernetFrameExtendedPerformanceMonitoring64Bit, \
+ EthernetPMMonitoringHistoryData, FecPerformanceMonitoringHistoryData, \
+ GemPortNetworkCtpMonitoringHistoryData, XgPonTcPerformanceMonitoringHistoryData, \
+ XgPonDownstreamPerformanceMonitoringHistoryData, \
+ XgPonUpstreamPerformanceMonitoringHistoryData
+
+
+class OnuPmIntervalMetrics(AdapterPmMetrics):
+ """
+ ONU OMCI PM Interval metrics
+
+ These differ from other PM Metrics as they are collected and generated as a
+ result of receipt of OMCI get responses on various PM History MEs. They are
+ also always managed as a group with a fixed frequency of 15 minutes.
+ """
+ ME_ID_INFO = {
+ EthernetFrameUpstreamPerformanceMonitoringHistoryData.class_id: 'Ethernet_Bridge_Port_History',
+ EthernetFrameDownstreamPerformanceMonitoringHistoryData.class_id: 'Ethernet_Bridge_Port_History',
+ EthernetFrameExtendedPerformanceMonitoring.class_id: 'Ethernet_Bridge_Port_History',
+ EthernetFrameExtendedPerformanceMonitoring64Bit.class_id: 'Ethernet_Bridge_Port_History',
+ EthernetPMMonitoringHistoryData.class_id: 'Ethernet_UNI_History',
+ FecPerformanceMonitoringHistoryData.class_id: 'FEC_History',
+ GemPortNetworkCtpMonitoringHistoryData.class_id: 'GEM_Port_History',
+ XgPonTcPerformanceMonitoringHistoryData.class_id: 'xgPON_TC_History',
+ XgPonDownstreamPerformanceMonitoringHistoryData.class_id: 'xgPON_Downstream_History',
+ XgPonUpstreamPerformanceMonitoringHistoryData.class_id: 'xgPON_Upstream_History'
+ }
+ ETHERNET_BRIDGE_HISTORY_ENABLED = True
+ ETHERNET_UNI_HISTORY_ENABLED = True
+ FEC_HISTORY_ENABLED = True
+ GEM_PORT_HISTORY_ENABLED = False
+ TRANS_CONV_HISTORY_ENABLED = False
+ XGPON_DOWNSTREAM_HISTORY = False
+ XGPON_UPSTREAM_HISTORY = False
+
+ def __init__(self, adapter_agent, device_id, logical_device_id, **kwargs):
+ super(OnuPmIntervalMetrics, self).__init__(adapter_agent, device_id, logical_device_id,
+ grouped=True, freq_override=False,
+ **kwargs)
+ ethernet_bridge_history = {
+ ('class_id', PmConfig.CONTEXT),
+ ('entity_id', PmConfig.CONTEXT),
+ ("interval_end_time", PmConfig.CONTEXT),
+ ('parent_class_id', PmConfig.CONTEXT),
+ ('parent_entity_id', PmConfig.CONTEXT),
+ ('upstream', PmConfig.CONTEXT),
+
+ ("drop_events", PmConfig.COUNTER),
+ ("octets", PmConfig.COUNTER),
+ ("packets", PmConfig.COUNTER),
+ ("broadcast_packets", PmConfig.COUNTER),
+ ("multicast_packets", PmConfig.COUNTER),
+ ("crc_errored_packets", PmConfig.COUNTER),
+ ("undersize_packets", PmConfig.COUNTER),
+ ("oversize_packets", PmConfig.COUNTER),
+ ("64_octets", PmConfig.COUNTER),
+ ("65_to_127_octets", PmConfig.COUNTER),
+ ("128_to_255_octets", PmConfig.COUNTER),
+ ("256_to_511_octets", PmConfig.COUNTER),
+ ("512_to_1023_octets", PmConfig.COUNTER),
+ ("1024_to_1518_octets", PmConfig.COUNTER)
+ }
+ self._ethernet_bridge_history_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in ethernet_bridge_history}
+
+ ethernet_uni_history = { # Ethernet History Data (Class ID 24)
+ ('class_id', PmConfig.CONTEXT),
+ ('entity_id', PmConfig.CONTEXT),
+ ("interval_end_time", PmConfig.CONTEXT),
+
+ ("fcs_errors", PmConfig.COUNTER),
+ ("excessive_collision_counter", PmConfig.COUNTER),
+ ("late_collision_counter", PmConfig.COUNTER),
+ ("frames_too_long", PmConfig.COUNTER),
+ ("buffer_overflows_on_rx", PmConfig.COUNTER),
+ ("buffer_overflows_on_tx", PmConfig.COUNTER),
+ ("single_collision_frame_counter", PmConfig.COUNTER),
+ ("multiple_collisions_frame_counter", PmConfig.COUNTER),
+ ("sqe_counter", PmConfig.COUNTER),
+ ("deferred_tx_counter", PmConfig.COUNTER),
+ ("internal_mac_tx_error_counter", PmConfig.COUNTER),
+ ("carrier_sense_error_counter", PmConfig.COUNTER),
+ ("alignment_error_counter", PmConfig.COUNTER),
+ ("internal_mac_rx_error_counter", PmConfig.COUNTER),
+ }
+ self._ethernet_uni_history_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in ethernet_uni_history}
+
+ fec_history = { # FEC History Data (Class ID 312)
+ ('class_id', PmConfig.CONTEXT),
+ ('entity_id', PmConfig.CONTEXT),
+ ("interval_end_time", PmConfig.CONTEXT),
+
+ ("corrected_bytes", PmConfig.COUNTER),
+ ("corrected_code_words", PmConfig.COUNTER),
+ ("uncorrectable_code_words", PmConfig.COUNTER),
+ ("total_code_words", PmConfig.COUNTER),
+ ("fec_seconds", PmConfig.COUNTER),
+ }
+ self._fec_history_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in fec_history}
+
+ gem_port_history = { # GEM Port Network CTP History Data (Class ID 341)
+ ('class_id', PmConfig.CONTEXT),
+ ('entity_id', PmConfig.CONTEXT),
+ ("interval_end_time", PmConfig.CONTEXT),
+
+ ("transmitted_gem_frames", PmConfig.COUNTER),
+ ("received_gem_frames", PmConfig.COUNTER),
+ ("received_payload_bytes", PmConfig.COUNTER),
+ ("transmitted_payload_bytes", PmConfig.COUNTER),
+ ("encryption_key_errors", PmConfig.COUNTER),
+ }
+ self._gem_port_history_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in gem_port_history}
+
+ xgpon_tc_history = { # XgPon TC History Data (Class ID 344)
+ ('class_id', PmConfig.CONTEXT),
+ ('entity_id', PmConfig.CONTEXT),
+ ("interval_end_time", PmConfig.CONTEXT),
+
+ ("psbd_hec_error_count", PmConfig.COUNTER),
+ ("xgtc_hec_error_count", PmConfig.COUNTER),
+ ("unknown_profile_count", PmConfig.COUNTER),
+ ("transmitted_xgem_frames", PmConfig.COUNTER),
+ ("fragment_xgem_frames", PmConfig.COUNTER),
+ ("xgem_hec_lost_words_count", PmConfig.COUNTER),
+ ("xgem_key_errors", PmConfig.COUNTER),
+ ("xgem_hec_error_count", PmConfig.COUNTER),
+ }
+ self._xgpon_tc_history_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in xgpon_tc_history}
+
+ xgpon_downstream_history = { # XgPon Downstream History Data (Class ID 345)
+ ('class_id', PmConfig.CONTEXT),
+ ('entity_id', PmConfig.CONTEXT),
+ ("interval_end_time", PmConfig.CONTEXT),
+
+ ("ploam_mic_error_count", PmConfig.COUNTER),
+ ("downstream_ploam_messages_count", PmConfig.COUNTER),
+ ("profile_messages_received", PmConfig.COUNTER),
+ ("ranging_time_messages_received", PmConfig.COUNTER),
+ ("deactivate_onu_id_messages_received", PmConfig.COUNTER),
+ ("disable_serial_number_messages_received", PmConfig.COUNTER),
+ ("request_registration_messages_received", PmConfig.COUNTER),
+ ("assign_alloc_id_messages_received", PmConfig.COUNTER),
+ ("key_control_messages_received", PmConfig.COUNTER),
+ ("sleep_allow_messages_received", PmConfig.COUNTER),
+ ("baseline_omci_messages_received_count", PmConfig.COUNTER),
+ ("extended_omci_messages_received_count", PmConfig.COUNTER),
+ ("assign_onu_id_messages_received", PmConfig.COUNTER),
+ ("omci_mic_error_count", PmConfig.COUNTER),
+ }
+ self._xgpon_downstream_history_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in xgpon_downstream_history}
+
+ xgpon_upstream_history = { # XgPon Upstream History Data (Class ID 346)
+ ('class_id', PmConfig.CONTEXT),
+ ('entity_id', PmConfig.CONTEXT),
+ ("interval_end_time", PmConfig.CONTEXT),
+
+ ("upstream_ploam_message_count", PmConfig.COUNTER),
+ ("serial_number_onu_message_count", PmConfig.COUNTER),
+ ("registration_message_count", PmConfig.COUNTER),
+ ("key_report_message_count", PmConfig.COUNTER),
+ ("acknowledge_message_count", PmConfig.COUNTER),
+ ("sleep_request_message_count", PmConfig.COUNTER),
+ }
+ self._xgpon_upstream_history_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in xgpon_upstream_history}
+ self._configs = {
+ EthernetFrameUpstreamPerformanceMonitoringHistoryData.class_id: self._ethernet_bridge_history_config,
+ EthernetFrameDownstreamPerformanceMonitoringHistoryData.class_id: self._ethernet_bridge_history_config,
+ EthernetFrameExtendedPerformanceMonitoring.class_id: self._ethernet_bridge_history_config,
+ EthernetFrameExtendedPerformanceMonitoring64Bit.class_id: self._ethernet_bridge_history_config,
+ EthernetPMMonitoringHistoryData.class_id: self._ethernet_uni_history_config,
+ FecPerformanceMonitoringHistoryData.class_id: self._fec_history_config,
+ GemPortNetworkCtpMonitoringHistoryData.class_id: self._gem_port_history_config,
+ XgPonTcPerformanceMonitoringHistoryData.class_id: self._xgpon_tc_history_config,
+ XgPonDownstreamPerformanceMonitoringHistoryData.class_id: self._xgpon_downstream_history_config,
+ XgPonUpstreamPerformanceMonitoringHistoryData.class_id: self._xgpon_upstream_history_config
+ }
+
+ def update(self, pm_config):
+ """
+ Update the PM Configuration.
+
+ For historical PM Intervals, the frequency always zero since the actual collection
+ and publishing is provided by the OpenOMCI library
+
+ :param pm_config:
+ """
+ self.log.debug('update')
+
+ try:
+ if pm_config.grouped:
+ for group in pm_config.groups:
+ group_config = self.pm_group_metrics.get(group.group_name)
+ if group_config is not None and group_config.enabled != group.enabled:
+ group_config.enabled = group.enabled
+ # TODO: For OMCI PM Metrics, tie this into add/remove of the PM Interval ME itself
+ else:
+ msg = 'There are on independent OMCI Interval metrics, only group metrics at this time'
+ raise NotImplemented(msg)
+
+ except Exception as e:
+ self.log.exception('update-failure', e=e)
+ raise
+
+ def make_proto(self, pm_config=None):
+ """
+ From the PM Configurations defined in this class's initializer, create
+ the PMConfigs protobuf message that defines our PM configuration and
+ data.
+
+ All ONU PM Interval metrics are grouped metrics that are generated autonmouslly
+ from the OpenOMCI Performance Intervals state machine.
+
+ :param pm_config (PMConfigs) PM Configuration message to add OpenOMCI config items too
+ :return: (PmConfigs) PM Configuration Protobuf message
+ """
+ assert pm_config is not None
+
+ pm_ethernet_bridge_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[EthernetFrameUpstreamPerformanceMonitoringHistoryData.class_id],
+ group_freq=0,
+ enabled=OnuPmIntervalMetrics.ETHERNET_BRIDGE_HISTORY_ENABLED)
+ self.pm_group_metrics[pm_ethernet_bridge_history.group_name] = pm_ethernet_bridge_history
+
+ for m in sorted(self._ethernet_bridge_history_config):
+ pm = self._ethernet_bridge_history_config[m]
+ pm_ethernet_bridge_history.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+
+ pm_ethernet_uni_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[EthernetPMMonitoringHistoryData.class_id],
+ group_freq=0,
+ enabled=OnuPmIntervalMetrics.ETHERNET_UNI_HISTORY_ENABLED)
+ self.pm_group_metrics[pm_ethernet_uni_history.group_name] = pm_ethernet_uni_history
+
+ for m in sorted(self._ethernet_uni_history_config):
+ pm = self._ethernet_uni_history_config[m]
+ pm_ethernet_uni_history.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+
+ pm_fec_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[FecPerformanceMonitoringHistoryData.class_id],
+ group_freq=0,
+ enabled=OnuPmIntervalMetrics.FEC_HISTORY_ENABLED)
+ self.pm_group_metrics[pm_fec_history.group_name] = pm_fec_history
+
+ for m in sorted(self._fec_history_config):
+ pm = self._fec_history_config[m]
+ pm_fec_history.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+
+ pm_gem_port_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[GemPortNetworkCtpMonitoringHistoryData.class_id],
+ group_freq=0,
+ enabled=OnuPmIntervalMetrics.GEM_PORT_HISTORY_ENABLED)
+ self.pm_group_metrics[pm_gem_port_history.group_name] = pm_gem_port_history
+
+ for m in sorted(self._gem_port_history_config):
+ pm = self._gem_port_history_config[m]
+ pm_gem_port_history.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+
+ pm_xgpon_tc_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[XgPonTcPerformanceMonitoringHistoryData.class_id],
+ group_freq=0,
+ enabled=OnuPmIntervalMetrics.TRANS_CONV_HISTORY_ENABLED)
+ self.pm_group_metrics[pm_xgpon_tc_history.group_name] = pm_xgpon_tc_history
+
+ for m in sorted(self._xgpon_tc_history_config):
+ pm = self._xgpon_tc_history_config[m]
+ pm_xgpon_tc_history.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+
+ pm_xgpon_downstream_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[XgPonDownstreamPerformanceMonitoringHistoryData.class_id],
+ group_freq=0,
+ enabled=OnuPmIntervalMetrics.XGPON_DOWNSTREAM_HISTORY)
+ self.pm_group_metrics[pm_xgpon_downstream_history.group_name] = pm_xgpon_downstream_history
+
+ for m in sorted(self._xgpon_downstream_history_config):
+ pm = self._xgpon_downstream_history_config[m]
+ pm_xgpon_downstream_history.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+
+ pm_xgpon_upstream_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[XgPonUpstreamPerformanceMonitoringHistoryData.class_id],
+ group_freq=0,
+ enabled=OnuPmIntervalMetrics.XGPON_UPSTREAM_HISTORY)
+ self.pm_group_metrics[pm_xgpon_upstream_history.group_name] = pm_xgpon_upstream_history
+
+ for m in sorted(self._xgpon_upstream_history_config):
+ pm = self._xgpon_upstream_history_config[m]
+ pm_xgpon_upstream_history.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+
+ pm_config.groups.extend([stats for stats in self.pm_group_metrics.itervalues()])
+
+ return pm_config
+
+ def publish_metrics(self, interval_data):
+ """
+ Collect the metrics for this ONU PM Interval
+
+ :param interval_data: (dict) PM interval dictionary with structure of
+ {
+ 'class_id': self._class_id,
+ 'entity_id': self._entity_id,
+ 'me_name': self._entity.__name__, # Mostly for debugging...
+ 'interval_utc_time': None,
+ # Counters added here as they are retrieved
+ }
+
+ :return: (dict) Key/Value of metric data
+ """
+ self.log.debug('publish-metrics')
+
+ try:
+ # Locate config
+ now = arrow.utcnow()
+ class_id = interval_data['class_id']
+ config = self._configs.get(class_id)
+ group = self.pm_group_metrics.get(OnuPmIntervalMetrics.ME_ID_INFO.get(class_id, ''))
+
+ if config is not None and group is not None and group.enabled:
+ # Extract only the metrics we need to publish
+ metrics = dict()
+ context = {
+ 'interval_start_time': str(now.replace(minute=int(now.minute / 15) * 15,
+ second=0,
+ microsecond=0).timestamp)
+ }
+ for metric, config_item in config.items():
+ if config_item.type == PmConfig.CONTEXT and metric in interval_data:
+ context[metric] = str(interval_data[metric])
+
+ elif (config_item.type in (PmConfig.COUNTER, PmConfig.GAUGE, PmConfig.STATE) and
+ metric in interval_data and
+ config_item.enabled):
+ metrics[metric] = interval_data[metric]
+
+ if len(metrics):
+ metadata = MetricMetaData(title=group.group_name,
+ ts=now.float_timestamp,
+ logical_device_id=self.logical_device_id,
+ serial_no=self.serial_number,
+ device_id=self.device_id,
+ context=context)
+ slice_data = [MetricInformation(metadata=metadata, metrics=metrics)]
+
+ kpi_event = KpiEvent2(type=KpiEventType.slice,
+ ts=now.float_timestamp,
+ slice_data=slice_data)
+ self.adapter_agent.submit_kpis(kpi_event)
+
+ except Exception as e:
+ self.log.exception('failed-to-submit-kpis', e=e)
diff --git a/python/adapters/extensions/kpi/onu/onu_pm_metrics.py b/python/adapters/extensions/kpi/onu/onu_pm_metrics.py
new file mode 100644
index 0000000..c94136a
--- /dev/null
+++ b/python/adapters/extensions/kpi/onu/onu_pm_metrics.py
@@ -0,0 +1,171 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from voltha.protos.device_pb2 import PmConfig, PmConfigs, PmGroupConfig
+from voltha.extensions.kpi.adapter_pm_metrics import AdapterPmMetrics
+from voltha.extensions.kpi.onu.onu_omci_pm import OnuOmciPmMetrics
+
+
+class OnuPmMetrics(AdapterPmMetrics):
+ """
+ Shared ONU Device Adapter PM Metrics Manager
+
+ This class specifically addresses ONU general PM (health, ...) area
+ specific PM (OMCI, PON, UNI) is supported in encapsulated classes accessible
+ from this object
+ """
+
+ # Metric default settings
+ DEFAULT_HEARTBEAT_ENABLED = False
+ DEFAULT_HEARTBEAT_FREQUENCY = 1200 # 1/10ths of a second
+ #
+ # Currently only a single KPI metrics collection occurs (individual group
+ # frequency not supported). The next value defines this single frequency until
+ # the KPI shared library supports individual collection.
+ DEFAULT_ONU_COLLECTION_FREQUENCY = 60 * 10 # 1 minute
+
+ def __init__(self, adapter_agent, device_id, logical_device_id,
+ grouped=False, freq_override=False, **kwargs):
+ """
+ Initializer for shared ONU Device Adapter PM metrics
+
+ :param adapter_agent: (AdapterAgent) Adapter agent for the device
+ :param device_id: (str) Device ID
+ :param logical_device_id: (str) VOLTHA Logical Device ID
+ :param grouped: (bool) Flag indicating if statistics are managed as a group
+ :param freq_override: (bool) Flag indicating if frequency collection can be specified
+ on a per group basis
+ :param kwargs: (dict) Device Adapter specific values. For an ONU Device adapter, the
+ expected key-value pairs are listed below. If not provided, the
+ associated PMv statistics are not gathered:
+
+ 'heartbeat': Reference to the a class that provides an ONU heartbeat
+ statistics. TODO: This should be standardized across adapters
+ """
+ super(OnuPmMetrics, self).__init__(adapter_agent, device_id, logical_device_id,
+ grouped=grouped, freq_override=freq_override,
+ **kwargs)
+
+ # The following HeartBeat PM is only an example. We may want to have a common heartbeat
+ # object for OLT and ONU DAs that work the same. If so, it could also provide PM information
+ #
+ # TODO: In the actual 'collection' of PM data, I have the heartbeat stats disabled since
+ # there is not yet a common 'heartbeat' object
+ #
+ self.health_pm_names = {
+ ('alarm_active', PmConfig.STATE),
+ ('heartbeat_count', PmConfig.COUNTER),
+ ('heartbeat_miss', PmConfig.COUNTER),
+ ('alarms_raised_count', PmConfig.COUNTER),
+ ('heartbeat_failed_limit', PmConfig.COUNTER),
+ ('heartbeat_interval', PmConfig.COUNTER),
+ }
+ # TODO Add PON Port pollable PM as a separate class and include like OMCI
+ # TODO Add UNI Port pollable PM as a separate class and include like OMCI
+ self._heartbeat = kwargs.pop('heartbeat', None)
+ self.health_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in self.health_pm_names}
+
+ self.omci_pm = OnuOmciPmMetrics(adapter_agent, device_id, logical_device_id,
+ grouped=grouped, freq_override=freq_override,
+ **kwargs)
+
+ def update(self, pm_config):
+ try:
+ # TODO: Test frequency override capability for a particular group
+ if self.default_freq != pm_config.default_freq:
+ # Update the callback to the new frequency.
+ self.default_freq = pm_config.default_freq
+ self.lc.stop()
+ self.lc.start(interval=self.default_freq / 10)
+
+ if pm_config.grouped:
+ for group in pm_config.groups:
+ group_config = self.pm_group_metrics.get(group.group_name)
+ if group_config is not None:
+ group_config.enabled = group.enabled
+ else:
+ msg = 'There are no independent ONU metrics, only group metrics at this time'
+ raise NotImplemented(msg)
+
+ except Exception as e:
+ self.log.exception('update-failure', e=e)
+ raise
+
+ self.omci_pm.update(pm_config)
+
+ def make_proto(self, pm_config=None):
+ if pm_config is None:
+ pm_config = PmConfigs(id=self.device_id,
+ default_freq=self.default_freq,
+ grouped=self.grouped,
+ freq_override=self.freq_override)
+ metrics = set()
+
+ if self._heartbeat is not None:
+ if self.grouped:
+ pm_health_stats = PmGroupConfig(group_name='Heartbeat',
+ group_freq=OnuPmMetrics.DEFAULT_HEARTBEAT_FREQUENCY,
+ enabled=OnuPmMetrics.DEFAULT_HEARTBEAT_ENABLED)
+ self.pm_group_metrics[pm_health_stats.group_name] = pm_health_stats
+ else:
+ pm_health_stats = pm_config
+
+ # Add metrics to the PM Group (or as individual metrics_
+ for m in sorted(self.health_metrics_config):
+ pm = self.health_metrics_config[m]
+ if not self.grouped:
+ if pm.name in metrics:
+ continue
+ metrics.add(pm.name)
+
+ pm_health_stats.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+ if self.grouped:
+ pm_config.groups.extend([pm_health_stats])
+
+ # TODO Add PON Port PM
+ # TODO Add UNI Port PM
+ pm_config = self.omci_pm.make_proto(pm_config)
+ return pm_config
+
+ def collect_metrics(self, data=None):
+ """
+ Collect metrics for this adapter.
+
+ The data collected (or passed in) is a list of pairs/tuples. Each
+ pair is composed of a MetricMetaData metadata-portion and list of MetricValuePairs
+ that contains a single individual metric or list of metrics if this is a
+ group metric.
+
+ This method is called for each adapter at a fixed frequency.
+ TODO: Currently all group metrics are collected on a single timer tick.
+ This needs to be fixed as independent group or instance collection is
+ desirable.
+
+ :param data: (list) Existing list of collected metrics (MetricInformation).
+ This is provided to allow derived classes to call into
+ further encapsulated classes.
+
+ :return: (list) metadata and metrics pairs - see description above
+ """
+ if data is None:
+ data = list()
+
+ # TODO: Heartbeat stats disabled since it is not a common item on all ONUs (or OLTs)
+ # if self._heartbeat is not None:
+ # data.extend(self.collect_metrics(self._heartbeat, self.health_pm_names,
+ # self.health_metrics_config))
+ return self.omci_pm.collect_metrics(data=data)
diff --git a/python/adapters/extensions/omci/__init__.py b/python/adapters/extensions/omci/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/omci/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/omci/database/__init__.py b/python/adapters/extensions/omci/database/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/omci/database/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/omci/database/alarm_db_ext.py b/python/adapters/extensions/omci/database/alarm_db_ext.py
new file mode 100644
index 0000000..2af6923
--- /dev/null
+++ b/python/adapters/extensions/omci/database/alarm_db_ext.py
@@ -0,0 +1,698 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from mib_db_api import *
+from voltha.protos.omci_alarm_db_pb2 import AlarmInstanceData, AlarmClassData, \
+ AlarmDeviceData, AlarmAttributeData
+
+
+class AlarmDbExternal(MibDbApi):
+ """
+ A persistent external OpenOMCI Alarm Database
+ """
+ CURRENT_VERSION = 1 # VOLTHA v1.3.0 release
+ ALARM_BITMAP_KEY = 'alarm_bit_map'
+
+ _TIME_FORMAT = '%Y%m%d-%H%M%S.%f'
+
+ # Paths from root proxy
+ ALARM_PATH = '/omci_alarms'
+ DEVICE_PATH = ALARM_PATH + '/{}' # .format(device_id)
+
+ # Classes, Instances, and Attributes as lists from root proxy
+ CLASSES_PATH = DEVICE_PATH + '/classes' # .format(device_id)
+ INSTANCES_PATH = DEVICE_PATH + '/classes/{}/instances' # .format(device_id, class_id)
+ ATTRIBUTES_PATH = DEVICE_PATH + '/classes/{}/instances/{}/attributes' # .format(device_id, class_id, instance_id)
+
+ # Single Class, Instance, and Attribute as objects from device proxy
+ CLASS_PATH = '/classes/{}' # .format(class_id)
+ INSTANCE_PATH = '/classes/{}/instances/{}' # .format(class_id, instance_id)
+ ATTRIBUTE_PATH = '/classes/{}/instances/{}/attributes/{}' # .format(class_id, instance_id
+ # attribute_name)
+
+ def __init__(self, omci_agent):
+ """
+ Class initializer
+ :param omci_agent: (OpenOMCIAgent) OpenOMCI Agent
+ """
+ super(AlarmDbExternal, self).__init__(omci_agent)
+ self._core = omci_agent.core
+
+ def start(self):
+ """
+ Start up/restore the database
+ """
+ self.log.debug('start')
+
+ if not self._started:
+ super(AlarmDbExternal, self).start()
+ root_proxy = self._core.get_proxy('/')
+
+ try:
+ base = root_proxy.get(AlarmDbExternal.ALARM_PATH)
+ self.log.info('db-exists', num_devices=len(base))
+
+ except Exception as e:
+ self.log.exception('start-failure', e=e)
+ raise
+
+ def stop(self):
+ """
+ Start up the database
+ """
+ self.log.debug('stop')
+
+ if self._started:
+ super(AlarmDbExternal, self).stop()
+ # TODO: Delete this method if nothing else is done except calling the base class
+
+ def _time_to_string(self, time):
+ return time.strftime(AlarmDbExternal._TIME_FORMAT) if time is not None else ''
+
+ def _string_to_time(self, time):
+ return datetime.strptime(time, AlarmDbExternal._TIME_FORMAT) if len(time) else None
+
+ def _attribute_to_string(self, value):
+ """
+ Convert an ME's attribute value to string representation
+
+ :param value: (long) Alarm bitmaps are always a Long
+ :return: (str) String representation of the value
+ """
+ return str(value)
+
+ def _string_to_attribute(self, str_value):
+ """
+ Convert an ME's attribute value-string to its Scapy decode equivalent
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) Class ID
+ :param attr_name: (str) Attribute Name (see EntityClasses)
+ :param str_value: (str) Attribute Value in string form
+
+ :return: (various) String representation of the value
+ :raises KeyError: Device, Class ID, or Attribute does not exist
+ """
+ # Alarms are always a bitmap which is a long
+ return long(str_value) if len(str_value) else 0L
+
+ def add(self, device_id, overwrite=False):
+ """
+ Add a new ONU to database
+
+ :param device_id: (str) Device ID of ONU to add
+ :param overwrite: (bool) Overwrite existing entry if found.
+
+ :raises KeyError: If device already exists and 'overwrite' is False
+ """
+ self.log.debug('add-device', device_id=device_id, overwrite=overwrite)
+
+ now = datetime.utcnow()
+ found = False
+ root_proxy = self._core.get_proxy('/')
+
+ data = AlarmDeviceData(device_id=device_id,
+ created=self._time_to_string(now),
+ version=AlarmDbExternal.CURRENT_VERSION,
+ last_alarm_sequence=0)
+ try:
+ dev_proxy = self._device_proxy(device_id)
+ found = True
+
+ if not overwrite:
+ # Device already exists
+ raise KeyError('Device with ID {} already exists in Alarm database'.
+ format(device_id))
+
+ # Overwrite with new data
+ data = dev_proxy.get('/', depth=0)
+ self._root_proxy.update(AlarmDbExternal.DEVICE_PATH.format(device_id), data)
+ self._modified = now
+
+ except KeyError:
+ if found:
+ raise
+ # Did not exist, add it now
+ root_proxy.add(AlarmDbExternal.ALARM_PATH, data)
+ self._created = now
+ self._modified = now
+
+ def remove(self, device_id):
+ """
+ Remove an ONU from the database
+
+ :param device_id: (str) Device ID of ONU to remove from database
+ """
+ self.log.debug('remove-device', device_id=device_id)
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ try:
+ # self._root_proxy.get(AlarmDbExternal.DEVICE_PATH.format(device_id))
+ self._root_proxy.remove(AlarmDbExternal.DEVICE_PATH.format(device_id))
+ self._modified = datetime.utcnow()
+
+ except KeyError:
+ # Did not exists, which is not a failure
+ pass
+
+ except Exception as e:
+ self.log.exception('remove-exception', device_id=device_id, e=e)
+ raise
+
+ @property
+ def _root_proxy(self):
+ return self._core.get_proxy('/')
+
+ def _device_proxy(self, device_id):
+ """
+ Return a config proxy to the OMCI Alarm_DB leaf for a given device
+
+ :param device_id: (str) ONU Device ID
+ :return: (ConfigProxy) Configuration proxy rooted at OMCI Alarm DB
+ :raises KeyError: If the device does not exist in the database
+ """
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ return self._core.get_proxy(AlarmDbExternal.DEVICE_PATH.format(device_id))
+
+ def _class_proxy(self, device_id, class_id, create=False):
+ """
+ Get a config proxy to a specific managed entity class
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) Class ID
+ :param create: (bool) If true, create default instance (and class)
+ :return: (ConfigProxy) Class configuration proxy
+
+ :raises DatabaseStateError: If database is not started
+ :raises KeyError: If Instance does not exist and 'create' is False
+ """
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not 0 <= class_id <= 0xFFFF:
+ raise ValueError('class-id is 0..0xFFFF')
+
+ fmt = AlarmDbExternal.DEVICE_PATH + AlarmDbExternal.CLASS_PATH
+ path = fmt.format(device_id, class_id)
+
+ try:
+ return self._core.get_proxy(path)
+
+ except KeyError:
+ if not create:
+ self.log.error('class-proxy-does-not-exist', device_id=device_id,
+ class_id=class_id)
+ raise
+
+ # Create class
+ data = AlarmClassData(class_id=class_id)
+ root_path = AlarmDbExternal.CLASSES_PATH.format(device_id)
+ self._root_proxy.add(root_path, data)
+
+ return self._core.get_proxy(path)
+
+ def _instance_proxy(self, device_id, class_id, instance_id, create=False):
+ """
+ Get a config proxy to a specific managed entity instance
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) Class ID
+ :param instance_id: (int) Instance ID
+ :param create: (bool) If true, create default instance (and class)
+ :return: (ConfigProxy) Instance configuration proxy
+
+ :raises DatabaseStateError: If database is not started
+ :raises KeyError: If Instance does not exist and 'create' is False
+ """
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID is a string')
+
+ if not 0 <= class_id <= 0xFFFF:
+ raise ValueError('class-id is 0..0xFFFF')
+
+ if not 0 <= instance_id <= 0xFFFF:
+ raise ValueError('instance-id is 0..0xFFFF')
+
+ fmt = AlarmDbExternal.DEVICE_PATH + AlarmDbExternal.INSTANCE_PATH
+ path = fmt.format(device_id, class_id, instance_id)
+
+ try:
+ return self._core.get_proxy(path)
+
+ except KeyError:
+ if not create:
+ self.log.error('instance-proxy-does-not-exist', device_id=device_id,
+ class_id=class_id, instance_id=instance_id)
+ raise
+
+ # Create instance, first make sure class exists
+ self._class_proxy(device_id, class_id, create=True)
+
+ now = self._time_to_string(datetime.utcnow())
+ data = AlarmInstanceData(instance_id=instance_id, created=now, modified=now)
+ root_path = AlarmDbExternal.INSTANCES_PATH.format(device_id, class_id)
+ self._root_proxy.add(root_path, data)
+
+ return self._core.get_proxy(path)
+
+ def save_last_sync_time(self, device_id, value):
+ """
+ Save the Last Sync time to the database in an easy location to access
+
+ :param device_id: (str) ONU Device ID
+ :param value: (DateTime) Value to save
+ """
+ self.log.debug('save-last-sync-time', device_id=device_id, time=str(value))
+
+ try:
+ if not isinstance(value, datetime):
+ raise TypeError('Expected a datetime object, got {}'.
+ format(type(datetime)))
+
+ device_proxy = self._device_proxy(device_id)
+ data = device_proxy.get(depth=0)
+
+ now = datetime.utcnow()
+ data.last_sync_time = self._time_to_string(value)
+
+ # Update
+ self._root_proxy.update(AlarmDbExternal.DEVICE_PATH.format(device_id),
+ data)
+ self._modified = now
+ self.log.debug('save-sync-time-complete', device_id=device_id)
+
+ except Exception as e:
+ self.log.exception('save-last-sync-exception', device_id=device_id, e=e)
+ raise
+
+ def get_last_sync_time(self, device_id):
+ """
+ Get the Last Sync Time saved to the database for a device
+
+ :param device_id: (str) ONU Device ID
+ :return: (int) The Value or None if not found
+ """
+ self.log.debug('get-last-sync-time', device_id=device_id)
+
+ try:
+ device_proxy = self._device_proxy(device_id)
+ data = device_proxy.get(depth=0)
+ return self._string_to_time(data.last_sync_time)
+
+ except KeyError:
+ return None # OMCI MIB_DB entry has not yet been created
+
+ except Exception as e:
+ self.log.exception('get-last-sync-time-exception', e=e)
+ raise
+
+ def save_alarm_last_sync(self, device_id, value):
+ """
+ Save the Last Alarm Sequence value to the database in an easy location to access
+
+ :param device_id: (str) ONU Device ID
+ :param value: (int) Value to save
+ """
+ self.log.debug('save-last-sync', device_id=device_id, seq=str(value))
+
+ try:
+ if not isinstance(value, int):
+ raise TypeError('Expected a integer, got {}'.format(type(value)))
+
+ device_proxy = self._device_proxy(device_id)
+ data = device_proxy.get(depth=0)
+
+ now = datetime.utcnow()
+ data.last_alarm_sequence = int(value)
+
+ # Update
+ self._root_proxy.update(AlarmDbExternal.DEVICE_PATH.format(device_id),
+ data)
+ self._modified = now
+ self.log.debug('save-sequence-complete', device_id=device_id)
+
+ except Exception as e:
+ self.log.exception('save-last-sync-exception', device_id=device_id, e=e)
+ raise
+
+ def get_alarm_last_sync(self, device_id):
+ """
+ Get the Last Sync Time saved to the database for a device
+
+ :param device_id: (str) ONU Device ID
+ :return: (int) The Value or None if not found
+ """
+ self.log.debug('get-last-sync', device_id=device_id)
+
+ try:
+ device_proxy = self._device_proxy(device_id)
+ data = device_proxy.get(depth=0)
+ return int(data.last_alarm_sequence)
+
+ except KeyError:
+ return None # OMCI ALARM_DB entry has not yet been created
+
+ except Exception as e:
+ self.log.exception('get-last-alarm-exception', e=e)
+ raise
+
+ def _add_new_class(self, device_id, class_id, instance_id, attributes):
+ """
+ Create an entry for a new class in the external database
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param instance_id: (int) ME Entity ID
+ :param attributes: (dict) Attribute dictionary
+
+ :returns: (bool) True if the value was saved to the database. False if the
+ value was identical to the current instance
+ """
+ self.log.debug('add', device_id=device_id, class_id=class_id,
+ instance_id=instance_id, attributes=attributes)
+
+ now = self._time_to_string(datetime.utcnow())
+ attrs = [AlarmAttributeData(name=k,
+ value=self._attribute_to_string(v)) for k, v in attributes.items()]
+ class_data = AlarmClassData(class_id=class_id,
+ instances=[AlarmInstanceData(instance_id=instance_id,
+ created=now,
+ modified=now,
+ attributes=attrs)])
+
+ self._root_proxy.add(AlarmDbExternal.CLASSES_PATH.format(device_id), class_data)
+ self.log.debug('set-complete', device_id=device_id, class_id=class_id,
+ entity_id=instance_id, attributes=attributes)
+ return True
+
+ def _add_new_instance(self, device_id, class_id, instance_id, attributes):
+ """
+ Create an entry for a instance of an existing class in the external database
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param instance_id: (int) ME Entity ID
+ :param attributes: (dict) Attribute dictionary
+
+ :returns: (bool) True if the value was saved to the database. False if the
+ value was identical to the current instance
+ """
+ self.log.debug('add', device_id=device_id, class_id=class_id,
+ instance_id=instance_id, attributes=attributes)
+
+ now = self._time_to_string(datetime.utcnow())
+ attrs = [AlarmAttributeData(name=k,
+ value=self._attribute_to_string(v)) for k, v in attributes.items()]
+ instance_data = AlarmInstanceData(instance_id=instance_id,
+ created=now,
+ modified=now,
+ attributes=attrs)
+
+ self._root_proxy.add(AlarmDbExternal.INSTANCES_PATH.format(device_id, class_id),
+ instance_data)
+
+ self.log.debug('set-complete', device_id=device_id, class_id=class_id,
+ entity_id=instance_id, attributes=attributes)
+ return True
+
+ def set(self, device_id, class_id, instance_id, attributes):
+ """
+ Set a database value. This should only be called by the Alarm synchronizer
+ and its related tasks
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param instance_id: (int) ME Entity ID
+ :param attributes: (dict) Attribute dictionary
+
+ :returns: (bool) True if the value was saved to the database. False if the
+ value was identical to the current instance
+
+ :raises KeyError: If device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ self.log.debug('set', device_id=device_id, class_id=class_id,
+ instance_id=instance_id, attributes=attributes)
+ try:
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be a string')
+
+ if not 0 <= class_id <= 0xFFFF:
+ raise ValueError("Invalid Class ID: {}, should be 0..65535".format(class_id))
+
+ if not 0 <= instance_id <= 0xFFFF:
+ raise ValueError("Invalid Instance ID: {}, should be 0..65535".format(instance_id))
+
+ if not isinstance(attributes, dict):
+ raise TypeError("Attributes should be a dictionary")
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ # Determine the best strategy to add the information
+ dev_proxy = self._device_proxy(device_id)
+
+ try:
+ class_data = dev_proxy.get(AlarmDbExternal.CLASS_PATH.format(class_id), deep=True)
+
+ inst_data = next((inst for inst in class_data.instances
+ if inst.instance_id == instance_id), None)
+
+ if inst_data is None:
+ return self._add_new_instance(device_id, class_id, instance_id, attributes)
+
+ # Possibly adding to or updating an existing instance
+ # Get instance proxy, creating it if needed
+
+ exist_attr_indexes = dict()
+ attr_len = len(inst_data.attributes)
+
+ for index in xrange(0, attr_len):
+ exist_attr_indexes[inst_data.attributes[index].name] = index
+
+ modified = False
+ str_value = ''
+ new_attributes = []
+
+ for k, v in attributes.items():
+ try:
+ str_value = self._attribute_to_string(v)
+ new_attributes.append(AlarmAttributeData(name=k, value=str_value))
+
+ except Exception as e:
+ self.log.exception('save-error', e=e, class_id=class_id,
+ attr=k, value_type=type(v))
+
+ if k not in exist_attr_indexes or \
+ inst_data.attributes[exist_attr_indexes[k]].value != str_value:
+ modified = True
+
+ if modified:
+ now = datetime.utcnow()
+ new_data = AlarmInstanceData(instance_id=instance_id,
+ created=inst_data.created,
+ modified=self._time_to_string(now),
+ attributes=new_attributes)
+ dev_proxy.remove(AlarmDbExternal.INSTANCE_PATH.format(class_id, instance_id))
+ self._root_proxy.add(AlarmDbExternal.INSTANCES_PATH.format(device_id,
+ class_id), new_data)
+
+ self.log.debug('set-complete', device_id=device_id, class_id=class_id,
+ entity_id=instance_id, attributes=attributes, modified=modified)
+ return modified
+
+ except KeyError:
+ # Here if the class-id does not yet exist in the database
+ return self._add_new_class(device_id, class_id, instance_id,
+ attributes)
+ except Exception as e:
+ self.log.exception('set-exception', device_id=device_id, class_id=class_id,
+ instance_id=instance_id, attributes=attributes, e=e)
+ raise
+
+ def delete(self, device_id, class_id, entity_id):
+ """
+ Delete an entity from the database if it exists. If all instances
+ of a class are deleted, the class is deleted as well.
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param entity_id: (int) ME Entity ID
+
+ :returns: (bool) True if the instance was found and deleted. False
+ if it did not exist.
+
+ :raises KeyError: If device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ self.log.debug('delete', device_id=device_id, class_id=class_id,
+ entity_id=entity_id)
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ if not 0 <= class_id <= 0xFFFF:
+ raise ValueError('class-id is 0..0xFFFF')
+
+ if not 0 <= entity_id <= 0xFFFF:
+ raise ValueError('instance-id is 0..0xFFFF')
+
+ try:
+ # Remove instance
+ self._instance_proxy(device_id, class_id, entity_id).remove('/')
+ now = datetime.utcnow()
+
+ # If resulting class has no instance, remove it as well
+ class_proxy = self._class_proxy(device_id, class_id)
+ class_data = class_proxy.get('/', depth=1)
+
+ if len(class_data.instances) == 0:
+ class_proxy.remove('/')
+
+ self._modified = now
+ return True
+
+ except KeyError:
+ return False # Not found
+
+ except Exception as e:
+ self.log.exception('get-last-data-exception', device_id=device_id, e=e)
+ raise
+
+ def query(self, device_id, class_id=None, instance_id=None, attributes=None):
+ """
+ Get database information.
+
+ This method can be used to request information from the database to the detailed
+ level requested
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) Managed Entity class ID
+ :param instance_id: (int) Managed Entity instance
+ :param attributes: (list/set or str) Managed Entity instance's attributes
+
+ :return: (dict) The value(s) requested. If class/inst/attribute is
+ not found, an empty dictionary is returned
+ :raises KeyError: If the requested device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ self.log.debug('query', device_id=device_id, class_id=class_id,
+ instance_id=instance_id, attributes=attributes)
+ try:
+ if class_id is None:
+ # Get full device info
+ dev_data = self._device_proxy(device_id).get('/', depth=-1)
+ data = self._device_to_dict(dev_data)
+
+ elif instance_id is None:
+ # Get all instances of the class
+ try:
+ cls_data = self._class_proxy(device_id, class_id).get('/', depth=-1)
+ data = self._class_to_dict(cls_data)
+
+ except KeyError:
+ data = dict()
+
+ else:
+ # Get all attributes of a specific ME
+ try:
+ inst_data = self._instance_proxy(device_id, class_id, instance_id).\
+ get('/', depth=-1)
+
+ if attributes is None:
+ # All Attributes
+ data = self._instance_to_dict(inst_data)
+
+ else:
+ # Specific attribute(s)
+ if isinstance(attributes, basestring):
+ attributes = {attributes}
+
+ data = {
+ attr.name: self._string_to_attribute(attr.value)
+ for attr in inst_data.attributes if attr.name in attributes}
+
+ except KeyError:
+ data = dict()
+
+ return data
+
+ except KeyError:
+ self.log.warn('query-no-device', device_id=device_id)
+ raise
+
+ except Exception as e:
+ self.log.exception('get-last-sync-exception', device_id=device_id, e=e)
+ raise
+
+ def _instance_to_dict(self, instance):
+ if not isinstance(instance, AlarmInstanceData):
+ raise TypeError('{} is not of type AlarmInstanceData'.format(type(instance)))
+
+ data = {
+ INSTANCE_ID_KEY: instance.instance_id,
+ CREATED_KEY: self._string_to_time(instance.created),
+ MODIFIED_KEY: self._string_to_time(instance.modified),
+ ATTRIBUTES_KEY: dict()
+ }
+ for attribute in instance.attributes:
+ data[ATTRIBUTES_KEY][attribute.name] = self._string_to_attribute(attribute.value)
+ return data
+
+ def _class_to_dict(self, val):
+ if not isinstance(val, AlarmClassData):
+ raise TypeError('{} is not of type AlarmClassData'.format(type(val)))
+
+ data = {
+ CLASS_ID_KEY: val.class_id,
+ }
+ for instance in val.instances:
+ data[instance.instance_id] = self._instance_to_dict(instance)
+ return data
+
+ def _device_to_dict(self, val):
+ if not isinstance(val, AlarmDeviceData):
+ raise TypeError('{} is not of type AlarmDeviceData'.format(type(val)))
+
+ data = {
+ DEVICE_ID_KEY: val.device_id,
+ CREATED_KEY: self._string_to_time(val.created),
+ VERSION_KEY: val.version,
+ ME_KEY: dict(),
+ MSG_TYPE_KEY: set()
+ }
+ for class_data in val.classes:
+ data[class_data.class_id] = self._class_to_dict(class_data)
+ for managed_entity in val.managed_entities:
+ data[ME_KEY][managed_entity.class_id] = managed_entity.name
+
+ for msg_type in val.message_types:
+ data[MSG_TYPE_KEY].add(msg_type.message_type)
+
+ return data
diff --git a/python/adapters/extensions/omci/database/mib_db_api.py b/python/adapters/extensions/omci/database/mib_db_api.py
new file mode 100644
index 0000000..eb93323
--- /dev/null
+++ b/python/adapters/extensions/omci/database/mib_db_api.py
@@ -0,0 +1,245 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+OpenOMCI MIB Database API
+"""
+
+import structlog
+from datetime import datetime
+
+CREATED_KEY = 'created'
+MODIFIED_KEY = 'modified'
+MDS_KEY = 'mib_data_sync'
+LAST_SYNC_KEY = 'last_mib_sync'
+VERSION_KEY = 'version'
+DEVICE_ID_KEY = 'device_id'
+CLASS_ID_KEY = 'class_id'
+INSTANCE_ID_KEY = 'instance_id'
+ATTRIBUTES_KEY = 'attributes'
+ME_KEY = 'managed_entities'
+MSG_TYPE_KEY = 'message_types'
+
+
+class DatabaseStateError(Exception):
+ def __init__(self, *args):
+ Exception.__init__(self, *args)
+
+
+class MibDbApi(object):
+ """
+ MIB Database API Base Class
+
+ Derive the ME MIB Database implementation from this API. For an example
+ implementation, look at the mib_db_dict.py implementation
+ """
+ def __init__(self, omci_agent):
+ """
+ Class initializer
+ :param omci_agent: (OpenOMCIAgent) OpenOMCI Agent
+ """
+ self.log = structlog.get_logger()
+ self._omci_agent = omci_agent
+ self._started = False
+
+ now = datetime.utcnow()
+ self._created = now
+ self._modified = now
+
+ def start(self):
+ """
+ Start up/restore the database. For in-memory, will be a nop. For external
+ DB, may need to create the DB and fetch create/modified values
+ """
+ if not self._started:
+ self._started = True
+ # For a derived class that is a persistent DB, Restore DB (connect,
+ # get created/modified times, ....) or something along those lines.
+ # Minimal restore could just be getting ONU device IDs' so they are cached
+ # locally. Maximum restore would be a full in-memory version of database
+ # for fast 'GET' request support.
+ # Remember to restore the '_created' and '_modified' times (above) as well
+ # from the database
+
+ def stop(self):
+ """
+ Start up the database. For in-memory, will be a nop. For external
+ DB, may need to create the DB and fetch create/modified values
+ """
+ if self._started:
+ self._started = False
+
+ @property
+ def active(self):
+ """
+ Is the database active
+ :return: (bool) True if active
+ """
+ return self._started
+
+ @property
+ def created(self):
+ """
+ Date (UTC) that the database was created
+ :return: (datetime) creation date
+ """
+ return self._created
+
+ @property
+ def modified(self):
+ """
+ Date (UTC) that the database last added or removed a device
+ or updated a device's ME information
+ :return: (datetime) last modification date
+ """
+ return self._modified
+
+ def add(self, device_id, overwrite=False):
+ """
+ Add a new ONU to database
+
+ :param device_id: (str) Device ID of ONU to add
+ :param overwrite: (bool) Overwrite existing entry if found.
+
+ :raises KeyError: If device already exists and 'overwrite' is False
+ """
+ raise NotImplementedError('Implement this in your derive class')
+
+ def remove(self, device_id):
+ """
+ Remove an ONU from the database
+
+ :param device_id: (str) Device ID of ONU to remove from database
+ """
+ raise NotImplementedError('Implement this in your derive class')
+
+ def set(self, device_id, class_id, entity_id, attributes):
+ """
+ Set/Create a database value. This should only be called by the MIB synchronizer
+ and its related tasks
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param entity_id: (int) ME Entity ID
+ :param attributes: (dict) Attribute dictionary
+
+ :returns: (bool) True if the value was saved to the database. False if the
+ value was identical to the current instance
+
+ :raises KeyError: If device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ raise NotImplementedError('Implement this in your derive class')
+
+ def delete(self, device_id, class_id, entity_id):
+ """
+ Delete an entity from the database if it exists
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param entity_id: (int) ME Entity ID
+
+ :returns: (bool) True if the instance was found and deleted. False
+ if it did not exist.
+
+ :raises KeyError: If device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ raise NotImplementedError('Implement this in your derive class')
+
+ def query(self, device_id, class_id=None, instance_id=None, attributes=None):
+ """
+ Get database information.
+
+ This method can be used to request information from the database to the detailed
+ level requested
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) Managed Entity class ID
+ :param instance_id: (int) Managed Entity instance
+ :param attributes: (list/set or str) Managed Entity instance's attributes
+
+ :return: (dict) The value(s) requested. If class/inst/attribute is
+ not found, an empty dictionary is returned
+ :raises KeyError: If the requested device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ raise NotImplementedError('Implement this in your derive class')
+
+ def on_mib_reset(self, device_id):
+ """
+ Reset/clear the database for a specific Device
+
+ :param device_id: (str) ONU Device ID
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ # Your derived class should clear out all MIB data and update the
+ # modified stats appropriately
+ raise NotImplementedError('Implement this in your derive class')
+
+ def save_mib_data_sync(self, device_id, value):
+ """
+ Save the MIB Data Sync to the database in an easy location to access
+
+ :param device_id: (str) ONU Device ID
+ :param value: (int) Value to save
+ """
+ raise NotImplementedError('Implement this in your derive class')
+
+ def get_mib_data_sync(self, device_id):
+ """
+ Get the MIB Data Sync value last saved to the database for a device
+
+ :param device_id: (str) ONU Device ID
+ :return: (int) The Value or None if not found
+ """
+ raise NotImplementedError('Implement this in your derive class')
+
+ def save_last_sync(self, device_id, value):
+ """
+ Save the Last Sync time to the database in an easy location to access
+
+ :param device_id: (str) ONU Device ID
+ :param value: (DateTime) Value to save
+ """
+ raise NotImplementedError('Implement this in your derive class')
+
+ def get_last_sync(self, device_id):
+ """
+ Get the Last SYnc Time saved to the database for a device
+
+ :param device_id: (str) ONU Device ID
+ :return: (int) The Value or None if not found
+ """
+ raise NotImplementedError('Implement this in your derive class')
+
+ def update_supported_managed_entities(self, device_id, managed_entities):
+ """
+ Update the supported OMCI Managed Entities for this device
+
+ :param device_id: (str) ONU Device ID
+ :param managed_entities: (set) Managed Entity class IDs
+ """
+ raise NotImplementedError('Implement this in your derive class')
+
+ def update_supported_message_types(self, device_id, msg_types):
+ """
+ Update the supported OMCI Managed Entities for this device
+
+ :param device_id: (str) ONU Device ID
+ :param msg_types: (set) Message Type values (ints)
+ """
+ raise NotImplementedError('Implement this in your derive class')
diff --git a/python/adapters/extensions/omci/database/mib_db_dict.py b/python/adapters/extensions/omci/database/mib_db_dict.py
new file mode 100644
index 0000000..6a7de8f
--- /dev/null
+++ b/python/adapters/extensions/omci/database/mib_db_dict.py
@@ -0,0 +1,524 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import copy
+from mib_db_api import *
+import json
+
+
+class MibDbVolatileDict(MibDbApi):
+ """
+ A very simple in-memory database for ME storage. Data is not persistent
+ across reboots.
+
+ In Phase 2, this DB will be instantiated on a per-ONU basis but act as if
+ it is shared for all ONUs. This class will be updated with and external
+ key-value store (or other appropriate database) in Voltha 1.3 Sprint 3
+
+ This class can be used for unit tests
+ """
+ CURRENT_VERSION = 1
+
+ def __init__(self, omci_agent):
+ """
+ Class initializer
+ :param omci_agent: (OpenOMCIAgent) OpenOMCI Agent
+ """
+ super(MibDbVolatileDict, self).__init__(omci_agent)
+ self._data = dict() # device_id -> ME ID -> Inst ID -> Attr Name -> Values
+
+ def start(self):
+ """
+ Start up/restore the database. For in-memory, will be a nop. For external
+ DB, may need to create the DB and fetch create/modified values
+ """
+ super(MibDbVolatileDict, self).start()
+ # TODO: Delete this method if nothing else is done except calling the base class
+
+ def stop(self):
+ """
+ Start up the database. For in-memory, will be a nop. For external
+ DB, may need to create the DB and fetch create/modified values
+ """
+ super(MibDbVolatileDict, self).stop()
+ # TODO: Delete this method if nothing else is done except calling the base class
+
+ def add(self, device_id, overwrite=False):
+ """
+ Add a new ONU to database
+
+ :param device_id: (str) Device ID of ONU to add
+ :param overwrite: (bool) Overwrite existing entry if found.
+
+ :raises KeyError: If device already exist and 'overwrite' is False
+ """
+ self.log.debug('add-device', device_id=device_id, overwrite=overwrite)
+
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not overwrite and device_id in self._data:
+ raise KeyError('Device {} already exists in the database'
+ .format(device_id))
+
+ now = datetime.utcnow()
+ self._data[device_id] = {
+ DEVICE_ID_KEY: device_id,
+ CREATED_KEY: now,
+ LAST_SYNC_KEY: None,
+ MDS_KEY: 0,
+ VERSION_KEY: MibDbVolatileDict.CURRENT_VERSION,
+ ME_KEY: dict(),
+ MSG_TYPE_KEY: set()
+ }
+
+ def remove(self, device_id):
+ """
+ Remove an ONU from the database
+
+ :param device_id: (str) Device ID of ONU to remove from database
+ """
+ self.log.debug('remove-device', device_id=device_id)
+
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if device_id in self._data:
+ del self._data[device_id]
+ self._modified = datetime.utcnow()
+
+ def on_mib_reset(self, device_id):
+ """
+ Reset/clear the database for a specific Device
+
+ :param device_id: (str) ONU Device ID
+ :raises DatabaseStateError: If the database is not enabled
+ :raises KeyError: If the device does not exist in the database
+ """
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ device_db = self._data[device_id]
+ self._modified = datetime.utcnow()
+
+ self._data[device_id] = {
+ DEVICE_ID_KEY: device_id,
+ CREATED_KEY: device_db[CREATED_KEY],
+ LAST_SYNC_KEY: device_db[LAST_SYNC_KEY],
+ MDS_KEY: 0,
+ VERSION_KEY: MibDbVolatileDict.CURRENT_VERSION,
+ ME_KEY: device_db[ME_KEY],
+ MSG_TYPE_KEY: device_db[MSG_TYPE_KEY]
+ }
+
+ def save_mib_data_sync(self, device_id, value):
+ """
+ Save the MIB Data Sync to the database in an easy location to access
+
+ :param device_id: (str) ONU Device ID
+ :param value: (int) Value to save
+ """
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ if not isinstance(value, int):
+ raise TypeError('MIB Data Sync is an integer')
+
+ if not 0 <= value <= 255:
+ raise ValueError('Invalid MIB-data-sync value {}. Must be 0..255'.
+ format(value))
+
+ self._data[device_id][MDS_KEY] = value
+ self._modified = datetime.utcnow()
+
+ def get_mib_data_sync(self, device_id):
+ """
+ Get the MIB Data Sync value last saved to the database for a device
+
+ :param device_id: (str) ONU Device ID
+ :return: (int) The Value or None if not found
+ """
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ if device_id not in self._data:
+ return None
+
+ return self._data[device_id].get(MDS_KEY)
+
+ def save_last_sync(self, device_id, value):
+ """
+ Save the Last Sync time to the database in an easy location to access
+
+ :param device_id: (str) ONU Device ID
+ :param value: (DateTime) Value to save
+ """
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ if not isinstance(value, datetime):
+ raise TypeError('Expected a datetime object, got {}'.
+ format(type(datetime)))
+
+ self._data[device_id][LAST_SYNC_KEY] = value
+ self._modified = datetime.utcnow()
+
+ def get_last_sync(self, device_id):
+ """
+ Get the Last SYnc Time saved to the database for a device
+
+ :param device_id: (str) ONU Device ID
+ :return: (int) The Value or None if not found
+ """
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ if device_id not in self._data:
+ return None
+
+ return self._data[device_id].get(LAST_SYNC_KEY)
+
+ def set(self, device_id, class_id, instance_id, attributes):
+ """
+ Set a database value. This should only be called by the MIB synchronizer
+ and its related tasks
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param instance_id: (int) ME Entity ID
+ :param attributes: (dict) Attribute dictionary
+
+ :returns: (bool) True if the value was saved to the database. False if the
+ value was identical to the current instance
+
+ :raises KeyError: If device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be a string')
+
+ if not 0 <= class_id <= 0xFFFF:
+ raise ValueError("Invalid Class ID: {}, should be 0..65535".format(class_id))
+
+ if not 0 <= instance_id <= 0xFFFF:
+ raise ValueError("Invalid Instance ID: {}, should be 0..65535".format(instance_id))
+
+ if not isinstance(attributes, dict):
+ raise TypeError("Attributes should be a dictionary")
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ now = datetime.utcnow()
+ try:
+ device_db = self._data[device_id]
+ class_db = device_db.get(class_id)
+ created = False
+
+ if class_db is None:
+ device_db[class_id] = {CLASS_ID_KEY: class_id}
+
+ class_db = device_db[class_id]
+ self._modified = now
+ created = True
+
+ instance_db = class_db.get(instance_id)
+ if instance_db is None:
+ class_db[instance_id] = {
+ INSTANCE_ID_KEY: instance_id,
+ CREATED_KEY: now,
+ MODIFIED_KEY: now,
+ ATTRIBUTES_KEY: dict()
+ }
+ instance_db = class_db[instance_id]
+ self._modified = now
+ created = True
+
+ changed = False
+
+ me_map = self._omci_agent.get_device(device_id).me_map
+ entity = me_map.get(class_id)
+
+ for attribute, value in attributes.items():
+ assert isinstance(attribute, basestring)
+ assert value is not None, "Attribute '{}' value cannot be 'None'".\
+ format(attribute)
+
+ db_value = instance_db[ATTRIBUTES_KEY].get(attribute) \
+ if ATTRIBUTES_KEY in instance_db else None
+
+ if entity is not None and isinstance(value, basestring):
+ from scapy.fields import StrFixedLenField
+ attr_index = entity.attribute_name_to_index_map[attribute]
+ eca = entity.attributes[attr_index]
+ field = eca.field
+
+ if isinstance(field, StrFixedLenField):
+ from scapy.base_classes import Packet_metaclass
+ if isinstance(field.default, Packet_metaclass) \
+ and hasattr(field.default, 'json_from_value'):
+ # Value/hex of Packet Class to string
+ value = field.default.json_from_value(value)
+
+ if entity is not None and attribute in entity.attribute_name_to_index_map:
+ attr_index = entity.attribute_name_to_index_map[attribute]
+ eca = entity.attributes[attr_index]
+ field = eca.field
+
+ if hasattr(field, 'to_json'):
+ value = field.to_json(value, db_value)
+
+ # Complex packet types may have an attribute encoded as an object, this
+ # can be check by seeing if there is a to_json() conversion callable
+ # defined
+ if hasattr(value, 'to_json'):
+ value = value.to_json()
+
+ # Other complex packet types may be a repeated list field (FieldListField)
+ elif isinstance(value, (list, dict)):
+ value = json.dumps(value, separators=(',', ':'))
+
+ assert db_value is None or isinstance(value, type(db_value)), \
+ "New value type for attribute '{}' type is changing from '{}' to '{}'".\
+ format(attribute, type(db_value), type(value))
+
+ if db_value is None or db_value != value:
+ instance_db[ATTRIBUTES_KEY][attribute] = value
+ changed = True
+
+ if changed:
+ instance_db[MODIFIED_KEY] = now
+ self._modified = now
+
+ return changed or created
+
+ except Exception as e:
+ self.log.error('set-failure', e=e, class_id=class_id,
+ instance_id=instance_id, attributes=attributes)
+ raise
+
+ def delete(self, device_id, class_id, instance_id):
+ """
+ Delete an entity from the database if it exists. If all instances
+ of a class are deleted, the class is deleted as well.
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param instance_id: (int) ME Entity ID
+
+ :returns: (bool) True if the instance was found and deleted. False
+ if it did not exist.
+
+ :raises KeyError: If device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ if not 0 <= class_id <= 0xFFFF:
+ raise ValueError('class-id is 0..0xFFFF')
+
+ if not 0 <= instance_id <= 0xFFFF:
+ raise ValueError('instance-id is 0..0xFFFF')
+
+ try:
+ device_db = self._data[device_id]
+ class_db = device_db.get(class_id)
+
+ if class_db is None:
+ return False
+
+ instance_db = class_db.get(instance_id)
+ if instance_db is None:
+ return False
+
+ now = datetime.utcnow()
+ del class_db[instance_id]
+
+ if len(class_db) == 1: # Is only 'CLASS_ID_KEY' remaining
+ del device_db[class_id]
+
+ self._modified = now
+ return True
+
+ except Exception as e:
+ self.log.error('delete-failure', e=e)
+ raise
+
+ def query(self, device_id, class_id=None, instance_id=None, attributes=None):
+ """
+ Get database information.
+
+ This method can be used to request information from the database to the detailed
+ level requested
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) Managed Entity class ID
+ :param instance_id: (int) Managed Entity instance
+ :param attributes: (list/set or str) Managed Entity instance's attributes
+
+ :return: (dict) The value(s) requested. If class/inst/attribute is
+ not found, an empty dictionary is returned
+ :raises KeyError: If the requested device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ self.log.debug('query', device_id=device_id, class_id=class_id,
+ instance_id=instance_id, attributes=attributes)
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID is a string')
+
+ device_db = self._data[device_id]
+ if class_id is None:
+ return self._fix_dev_json_attributes(copy.copy(device_db), device_id)
+
+ if not isinstance(class_id, int):
+ raise TypeError('Class ID is an integer')
+
+ me_map = self._omci_agent.get_device(device_id).me_map
+ entity = me_map.get(class_id)
+
+ class_db = device_db.get(class_id, dict())
+ if instance_id is None or len(class_db) == 0:
+ return self._fix_cls_json_attributes(copy.copy(class_db), entity)
+
+ if not isinstance(instance_id, int):
+ raise TypeError('Instance ID is an integer')
+
+ instance_db = class_db.get(instance_id, dict())
+ if attributes is None or len(instance_db) == 0:
+ return self._fix_inst_json_attributes(copy.copy(instance_db), entity)
+
+ if not isinstance(attributes, (basestring, list, set)):
+ raise TypeError('Attributes should be a string or list/set of strings')
+
+ if not isinstance(attributes, (list, set)):
+ attributes = [attributes]
+
+ results = {attr: val for attr, val in instance_db[ATTRIBUTES_KEY].iteritems()
+ if attr in attributes}
+
+ for attr, attr_data in results.items():
+ attr_index = entity.attribute_name_to_index_map[attr]
+ eca = entity.attributes[attr_index]
+ results[attr] = self._fix_attr_json_attribute(copy.copy(attr_data), eca)
+
+ return results
+
+ #########################################################################
+ # Following routines are used to fix-up JSON encoded complex data. A
+ # nice side effect is that the values returned will be a deep-copy of
+ # the class/instance/attribute data of what is in the database. Note
+ # That other database values (created, modified, ...) will still reference
+ # back to the original DB.
+
+ def _fix_dev_json_attributes(self, dev_data, device_id):
+ for cls_id, cls_data in dev_data.items():
+ if isinstance(cls_id, int):
+ me_map = self._omci_agent.get_device(device_id).me_map
+ entity = me_map.get(cls_id)
+ dev_data[cls_id] = self._fix_cls_json_attributes(copy.copy(cls_data), entity)
+ return dev_data
+
+ def _fix_cls_json_attributes(self, cls_data, entity):
+ for inst_id, inst_data in cls_data.items():
+ if isinstance(inst_id, int):
+ cls_data[inst_id] = self._fix_inst_json_attributes(copy.copy(inst_data), entity)
+ return cls_data
+
+ def _fix_inst_json_attributes(self, inst_data, entity):
+ if ATTRIBUTES_KEY in inst_data:
+ for attr, attr_data in inst_data[ATTRIBUTES_KEY].items():
+ attr_index = entity.attribute_name_to_index_map[attr] \
+ if entity is not None and attr in entity.attribute_name_to_index_map else None
+ eca = entity.attributes[attr_index] if attr_index is not None else None
+ inst_data[ATTRIBUTES_KEY][attr] = self._fix_attr_json_attribute(copy.copy(attr_data), eca)
+ return inst_data
+
+ def _fix_attr_json_attribute(self, attr_data, eca):
+
+ try:
+ if eca is not None:
+ field = eca.field
+ if hasattr(field, 'load_json'):
+ value = field.load_json(attr_data)
+ return value
+
+ return json.loads(attr_data) if isinstance(attr_data, basestring) else attr_data
+
+ except ValueError:
+ return attr_data
+
+ except Exception as e:
+ pass
+
+ def update_supported_managed_entities(self, device_id, managed_entities):
+ """
+ Update the supported OMCI Managed Entities for this device
+
+ :param device_id: (str) ONU Device ID
+ :param managed_entities: (set) Managed Entity class IDs
+ """
+ now = datetime.utcnow()
+ try:
+ device_db = self._data[device_id]
+
+ entities = {class_id: self._managed_entity_to_name(device_id, class_id)
+ for class_id in managed_entities}
+
+ device_db[ME_KEY] = entities
+ self._modified = now
+
+ except Exception as e:
+ self.log.error('set-me-failure', e=e)
+ raise
+
+ def _managed_entity_to_name(self, device_id, class_id):
+ me_map = self._omci_agent.get_device(device_id).me_map
+ entity = me_map.get(class_id)
+
+ return entity.__name__ if entity is not None else 'UnknownManagedEntity'
+
+ def update_supported_message_types(self, device_id, msg_types):
+ """
+ Update the supported OMCI Managed Entities for this device
+
+ :param device_id: (str) ONU Device ID
+ :param msg_types: (set) Message Type values (ints)
+ """
+ now = datetime.utcnow()
+ try:
+ msg_type_set = {msg_type.value for msg_type in msg_types}
+ self._data[device_id][MSG_TYPE_KEY] = msg_type_set
+ self._modified = now
+
+ except Exception as e:
+ self.log.error('set-me-failure', e=e)
+ raise
diff --git a/python/adapters/extensions/omci/database/mib_db_ext.py b/python/adapters/extensions/omci/database/mib_db_ext.py
new file mode 100644
index 0000000..d06a7fa
--- /dev/null
+++ b/python/adapters/extensions/omci/database/mib_db_ext.py
@@ -0,0 +1,1061 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from mib_db_api import *
+from voltha.protos.omci_mib_db_pb2 import MibInstanceData, MibClassData, \
+ MibDeviceData, MibAttributeData, MessageType, ManagedEntity
+from voltha.extensions.omci.omci_entities import *
+from voltha.extensions.omci.omci_fields import *
+from scapy.fields import StrField, FieldListField, PacketField
+
+
+class MibDbStatistic(object):
+ """
+ For debug/tuning purposes.
+
+ With etcd around the Nov 8 time frame, (took out some created/modified settins) seeing the following:
+
+ o Creates: Avg: 141.4 mS, Min: 47 mS, Max: 323 mS (148 samples)
+ o Sets: Avg: 206.4 mS, Min: 85 mS, Max: 781 mS (142 samples)
+
+ With etcd around the Nov 7 time frame, seeing the following:
+
+ o Creates: Avg: 124.4 mS, Min: 48 mS, Max: 531 mS (148 samples)
+ o Sets: Avg: 210.7 mS, Min: 82 mS, Max: 944 mS (140 samples)
+ o Gets: Avg: 10.9 mS, Min: 0 mS, Max: 384 mS ( 99 samples)
+ o Deletes: No samples
+
+ With etcd around the v1.5 time frame, seeing the following:
+
+ o Creates: Avg: 57.1 mS, Min: 76 mS, Max: 511 mS (146 samples)
+ o Sets: Avg: 303.9 mS, Min: 126 mS, Max: 689 mS (103 samples)
+ o Gets: Avg: 3.3 mS, Min: 0 mS, Max: 8 mS ( 9 samples)
+ o Deletes: No samples
+ """
+ def __init__(self, name):
+ self._name = name
+ self._count = 0
+ self._total_time = 0 # Total milliseconds
+ self._min_time = 99999999
+ self._max_time = 0
+
+ def get_statistics(self):
+ return {
+ 'name': self._name,
+ 'count': self._count,
+ 'total_time': self._total_time,
+ 'min_time': self._min_time,
+ 'max_time': self._max_time,
+ 'avg_time': self._total_time / self._count if self._count > 0 else 0
+ }
+
+ def clear_statistics(self):
+ self._count = 0
+ self._total_time = 0 # Total milliseconds
+ self._min_time = 99999999
+ self._max_time = 0
+
+ def increment(self, time):
+ self._count += 1
+ self._total_time += time # Total milliseconds
+ if self._min_time > time:
+ self._min_time = time
+ if self._max_time < time:
+ self._max_time = time
+
+
+class MibDbExternal(MibDbApi):
+ """
+ A persistent external OpenOMCI MIB Database
+ """
+ CURRENT_VERSION = 1 # VOLTHA v1.3.0 release
+
+ _TIME_FORMAT = '%Y%m%d-%H%M%S.%f'
+
+ # Paths from root proxy
+ MIB_PATH = '/omci_mibs'
+ DEVICE_PATH = MIB_PATH + '/{}' # .format(device_id)
+
+ # Classes, Instances, and Attributes as lists from root proxy
+ CLASSES_PATH = DEVICE_PATH + '/classes' # .format(device_id)
+ INSTANCES_PATH = DEVICE_PATH + '/classes/{}/instances' # .format(device_id, class_id)
+ ATTRIBUTES_PATH = DEVICE_PATH + '/classes/{}/instances/{}/attributes' # .format(device_id, class_id, instance_id)
+
+ # Single Class, Instance, and Attribute as objects from device proxy
+ CLASS_PATH = '/classes/{}' # .format(class_id)
+ INSTANCE_PATH = '/classes/{}/instances/{}' # .format(class_id, instance_id)
+ ATTRIBUTE_PATH = '/classes/{}/instances/{}/attributes/{}' # .format(class_id, instance_id
+ # attribute_name)
+
+ def __init__(self, omci_agent):
+ """
+ Class initializer
+ :param omci_agent: (OpenOMCIAgent) OpenOMCI Agent
+ """
+ super(MibDbExternal, self).__init__(omci_agent)
+ self._core = omci_agent.core
+ # Some statistics to help with debug/tuning/...
+ self._statistics = {
+ 'get': MibDbStatistic('get'),
+ 'set': MibDbStatistic('set'),
+ 'create': MibDbStatistic('create'),
+ 'delete': MibDbStatistic('delete')
+ }
+
+ def start(self):
+ """
+ Start up/restore the database
+ """
+ self.log.debug('start')
+
+ if not self._started:
+ super(MibDbExternal, self).start()
+ root_proxy = self._core.get_proxy('/')
+
+ try:
+ base = root_proxy.get(MibDbExternal.MIB_PATH)
+ self.log.info('db-exists', num_devices=len(base))
+
+ except Exception as e:
+ self.log.exception('start-failure', e=e)
+ raise
+
+ def stop(self):
+ """
+ Start up the database
+ """
+ self.log.debug('stop')
+
+ if self._started:
+ super(MibDbExternal, self).stop()
+ # TODO: Delete this method if nothing else is done except calling the base class
+
+ def _time_to_string(self, time):
+ return time.strftime(MibDbExternal._TIME_FORMAT) if time is not None else ''
+
+ def _string_to_time(self, time):
+ return datetime.strptime(time, MibDbExternal._TIME_FORMAT) if len(time) else None
+
+ def _attribute_to_string(self, device_id, class_id, attr_name, value, old_value = None):
+ """
+ Convert an ME's attribute value to string representation
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) Class ID
+ :param attr_name: (str) Attribute Name (see EntityClasses)
+ :param value: (various) Attribute Value
+
+ :return: (str) String representation of the value
+ :raises KeyError: Device, Class ID, or Attribute does not exist
+ """
+ try:
+ me_map = self._omci_agent.get_device(device_id).me_map
+
+ if class_id in me_map:
+ entity = me_map[class_id]
+ attr_index = entity.attribute_name_to_index_map[attr_name]
+ eca = entity.attributes[attr_index]
+ field = eca.field
+ else:
+ # Here for auto-defined MEs (ones not defined in ME Map)
+ from voltha.extensions.omci.omci_cc import UNKNOWN_CLASS_ATTRIBUTE_KEY
+ field = StrFixedLenField(UNKNOWN_CLASS_ATTRIBUTE_KEY, None, 24)
+
+ if isinstance(field, StrFixedLenField):
+ from scapy.base_classes import Packet_metaclass
+ if hasattr(value, 'to_json') and not isinstance(value, basestring):
+ # Packet Class to string
+ str_value = value.to_json()
+ elif isinstance(field.default, Packet_metaclass) \
+ and hasattr(field.default, 'json_from_value'):
+ #and not isinstance(value, basestring):
+ # Value/hex of Packet Class to string
+ str_value = field.default.json_from_value(value)
+ else:
+ str_value = str(value)
+
+ elif isinstance(field, (StrField, MACField, IPField)):
+ # For StrField, value is an str already
+ # For MACField, value is a string in ':' delimited form
+ # For IPField, value is a string in '.' delimited form
+ str_value = str(value)
+
+ elif isinstance(field, (ByteField, ShortField, IntField, LongField)):
+ # For ByteField, ShortField, IntField, and LongField value is an int
+ str_value = str(value)
+
+ elif isinstance(field, BitField):
+ # For BitField, value is a long
+ #
+ str_value = str(value)
+
+ elif hasattr(field, 'to_json'):
+ str_value = field.to_json(value, old_value)
+
+ elif isinstance(field, FieldListField):
+ str_value = json.dumps(value, separators=(',', ':'))
+
+ else:
+ self.log.warning('default-conversion', type=type(field),
+ class_id=class_id, attribute=attr_name, value=str(value))
+ str_value = str(value)
+
+ return str_value
+
+ except Exception as e:
+ self.log.exception('attr-to-string', device_id=device_id,
+ class_id=class_id, attr=attr_name,
+ value=value, e=e)
+ raise
+
+ def _string_to_attribute(self, device_id, class_id, attr_name, str_value):
+ """
+ Convert an ME's attribute value-string to its Scapy decode equivalent
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) Class ID
+ :param attr_name: (str) Attribute Name (see EntityClasses)
+ :param str_value: (str) Attribute Value in string form
+
+ :return: (various) String representation of the value
+ :raises KeyError: Device, Class ID, or Attribute does not exist
+ """
+ try:
+ me_map = self._omci_agent.get_device(device_id).me_map
+
+ if class_id in me_map:
+ entity = me_map[class_id]
+ attr_index = entity.attribute_name_to_index_map[attr_name]
+ eca = entity.attributes[attr_index]
+ field = eca.field
+ else:
+ # Here for auto-defined MEs (ones not defined in ME Map)
+ from voltha.extensions.omci.omci_cc import UNKNOWN_CLASS_ATTRIBUTE_KEY
+ field = StrFixedLenField(UNKNOWN_CLASS_ATTRIBUTE_KEY, None, 24)
+
+ if isinstance(field, StrFixedLenField):
+ from scapy.base_classes import Packet_metaclass
+ default = field.default
+ if isinstance(default, Packet_metaclass) and \
+ hasattr(default, 'to_json'):
+ value = json.loads(str_value)
+ else:
+ value = str_value
+
+ elif isinstance(field, MACField):
+ value = str_value
+
+ elif isinstance(field, IPField):
+ value = str_value
+
+ elif isinstance(field, (ByteField, ShortField, IntField, LongField)):
+ if str_value.lower() in ('true', 'false'):
+ str_value = '1' if str_value.lower() == 'true' else '0'
+ value = int(str_value)
+
+ elif isinstance(field, BitField):
+ value = long(str_value)
+
+ elif hasattr(field, 'load_json'):
+ value = field.load_json(str_value)
+
+ elif isinstance(field, FieldListField):
+ value = json.loads(str_value)
+
+ else:
+ self.log.warning('default-conversion', type=type(field),
+ class_id=class_id, attribute=attr_name, value=str_value)
+ value = None
+
+ return value
+
+ except Exception as e:
+ self.log.exception('attr-to-string', device_id=device_id,
+ class_id=class_id, attr=attr_name,
+ value=str_value, e=e)
+ raise
+
+ def add(self, device_id, overwrite=False):
+ """
+ Add a new ONU to database
+
+ :param device_id: (str) Device ID of ONU to add
+ :param overwrite: (bool) Overwrite existing entry if found.
+
+ :raises KeyError: If device already exists and 'overwrite' is False
+ """
+ self.log.debug('add-device', device_id=device_id, overwrite=overwrite)
+
+ now = datetime.utcnow()
+ found = False
+ root_proxy = self._core.get_proxy('/')
+
+ data = MibDeviceData(device_id=device_id,
+ created=self._time_to_string(now),
+ last_sync_time='',
+ mib_data_sync=0,
+ version=MibDbExternal.CURRENT_VERSION)
+ try:
+ dev_proxy = self._device_proxy(device_id)
+ found = True
+
+ if not overwrite:
+ # Device already exists
+ raise KeyError('Device with ID {} already exists in MIB database'.
+ format(device_id))
+
+ # Overwrite with new data
+ data = dev_proxy.get('/', depth=0)
+ self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id), data)
+ self._modified = now
+
+ except KeyError:
+ if found:
+ raise
+ # Did not exist, add it now
+ root_proxy.add(MibDbExternal.MIB_PATH, data)
+ self._created = now
+ self._modified = now
+
+ def remove(self, device_id):
+ """
+ Remove an ONU from the database
+
+ :param device_id: (str) Device ID of ONU to remove from database
+ """
+ self.log.debug('remove-device', device_id=device_id)
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ try:
+ # self._root_proxy.get(MibDbExternal.DEVICE_PATH.format(device_id))
+ self._root_proxy.remove(MibDbExternal.DEVICE_PATH.format(device_id))
+ self._modified = datetime.utcnow()
+
+ except KeyError:
+ # Did not exists, which is not a failure
+ pass
+
+ except Exception as e:
+ self.log.exception('remove-exception', device_id=device_id, e=e)
+ raise
+
+ @property
+ def _root_proxy(self):
+ return self._core.get_proxy('/')
+
+ def _device_proxy(self, device_id):
+ """
+ Return a config proxy to the OMCI MIB_DB leaf for a given device
+
+ :param device_id: (str) ONU Device ID
+ :return: (ConfigProxy) Configuration proxy rooted at OMCI MIB DB
+ :raises KeyError: If the device does not exist in the database
+ """
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ return self._core.get_proxy(MibDbExternal.DEVICE_PATH.format(device_id))
+
+ def _class_proxy(self, device_id, class_id, create=False):
+ """
+ Get a config proxy to a specific managed entity class
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) Class ID
+ :param create: (bool) If true, create default instance (and class)
+ :return: (ConfigProxy) Class configuration proxy
+
+ :raises DatabaseStateError: If database is not started
+ :raises KeyError: If Instance does not exist and 'create' is False
+ """
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not 0 <= class_id <= 0xFFFF:
+ raise ValueError('class-id is 0..0xFFFF')
+
+ fmt = MibDbExternal.DEVICE_PATH + MibDbExternal.CLASS_PATH
+ path = fmt.format(device_id, class_id)
+
+ try:
+ return self._core.get_proxy(path)
+
+ except KeyError:
+ if not create:
+ # This can occur right after a MIB Reset if the ONU publishes AVCs right away
+ # and during the MIB audit resync for ONU created MEs in response to an OLT
+ # created ME. Fail since for these test cases they occur during a verification
+ # 'query' and not the ME creation during resync. Calling code should handle
+ # they exception if it is expected to occur on occasion.
+ self.log.debug('class-proxy-does-not-exist', device_id=device_id,
+ class_id=class_id)
+ raise
+
+ # Create class
+ data = MibClassData(class_id=class_id)
+ root_path = MibDbExternal.CLASSES_PATH.format(device_id)
+ self._root_proxy.add(root_path, data)
+
+ return self._core.get_proxy(path)
+
+ def _instance_proxy(self, device_id, class_id, instance_id, create=False):
+ """
+ Get a config proxy to a specific managed entity instance
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) Class ID
+ :param instance_id: (int) Instance ID
+ :param create: (bool) If true, create default instance (and class)
+ :return: (ConfigProxy) Instance configuration proxy
+
+ :raises DatabaseStateError: If database is not started
+ :raises KeyError: If Instance does not exist and 'create' is False
+ """
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID is a string')
+
+ if not 0 <= class_id <= 0xFFFF:
+ raise ValueError('class-id is 0..0xFFFF')
+
+ if not 0 <= instance_id <= 0xFFFF:
+ raise ValueError('instance-id is 0..0xFFFF')
+
+ fmt = MibDbExternal.DEVICE_PATH + MibDbExternal.INSTANCE_PATH
+ path = fmt.format(device_id, class_id, instance_id)
+
+ try:
+ return self._core.get_proxy(path)
+
+ except KeyError:
+ if not create:
+ # This can occur right after a MIB Reset if the ONU publishes AVCs right away
+ # and during the MIB audit resync for ONU created MEs in response to an OLT
+ # created ME. Fail since for these test cases they occur during a verification
+ # 'query' and not the ME creation during resync. Calling code should handle
+ # they exception if it is expected to occur on occasion.
+ self.log.info('instance-proxy-does-not-exist', device_id=device_id,
+ class_id=class_id, instance_id=instance_id)
+ raise
+
+ # Create instance, first make sure class exists
+ self._class_proxy(device_id, class_id, create=True)
+
+ now = self._time_to_string(datetime.utcnow())
+ data = MibInstanceData(instance_id=instance_id, created=now, modified=now)
+ root_path = MibDbExternal.INSTANCES_PATH.format(device_id, class_id)
+ self._root_proxy.add(root_path, data)
+
+ return self._core.get_proxy(path)
+
+ def on_mib_reset(self, device_id):
+ """
+ Reset/clear the database for a specific Device
+
+ :param device_id: (str) ONU Device ID
+ :raises DatabaseStateError: If the database is not enabled
+ :raises KeyError: If the device does not exist in the database
+ """
+ self.log.debug('on-mib-reset', device_id=device_id)
+
+ try:
+ device_proxy = self._device_proxy(device_id)
+ data = device_proxy.get(depth=2)
+
+ # Wipe out any existing class IDs
+ class_ids = [c.class_id for c in data.classes]
+
+ if len(class_ids):
+ for class_id in class_ids:
+ device_proxy.remove(MibDbExternal.CLASS_PATH.format(class_id))
+
+ # Reset MIB Data Sync to zero
+ now = datetime.utcnow()
+ data = MibDeviceData(device_id=device_id,
+ created=data.created,
+ last_sync_time=data.last_sync_time,
+ mib_data_sync=0,
+ version=MibDbExternal.CURRENT_VERSION)
+ # Update
+ self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id),
+ data)
+ self._modified = now
+ self.log.debug('mib-reset-complete', device_id=device_id)
+
+ except Exception as e:
+ self.log.exception('mib-reset-exception', device_id=device_id, e=e)
+ raise
+
+ def save_mib_data_sync(self, device_id, value):
+ """
+ Save the MIB Data Sync to the database in an easy location to access
+
+ :param device_id: (str) ONU Device ID
+ :param value: (int) Value to save
+ """
+ self.log.debug('save-mds', device_id=device_id, value=value)
+
+ try:
+ if not isinstance(value, int):
+ raise TypeError('MIB Data Sync is an integer')
+
+ if not 0 <= value <= 255:
+ raise ValueError('Invalid MIB-data-sync value {}. Must be 0..255'.
+ format(value))
+ device_proxy = self._device_proxy(device_id)
+ data = device_proxy.get(depth=0)
+
+ now = datetime.utcnow()
+ data.mib_data_sync = value
+
+ # Update
+ self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id),
+ data)
+ self._modified = now
+ self.log.debug('save-mds-complete', device_id=device_id)
+
+ except Exception as e:
+ self.log.exception('save-mds-exception', device_id=device_id, e=e)
+ raise
+
+ def get_mib_data_sync(self, device_id):
+ """
+ Get the MIB Data Sync value last saved to the database for a device
+
+ :param device_id: (str) ONU Device ID
+ :return: (int) The Value or None if not found
+ """
+ self.log.debug('get-mds', device_id=device_id)
+
+ try:
+ device_proxy = self._device_proxy(device_id)
+ data = device_proxy.get(depth=0)
+ return int(data.mib_data_sync)
+
+ except KeyError:
+ return None # OMCI MIB_DB entry has not yet been created
+
+ except Exception as e:
+ self.log.exception('get-mds-exception', device_id=device_id, e=e)
+ raise
+
+ def save_last_sync(self, device_id, value):
+ """
+ Save the Last Sync time to the database in an easy location to access
+
+ :param device_id: (str) ONU Device ID
+ :param value: (DateTime) Value to save
+ """
+ self.log.debug('save-last-sync', device_id=device_id, time=str(value))
+
+ try:
+ if not isinstance(value, datetime):
+ raise TypeError('Expected a datetime object, got {}'.
+ format(type(datetime)))
+
+ device_proxy = self._device_proxy(device_id)
+ data = device_proxy.get(depth=0)
+
+ now = datetime.utcnow()
+ data.last_sync_time = self._time_to_string(value)
+
+ # Update
+ self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id),
+ data)
+ self._modified = now
+ self.log.debug('save-mds-complete', device_id=device_id)
+
+ except Exception as e:
+ self.log.exception('save-last-sync-exception', device_id=device_id, e=e)
+ raise
+
+ def get_last_sync(self, device_id):
+ """
+ Get the Last Sync Time saved to the database for a device
+
+ :param device_id: (str) ONU Device ID
+ :return: (int) The Value or None if not found
+ """
+ self.log.debug('get-last-sync', device_id=device_id)
+
+ try:
+ device_proxy = self._device_proxy(device_id)
+ data = device_proxy.get(depth=0)
+ return self._string_to_time(data.last_sync_time)
+
+ except KeyError:
+ return None # OMCI MIB_DB entry has not yet been created
+
+ except Exception as e:
+ self.log.exception('get-last-sync-exception', e=e)
+ raise
+
+ def _add_new_class(self, device_id, class_id, instance_id, attributes):
+ """
+ Create an entry for a new class in the external database
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param instance_id: (int) ME Entity ID
+ :param attributes: (dict) Attribute dictionary
+
+ :returns: (bool) True if the value was saved to the database. False if the
+ value was identical to the current instance
+ """
+ self.log.debug('add', device_id=device_id, class_id=class_id,
+ instance_id=instance_id, attributes=attributes)
+
+ now = self._time_to_string(datetime.utcnow())
+ attrs = []
+ for k, v in attributes.items():
+ if k == 'serial_number':
+ vendor_id = str(v[0:4])
+ vendor_specific = v[4:]
+ vendor_specific = str(vendor_specific.encode('hex'))
+ str_value = vendor_id + vendor_specific
+ attrs.append(MibAttributeData(name=k, value=str_value))
+ else:
+ str_value = self._attribute_to_string(device_id, class_id, k, v)
+ attrs.append(MibAttributeData(name=k, value=str_value))
+
+ class_data = MibClassData(class_id=class_id,
+ instances=[MibInstanceData(instance_id=instance_id,
+ created=now,
+ modified=now,
+ attributes=attrs)])
+
+ self._root_proxy.add(MibDbExternal.CLASSES_PATH.format(device_id), class_data)
+ self.log.debug('set-complete', device_id=device_id, class_id=class_id,
+ entity_id=instance_id, attributes=attributes)
+ return True
+
+ def _add_new_instance(self, device_id, class_id, instance_id, attributes):
+ """
+ Create an entry for a instance of an existing class in the external database
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param instance_id: (int) ME Entity ID
+ :param attributes: (dict) Attribute dictionary
+
+ :returns: (bool) True if the value was saved to the database. False if the
+ value was identical to the current instance
+ """
+ self.log.debug('add', device_id=device_id, class_id=class_id,
+ instance_id=instance_id, attributes=attributes)
+
+ now = self._time_to_string(datetime.utcnow())
+ attrs = []
+ for k, v in attributes.items():
+ if k == 'serial_number':
+ vendor_id = str(v[0:4])
+ vendor_specific = v[4:]
+ vendor_specific = str(vendor_specific.encode('hex'))
+ str_value = vendor_id+vendor_specific
+ attrs.append(MibAttributeData(name=k, value=str_value))
+ else:
+ str_value = self._attribute_to_string(device_id, class_id, k, v)
+ attrs.append(MibAttributeData(name=k, value=str_value))
+
+ instance_data = MibInstanceData(instance_id=instance_id,
+ created=now,
+ modified=now,
+ attributes=attrs)
+
+ self._root_proxy.add(MibDbExternal.INSTANCES_PATH.format(device_id, class_id),
+ instance_data)
+
+ self.log.debug('set-complete', device_id=device_id, class_id=class_id,
+ entity_id=instance_id, attributes=attributes)
+ return True
+
+ def set(self, device_id, class_id, instance_id, attributes):
+ """
+ Set a database value. This should only be called by the MIB synchronizer
+ and its related tasks
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param instance_id: (int) ME Entity ID
+ :param attributes: (dict) Attribute dictionary
+
+ :returns: (bool) True if the value was saved to the database. False if the
+ value was identical to the current instance
+
+ :raises KeyError: If device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ self.log.debug('set', device_id=device_id, class_id=class_id,
+ instance_id=instance_id, attributes=attributes)
+ try:
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be a string')
+
+ if not 0 <= class_id <= 0xFFFF:
+ raise ValueError("Invalid Class ID: {}, should be 0..65535".format(class_id))
+
+ if not 0 <= instance_id <= 0xFFFF:
+ raise ValueError("Invalid Instance ID: {}, should be 0..65535".format(instance_id))
+
+ if not isinstance(attributes, dict):
+ raise TypeError("Attributes should be a dictionary")
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ # Determine the best strategy to add the information
+ dev_proxy = self._device_proxy(device_id)
+
+ operation = 'set'
+ start_time = None
+ try:
+ class_data = dev_proxy.get(MibDbExternal.CLASS_PATH.format(class_id), deep=True)
+
+ inst_data = next((inst for inst in class_data.instances
+ if inst.instance_id == instance_id), None)
+
+ if inst_data is None:
+ operation = 'create'
+ start_time = datetime.utcnow()
+ return self._add_new_instance(device_id, class_id, instance_id, attributes)
+
+ # Possibly adding to or updating an existing instance
+ # Get instance proxy, creating it if needed
+
+ modified = False
+ new_attributes = []
+ exist_attr_indexes = dict()
+ attr_len = len(inst_data.attributes)
+
+ for index in xrange(0, attr_len):
+ name = inst_data.attributes[index].name
+ value = inst_data.attributes[index].value
+ exist_attr_indexes[name] = index
+ new_attributes.append(MibAttributeData(name=name, value=value))
+
+ for k, v in attributes.items():
+ try:
+ old_value = None if k not in exist_attr_indexes \
+ else new_attributes[exist_attr_indexes[k]].value
+
+ str_value = self._attribute_to_string(device_id, class_id, k, v, old_value)
+
+ if k not in exist_attr_indexes:
+ new_attributes.append(MibAttributeData(name=k, value=str_value))
+ modified = True
+
+ elif new_attributes[exist_attr_indexes[k]].value != str_value:
+ new_attributes[exist_attr_indexes[k]].value = str_value
+ modified = True
+
+ except Exception as e:
+ self.log.exception('save-error', e=e, class_id=class_id,
+ attr=k, value_type=type(v))
+
+ if modified:
+ now = datetime.utcnow()
+ start_time = now
+ new_data = MibInstanceData(instance_id=instance_id,
+ created=inst_data.created,
+ modified=self._time_to_string(now),
+ attributes=new_attributes)
+ dev_proxy.remove(MibDbExternal.INSTANCE_PATH.format(class_id, instance_id))
+ self._root_proxy.add(MibDbExternal.INSTANCES_PATH.format(device_id,
+ class_id), new_data)
+ return modified
+
+ except KeyError:
+ # Here if the class-id does not yet exist in the database
+ self.log.debug("adding-key-not-found", class_id=class_id)
+ return self._add_new_class(device_id, class_id, instance_id,
+ attributes)
+ finally:
+ if start_time is not None:
+ diff = datetime.utcnow() - start_time
+ # NOTE: Change to 'debug' when checked in, manually change to 'info'
+ # for development testing.
+ self.log.debug('db-{}-time'.format(operation), milliseconds=diff.microseconds/1000)
+ self._statistics[operation].increment(diff.microseconds/1000)
+
+ except Exception as e:
+ self.log.exception('set-exception', device_id=device_id, class_id=class_id,
+ instance_id=instance_id, attributes=attributes, e=e)
+ raise
+
+ def delete(self, device_id, class_id, entity_id):
+ """
+ Delete an entity from the database if it exists. If all instances
+ of a class are deleted, the class is deleted as well.
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param entity_id: (int) ME Entity ID
+
+ :returns: (bool) True if the instance was found and deleted. False
+ if it did not exist.
+
+ :raises KeyError: If device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ self.log.debug('delete', device_id=device_id, class_id=class_id,
+ entity_id=entity_id)
+
+ if not self._started:
+ raise DatabaseStateError('The Database is not currently active')
+
+ if not isinstance(device_id, basestring):
+ raise TypeError('Device ID should be an string')
+
+ if not 0 <= class_id <= 0xFFFF:
+ raise ValueError('class-id is 0..0xFFFF')
+
+ if not 0 <= entity_id <= 0xFFFF:
+ raise ValueError('instance-id is 0..0xFFFF')
+
+ start_time = datetime.utcnow()
+ try:
+ # Remove instance
+ self._instance_proxy(device_id, class_id, entity_id).remove('/')
+ now = datetime.utcnow()
+
+ # If resulting class has no instance, remove it as well
+ class_proxy = self._class_proxy(device_id, class_id)
+ class_data = class_proxy.get('/', depth=1)
+
+ if len(class_data.instances) == 0:
+ class_proxy.remove('/')
+
+ self._modified = now
+ return True
+
+ except KeyError:
+ return False # Not found
+
+ except Exception as e:
+ self.log.exception('get-last-sync-exception', device_id=device_id, e=e)
+ raise
+
+ finally:
+ diff = datetime.utcnow() - start_time
+ # NOTE: Change to 'debug' when checked in, manually change to 'info'
+ # for development testing.
+ self.log.debug('db-delete-time', milliseconds=diff.microseconds/1000)
+ self._statistics['delete'].increment(diff.microseconds/1000)
+
+ def query(self, device_id, class_id=None, instance_id=None, attributes=None):
+ """
+ Get database information.
+
+ This method can be used to request information from the database to the detailed
+ level requested
+
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) Managed Entity class ID
+ :param instance_id: (int) Managed Entity instance
+ :param attributes: (list/set or str) Managed Entity instance's attributes
+
+ :return: (dict) The value(s) requested. If class/inst/attribute is
+ not found, an empty dictionary is returned
+ :raises KeyError: If the requested device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ self.log.debug('query', device_id=device_id, class_id=class_id,
+ instance_id=instance_id, attributes=attributes)
+
+ start_time = datetime.utcnow()
+ end_time = None
+ try:
+ if class_id is None:
+ # Get full device info
+ dev_data = self._device_proxy(device_id).get('/', depth=-1)
+ end_time = datetime.utcnow()
+ data = self._device_to_dict(dev_data)
+
+ elif instance_id is None:
+ # Get all instances of the class
+ try:
+ cls_data = self._class_proxy(device_id, class_id).get('/', depth=-1)
+ end_time = datetime.utcnow()
+ data = self._class_to_dict(device_id, cls_data)
+
+ except KeyError:
+ data = dict()
+
+ else:
+ # Get all attributes of a specific ME
+ try:
+ inst_data = self._instance_proxy(device_id, class_id, instance_id).\
+ get('/', depth=-1)
+ end_time = datetime.utcnow()
+
+ if attributes is None:
+ # All Attributes
+ data = self._instance_to_dict(device_id, class_id, inst_data)
+
+ else:
+ # Specific attribute(s)
+ if isinstance(attributes, basestring):
+ attributes = {attributes}
+
+ data = {
+ attr.name: self._string_to_attribute(device_id,
+ class_id,
+ attr.name,
+ attr.value)
+ for attr in inst_data.attributes if attr.name in attributes}
+
+ except KeyError:
+ data = dict()
+
+ return data
+
+ except KeyError:
+ self.log.warn('query-no-device', device_id=device_id)
+ raise
+
+ except Exception as e:
+ self.log.exception('get-last-sync-exception', device_id=device_id, e=e)
+ raise
+
+ finally:
+ if end_time is not None:
+ diff = end_time.utcnow() - start_time
+ # NOTE: Change to 'debug' when checked in, manually change to 'info'
+ # for development testing.
+ self.log.debug('db-get-time', milliseconds=diff.microseconds/1000, class_id=class_id,
+ instance_id=instance_id)
+ self._statistics['get'].increment(diff.microseconds/1000)
+
+ def _instance_to_dict(self, device_id, class_id, instance):
+ if not isinstance(instance, MibInstanceData):
+ raise TypeError('{} is not of type MibInstanceData'.format(type(instance)))
+
+ data = {
+ INSTANCE_ID_KEY: instance.instance_id,
+ CREATED_KEY: self._string_to_time(instance.created),
+ MODIFIED_KEY: self._string_to_time(instance.modified),
+ ATTRIBUTES_KEY: dict()
+ }
+ for attribute in instance.attributes:
+ data[ATTRIBUTES_KEY][attribute.name] = self._string_to_attribute(device_id,
+ class_id,
+ attribute.name,
+ attribute.value)
+ return data
+
+ def _class_to_dict(self, device_id, val):
+ if not isinstance(val, MibClassData):
+ raise TypeError('{} is not of type MibClassData'.format(type(val)))
+
+ data = {
+ CLASS_ID_KEY: val.class_id,
+ }
+ for instance in val.instances:
+ data[instance.instance_id] = self._instance_to_dict(device_id,
+ val.class_id,
+ instance)
+ return data
+
+ def _device_to_dict(self, val):
+ if not isinstance(val, MibDeviceData):
+ raise TypeError('{} is not of type MibDeviceData'.format(type(val)))
+
+ data = {
+ DEVICE_ID_KEY: val.device_id,
+ CREATED_KEY: self._string_to_time(val.created),
+ LAST_SYNC_KEY: self._string_to_time(val.last_sync_time),
+ MDS_KEY: val.mib_data_sync,
+ VERSION_KEY: val.version,
+ ME_KEY: dict(),
+ MSG_TYPE_KEY: set()
+ }
+ for class_data in val.classes:
+ data[class_data.class_id] = self._class_to_dict(val.device_id,
+ class_data)
+ for managed_entity in val.managed_entities:
+ data[ME_KEY][managed_entity.class_id] = managed_entity.name
+
+ for msg_type in val.message_types:
+ data[MSG_TYPE_KEY].add(msg_type.message_type)
+
+ return data
+
+ def _managed_entity_to_name(self, device_id, class_id):
+ me_map = self._omci_agent.get_device(device_id).me_map
+ entity = me_map.get(class_id)
+
+ return entity.__name__ if entity is not None else 'UnknownManagedEntity'
+
+ def update_supported_managed_entities(self, device_id, managed_entities):
+ """
+ Update the supported OMCI Managed Entities for this device
+ :param device_id: (str) ONU Device ID
+ :param managed_entities: (set) Managed Entity class IDs
+ """
+ try:
+ me_list = [ManagedEntity(class_id=class_id,
+ name=self._managed_entity_to_name(device_id,
+ class_id))
+ for class_id in managed_entities]
+
+ device_proxy = self._device_proxy(device_id)
+ data = device_proxy.get(depth=0)
+
+ now = datetime.utcnow()
+ data.managed_entities.extend(me_list)
+
+ # Update
+ self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id),
+ data)
+ self._modified = now
+ self.log.debug('save-me-list-complete', device_id=device_id)
+
+ except Exception as e:
+ self.log.exception('add-me-failure', e=e, me_list=managed_entities)
+ raise
+
+ def update_supported_message_types(self, device_id, msg_types):
+ """
+ Update the supported OMCI Managed Entities for this device
+ :param device_id: (str) ONU Device ID
+ :param msg_types: (set) Message Type values (ints)
+ """
+ try:
+ msg_type_list = [MessageType(message_type=msg_type.value)
+ for msg_type in msg_types]
+
+ device_proxy = self._device_proxy(device_id)
+ data = device_proxy.get(depth=0)
+
+ now = datetime.utcnow()
+ data.message_types.extend(msg_type_list)
+
+ # Update
+ self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id),
+ data)
+ self._modified = now
+ self.log.debug('save-msg-types-complete', device_id=device_id)
+
+ except Exception as e:
+ self.log.exception('add-msg-types-failure', e=e, msg_types=msg_types)
+ raise
diff --git a/python/adapters/extensions/omci/me_frame.py b/python/adapters/extensions/omci/me_frame.py
new file mode 100644
index 0000000..cf1cf3e
--- /dev/null
+++ b/python/adapters/extensions/omci/me_frame.py
@@ -0,0 +1,475 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""
+OMCI Managed Entity Message support base class
+"""
+from voltha.extensions.omci.omci import *
+
+# abbreviations
+OP = EntityOperations
+AA = AttributeAccess
+
+
+class MEFrame(object):
+ """Base class to help simplify Frame Creation"""
+ def __init__(self, entity_class, entity_id, data):
+ assert issubclass(entity_class, EntityClass), \
+ "'{}' must be a subclass of MEFrame".format(entity_class)
+ self.check_type(entity_id, int)
+
+ if not 0 <= entity_id <= 0xFFFF:
+ raise ValueError('entity_id should be 0..65535')
+
+ self.log = structlog.get_logger()
+ self._class = entity_class
+ self._entity_id = entity_id
+ self.data = data
+
+ def __str__(self):
+ return '{}: Entity_ID: {}, Data: {}'.\
+ format(self.entity_class_name, self._entity_id, self.data)
+
+ @property
+ def entity_class(self):
+ """
+ The Entity Class for this ME
+ :return: (EntityClass) Entity class
+ """
+ return self._class
+
+ @property
+ def entity_class_name(self):
+ return self._class.__name__
+
+ @property
+ def entity_id(self):
+ """
+ The Entity ID for this ME frame
+ :return: (int) Entity ID (0..0xFFFF)
+ """
+ return self._entity_id
+
+ @staticmethod
+ def check_type(param, types):
+ if not isinstance(param, types):
+ raise TypeError("Parameter '{}' should be a {}".format(param, types))
+
+ def _check_operation(self, operation):
+ allowed = self.entity_class.mandatory_operations | self.entity_class.optional_operations
+ assert operation in allowed, "{} not allowed for '{}'".format(operation.name,
+ self.entity_class_name)
+
+ def _check_attributes(self, attributes, access):
+ keys = attributes.keys() if isinstance(attributes, dict) else attributes
+ for attr_name in keys:
+ # Bad attribute name (invalid or spelling error)?
+ index = self.entity_class.attribute_name_to_index_map.get(attr_name)
+ if index is None:
+ raise KeyError("Attribute '{}' is not valid for '{}'".
+ format(attr_name, self.entity_class_name))
+ # Invalid access?
+ assert access in self.entity_class.attributes[index].access, \
+ "Access '{}' for attribute '{}' is not valid for '{}'".format(access.name,
+ attr_name,
+ self.entity_class_name)
+
+ if access.value in [AA.W.value, AA.SBC.value] and isinstance(attributes, dict):
+ for attr_name, value in attributes.iteritems():
+ index = self.entity_class.attribute_name_to_index_map.get(attr_name)
+ attribute = self.entity_class.attributes[index]
+ if not attribute.valid(value):
+ raise ValueError("Invalid value '{}' for attribute '{}' of '{}".
+ format(value, attr_name, self.entity_class_name))
+
+ @staticmethod
+ def _attr_to_data(attributes):
+ """
+ Convert an object into the 'data' set or dictionary for get/set/create/delete
+ requests.
+
+ This method takes a 'string', 'list', or 'set' for get requests and
+ converts it to a 'set' of attributes.
+
+ For create/set requests a dictionary of attribute/value pairs is required
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, set, or dict can be provided. For create/set
+ operations, a dictionary should be provided. For delete
+ the attributes may be None since they are ignored.
+
+ :return: (set, dict) set for get/deletes, dict for create/set
+ """
+ if isinstance(attributes, basestring):
+ # data = [str(attributes)]
+ data = set()
+ data.add(str(attributes))
+
+ elif isinstance(attributes, list):
+ assert all(isinstance(attr, basestring) for attr in attributes),\
+ 'attribute list must be strings'
+ data = {str(attr) for attr in attributes}
+ assert len(data) == len(attributes), 'Attributes were not unique'
+
+ elif isinstance(attributes, set):
+ assert all(isinstance(attr, basestring) for attr in attributes),\
+ 'attribute set must be strings'
+ data = {str(attr) for attr in attributes}
+
+ elif isinstance(attributes, (dict, type(None))):
+ data = attributes
+
+ else:
+ raise TypeError("Unsupported attributes type '{}'".format(type(attributes)))
+
+ return data
+
+ def create(self):
+ """
+ Create a Create request frame for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ assert hasattr(self.entity_class, 'class_id'), 'class_id required for Create actions'
+ assert hasattr(self, 'entity_id'), 'entity_id required for Create actions'
+ assert hasattr(self, 'data'), 'data required for Create actions'
+
+ data = getattr(self, 'data')
+ MEFrame.check_type(data, dict)
+ assert len(data) > 0, 'No attributes supplied'
+
+ self._check_operation(OP.Create)
+ self._check_attributes(data, AA.Writable)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciCreate.message_id,
+ omci_message=OmciCreate(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ data=data
+ ))
+
+ def delete(self):
+ """
+ Create a Delete request frame for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ self._check_operation(OP.Delete)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciDelete.message_id,
+ omci_message=OmciDelete(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id')
+ ))
+
+ def set(self):
+ """
+ Create a Set request frame for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ assert hasattr(self, 'data'), 'data required for Set actions'
+ data = getattr(self, 'data')
+ MEFrame.check_type(data, dict)
+ assert len(data) > 0, 'No attributes supplied'
+
+ self._check_operation(OP.Set)
+ self._check_attributes(data, AA.Writable)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciSet.message_id,
+ omci_message=OmciSet(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ attributes_mask=self.entity_class.mask_for(*data.keys()),
+ data=data
+ ))
+
+ def get(self):
+ """
+ Create a Get request frame for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ assert hasattr(self, 'data'), 'data required for Get actions'
+ data = getattr(self, 'data')
+ MEFrame.check_type(data, (list, set, dict))
+ assert len(data) > 0, 'No attributes supplied'
+
+ mask_set = data.keys() if isinstance(data, dict) else data
+
+ self._check_operation(OP.Get)
+ self._check_attributes(mask_set, AA.Readable)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciGet.message_id,
+ omci_message=OmciGet(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ attributes_mask=self.entity_class.mask_for(*mask_set)
+ ))
+
+ def reboot(self, reboot_code=0):
+ """
+ Create a Reboot request from for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ self._check_operation(OP.Reboot)
+ assert 0 <= reboot_code <= 2, 'Reboot code must be 0..2'
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciReboot.message_id,
+ omci_message=OmciReboot(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ reboot_code=reboot_code
+ ))
+
+ def mib_reset(self):
+ """
+ Create a MIB Reset request from for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ self._check_operation(OP.MibReset)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciMibReset.message_id,
+ omci_message=OmciMibReset(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id')
+ ))
+
+ def mib_upload(self):
+ """
+ Create a MIB Upload request from for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ self._check_operation(OP.MibUpload)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciMibUpload.message_id,
+ omci_message=OmciMibUpload(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id')
+ ))
+
+ def mib_upload_next(self):
+ """
+ Create a MIB Upload Next request from for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ assert hasattr(self, 'data'), 'data required for Set actions'
+ data = getattr(self, 'data')
+ MEFrame.check_type(data, dict)
+ assert len(data) > 0, 'No attributes supplied'
+ assert 'mib_data_sync' in data, "'mib_data_sync' not in attributes list"
+
+ self._check_operation(OP.MibUploadNext)
+ self._check_attributes(data, AA.Writable)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciMibUploadNext.message_id,
+ omci_message=OmciMibUploadNext(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ command_sequence_number=data['mib_data_sync']
+ ))
+
+ def get_next(self):
+ """
+ Create a Get Next request frame for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ assert hasattr(self, 'data'), 'data required for Get Next actions'
+ data = getattr(self, 'data')
+ MEFrame.check_type(data, dict)
+ assert len(data) == 1, 'Only one attribute should be specified'
+
+ mask_set = data.keys() if isinstance(data, dict) else data
+
+ self._check_operation(OP.GetNext)
+ self._check_attributes(mask_set, AA.Readable)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciGetNext.message_id,
+ omci_message=OmciGetNext(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ attributes_mask=self.entity_class.mask_for(*mask_set),
+ command_sequence_number=data.values()[0]
+ ))
+
+ def synchronize_time(self, time=None):
+ """
+ Create a Synchronize Time request from for this ME
+ :param time: (DateTime) Time to set to. If none, use UTC
+ :return: (OmciFrame) OMCI Frame
+ """
+ from datetime import datetime
+ self._check_operation(OP.SynchronizeTime)
+ dt = time or datetime.utcnow()
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciSynchronizeTime.message_id,
+ omci_message=OmciSynchronizeTime(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ year=dt.year,
+ month=dt.month,
+ hour=dt.hour,
+ minute=dt.minute,
+ second=dt.second,
+ ))
+
+ def get_all_alarm(self, alarm_retrieval_mode):
+ """
+ Create a Alarm request from for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ self._check_operation(OP.GetAllAlarms)
+ assert 0 <= alarm_retrieval_mode <= 1, 'Alarm retrieval mode must be 0..1'
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciGetAllAlarms.message_id,
+ omci_message=OmciGetAllAlarms(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ alarm_retrieval_mode=alarm_retrieval_mode
+ ))
+
+ def get_all_alarm_next(self, command_sequence_number):
+ """
+ Create a Alarm request from for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ self._check_operation(OP.GetAllAlarmsNext)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciGetAllAlarmsNext.message_id,
+ omci_message=OmciGetAllAlarmsNext(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ command_sequence_number=command_sequence_number
+ ))
+
+ def start_software_download(self, image_size, window_size):
+ """
+ Create Start Software Download message
+ :return: (OmciFrame) OMCI Frame
+ """
+ self.log.debug("--> start_software_download")
+ self._check_operation(OP.StartSoftwareDownload)
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciStartSoftwareDownload.message_id,
+ omci_message=OmciStartSoftwareDownload(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ window_size=window_size,
+ image_size=image_size,
+ instance_id=getattr(self, 'entity_id')
+ ))
+
+ def end_software_download(self, crc32, image_size):
+ """
+ Create End Software Download message
+ :return: (OmciFrame) OMCI Frame
+ """
+ self._check_operation(OP.EndSoftwareDownload)
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciEndSoftwareDownload.message_id,
+ omci_message=OmciEndSoftwareDownload(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ crc32=crc32,
+ image_size=image_size,
+ instance_id=getattr(self, 'entity_id')
+ ))
+
+ def download_section(self, is_last_section, section_number, data):
+ """
+ Create Download Section message
+ :is_last_section: (bool) indicate the last section in the window
+ :section_num : (int) current section number
+ :data : (byte) data to be sent in the section
+ :return: (OmciFrame) OMCI Frame
+ """
+ self.log.debug("--> download_section: ", section_number=section_number)
+
+ self._check_operation(OP.DownloadSection)
+ if is_last_section:
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciDownloadSectionLast.message_id,
+ omci_message=OmciDownloadSectionLast(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ section_number=section_number,
+ data=data
+ ))
+ else:
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciDownloadSection.message_id,
+ omci_message=OmciDownloadSection(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ section_number=section_number,
+ data=data
+ ))
+
+ def activate_image(self, activate_flag=0):
+ """
+ Activate Image message
+ :activate_flag: 00 Activate image unconditionally
+ 01 Activate image only if no POTS/VoIP calls are in progress
+ 10 Activate image only if no emergency call is in progress
+ :return: (OmciFrame) OMCI Frame
+ """
+ self.log.debug("--> activate_image", entity=self.entity_id, flag=activate_flag)
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciActivateImage.message_id,
+ omci_message=OmciActivateImage(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ activate_flag=activate_flag
+ ))
+
+ def commit_image(self):
+ """
+ Commit Image message
+ :return: (OmciFrame) OMCI Frame
+ """
+ self.log.debug("--> commit_image", entity=self.entity_id)
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciCommitImage.message_id,
+ omci_message=OmciCommitImage(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ ))
+
diff --git a/python/adapters/extensions/omci/omci.py b/python/adapters/extensions/omci/omci.py
new file mode 100644
index 0000000..5a94146
--- /dev/null
+++ b/python/adapters/extensions/omci/omci.py
@@ -0,0 +1,23 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Omci message generator and parser implementation using scapy
+"""
+
+from omci_frame import OmciFrame
+from omci_messages import *
+from omci_entities import *
diff --git a/python/adapters/extensions/omci/omci_cc.py b/python/adapters/extensions/omci/omci_cc.py
new file mode 100644
index 0000000..e1c6019
--- /dev/null
+++ b/python/adapters/extensions/omci/omci_cc.py
@@ -0,0 +1,1000 @@
+#
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""
+OMCI Message support
+"""
+
+import sys
+import arrow
+from twisted.internet import reactor, defer
+from twisted.internet.defer import TimeoutError, CancelledError, failure, fail, succeed, inlineCallbacks
+from common.frameio.frameio import hexify
+from voltha.extensions.omci.omci import *
+from voltha.extensions.omci.omci_me import OntGFrame, OntDataFrame, SoftwareImageFrame
+from voltha.extensions.omci.me_frame import MEFrame
+from voltha.extensions.omci.omci_defs import EntityOperations, ReasonCodes
+from common.event_bus import EventBusClient
+from enum import IntEnum
+from binascii import hexlify
+
+
+def hexify(buffer):
+ """Return a hexadecimal string encoding of input buffer"""
+ return ''.join('%02x' % ord(c) for c in buffer)
+
+
+DEFAULT_OMCI_TIMEOUT = 10 # 3 # Seconds
+MAX_OMCI_REQUEST_AGE = 60 # Seconds
+DEFAULT_OMCI_DOWNLOAD_SECTION_SIZE = 31 # Bytes
+MAX_TABLE_ROW_COUNT = 512 # Keep get-next logic reasonable
+
+CONNECTED_KEY = 'connected'
+TX_REQUEST_KEY = 'tx-request'
+RX_RESPONSE_KEY = 'rx-response'
+UNKNOWN_CLASS_ATTRIBUTE_KEY = 'voltha-unknown-blob'
+
+
+class OmciCCRxEvents(IntEnum):
+ AVC_Notification = 0,
+ MIB_Upload = 1,
+ MIB_Upload_Next = 2,
+ Create = 3,
+ Delete = 4,
+ Set = 5,
+ Alarm_Notification = 6,
+ Test_Result = 7,
+ MIB_Reset = 8,
+ Connectivity = 9,
+ Get_ALARM_Get = 10,
+ Get_ALARM_Get_Next = 11
+
+
+# abbreviations
+OP = EntityOperations
+RxEvent = OmciCCRxEvents
+
+
+class OMCI_CC(object):
+ """ Handle OMCI Communication Channel specifics for Adtran ONUs"""
+
+ MIN_OMCI_TX_ID_LOW_PRIORITY = 0x0001 # 2 Octets max
+ MAX_OMCI_TX_ID_LOW_PRIORITY = 0x7FFF # 2 Octets max
+ MIN_OMCI_TX_ID_HIGH_PRIORITY = 0x8000 # 2 Octets max
+ MAX_OMCI_TX_ID_HIGH_PRIORITY = 0xFFFF # 2 Octets max
+ LOW_PRIORITY = 0
+ HIGH_PRIORITY = 1
+
+ # Offset into some tuples for pending lists and tx in progress
+ PENDING_DEFERRED = 0
+ PENDING_FRAME = 1
+ PENDING_TIMEOUT = 2
+ PENDING_RETRY = 3
+
+ REQUEST_TIMESTAMP = 0
+ REQUEST_DEFERRED = 1
+ REQUEST_FRAME = 2
+ REQUEST_TIMEOUT = 3
+ REQUEST_RETRY = 4
+ REQUEST_DELAYED_CALL = 5
+
+ _frame_to_event_type = {
+ OmciMibResetResponse.message_id: RxEvent.MIB_Reset,
+ OmciMibUploadResponse.message_id: RxEvent.MIB_Upload,
+ OmciMibUploadNextResponse.message_id: RxEvent.MIB_Upload_Next,
+ OmciCreateResponse.message_id: RxEvent.Create,
+ OmciDeleteResponse.message_id: RxEvent.Delete,
+ OmciSetResponse.message_id: RxEvent.Set,
+ OmciGetAllAlarmsResponse.message_id: RxEvent.Get_ALARM_Get,
+ OmciGetAllAlarmsNextResponse.message_id: RxEvent.Get_ALARM_Get_Next
+ }
+
+ def __init__(self, adapter_agent, device_id, me_map=None,
+ clock=None):
+ self.log = structlog.get_logger(device_id=device_id)
+ self._adapter_agent = adapter_agent
+ self._device_id = device_id
+ self._proxy_address = None
+ self._enabled = False
+ self._extended_messaging = False
+ self._me_map = me_map
+ if clock is None:
+ self.reactor = reactor
+ else:
+ self.reactor = clock
+
+ # Support 2 levels of priority since only baseline message set supported
+ self._tx_tid = [OMCI_CC.MIN_OMCI_TX_ID_LOW_PRIORITY, OMCI_CC.MIN_OMCI_TX_ID_HIGH_PRIORITY]
+ self._tx_request = [None, None] # Tx in progress (timestamp, defer, frame, timeout, retry, delayedCall)
+ self._pending = [list(), list()] # pending queue (deferred, tx_frame, timeout, retry)
+ self._rx_response = [None, None]
+
+ # Statistics
+ self._tx_frames = 0
+ self._rx_frames = 0
+ self._rx_unknown_tid = 0 # Rx OMCI with no Tx TID match
+ self._rx_onu_frames = 0 # Autonomously generated ONU frames
+ self._rx_onu_discards = 0 # Autonomously generated ONU unknown message types
+ self._rx_timeouts = 0
+ self._rx_late = 0 # Frame response received after timeout on Tx
+ self._rx_unknown_me = 0 # Number of managed entities Rx without a decode definition
+ self._tx_errors = 0 # Exceptions during tx request
+ self._consecutive_errors = 0 # Rx & Tx errors in a row, a good RX resets this to 0
+ self._reply_min = sys.maxint # Fastest successful tx -> rx
+ self._reply_max = 0 # Longest successful tx -> rx
+ self._reply_sum = 0.0 # Total seconds for successful tx->rx (float for average)
+ self._max_hp_tx_queue = 0 # Maximum size of high priority tx pending queue
+ self._max_lp_tx_queue = 0 # Maximum size of low priority tx pending queue
+
+ self.event_bus = EventBusClient()
+
+ # If a list of custom ME Entities classes were provided, insert them into
+ # main class_id to entity map.
+ # TODO: If this class becomes hidden from the ONU DA, move this to the OMCI State Machine runner
+
+ def __str__(self):
+ return "OMCISupport: {}".format(self._device_id)
+
+ def _get_priority_index(self, high_priority):
+ """ Centralized logic to help make extended message support easier in the future"""
+ return OMCI_CC.HIGH_PRIORITY if high_priority and not self._extended_messaging \
+ else OMCI_CC.LOW_PRIORITY
+
+ def _tid_is_high_priority(self, tid):
+ """ Centralized logic to help make extended message support easier in the future"""
+
+ return not self._extended_messaging and \
+ OMCI_CC.MIN_OMCI_TX_ID_HIGH_PRIORITY <= tid <= OMCI_CC.MAX_OMCI_TX_ID_HIGH_PRIORITY
+
+ @staticmethod
+ def event_bus_topic(device_id, event):
+ """
+ Get the topic name for a given event Frame Type
+ :param device_id: (str) ONU Device ID
+ :param event: (OmciCCRxEvents) Type of event
+ :return: (str) Topic string
+ """
+ assert event in OmciCCRxEvents, \
+ 'Event {} is not an OMCI-CC Rx Event'.format(event.name)
+
+ return 'omci-rx:{}:{}'.format(device_id, event.name)
+
+ @property
+ def enabled(self):
+ return self._enabled
+
+ @enabled.setter
+ def enabled(self, value):
+ """
+ Enable/disable the OMCI Communications Channel
+
+ :param value: (boolean) True to enable, False to disable
+ """
+ assert isinstance(value, bool), 'enabled is a boolean'
+
+ if self._enabled != value:
+ self._enabled = value
+ if self._enabled:
+ self._start()
+ else:
+ self._stop()
+
+ @property
+ def tx_frames(self):
+ return self._tx_frames
+
+ @property
+ def rx_frames(self):
+ return self._rx_frames
+
+ @property
+ def rx_unknown_tid(self):
+ return self._rx_unknown_tid # Tx TID not found
+
+ @property
+ def rx_unknown_me(self):
+ return self._rx_unknown_me
+
+ @property
+ def rx_onu_frames(self):
+ return self._rx_onu_frames
+
+ @property
+ def rx_onu_discards(self):
+ return self._rx_onu_discards # Attribute Value change autonomous overflows
+
+ @property
+ def rx_timeouts(self):
+ return self._rx_timeouts
+
+ @property
+ def rx_late(self):
+ return self._rx_late
+
+ @property
+ def tx_errors(self):
+ return self._tx_errors
+
+ @property
+ def consecutive_errors(self):
+ return self._consecutive_errors
+
+ @property
+ def reply_min(self):
+ return int(round(self._reply_min * 1000.0)) # Milliseconds
+
+ @property
+ def reply_max(self):
+ return int(round(self._reply_max * 1000.0)) # Milliseconds
+
+ @property
+ def reply_average(self):
+ avg = self._reply_sum / self._rx_frames if self._rx_frames > 0 else 0.0
+ return int(round(avg * 1000.0)) # Milliseconds
+
+ @property
+ def hp_tx_queue_len(self):
+ return len(self._pending[OMCI_CC.HIGH_PRIORITY])
+
+ @property
+ def lp_tx_queue_len(self):
+ return len(self._pending[OMCI_CC.LOW_PRIORITY])
+
+ @property
+ def max_hp_tx_queue(self):
+ return self._max_hp_tx_queue
+
+ @property
+ def max_lp_tx_queue(self):
+ return self._max_lp_tx_queue
+
+ def _start(self):
+ """
+ Start the OMCI Communications Channel
+ """
+ assert self._enabled, 'Start should only be called if enabled'
+ self.flush()
+
+ device = self._adapter_agent.get_device(self._device_id)
+ self._proxy_address = device.proxy_address
+
+ def _stop(self):
+ """
+ Stop the OMCI Communications Channel
+ """
+ assert not self._enabled, 'Stop should only be called if disabled'
+ self.flush()
+ self._proxy_address = None
+
+ def _receive_onu_message(self, rx_frame):
+ """ Autonomously generated ONU frame Rx handler"""
+ self.log.debug('rx-onu-frame', frame_type=type(rx_frame),
+ frame=hexify(str(rx_frame)))
+
+ msg_type = rx_frame.fields['message_type']
+ self._rx_onu_frames += 1
+
+ msg = {TX_REQUEST_KEY: None,
+ RX_RESPONSE_KEY: rx_frame}
+
+ if msg_type == EntityOperations.AlarmNotification.value:
+ topic = OMCI_CC.event_bus_topic(self._device_id, RxEvent.Alarm_Notification)
+ self.reactor.callLater(0, self.event_bus.publish, topic, msg)
+
+ elif msg_type == EntityOperations.AttributeValueChange.value:
+ topic = OMCI_CC.event_bus_topic(self._device_id, RxEvent.AVC_Notification)
+ self.reactor.callLater(0, self.event_bus.publish, topic, msg)
+
+ elif msg_type == EntityOperations.TestResult.value:
+ topic = OMCI_CC.event_bus_topic(self._device_id, RxEvent.Test_Result)
+ self.reactor.callLater(0, self.event_bus.publish, topic, msg)
+
+ else:
+ self.log.warn('onu-unsupported-autonomous-message', type=msg_type)
+ self._rx_onu_discards += 1
+
+ def _update_rx_tx_stats(self, now, ts):
+ ts_diff = now - arrow.Arrow.utcfromtimestamp(ts)
+ secs = ts_diff.total_seconds()
+ self._reply_sum += secs
+ if secs < self._reply_min:
+ self._reply_min = secs
+ if secs > self._reply_max:
+ self._reply_max = secs
+ return secs
+
+ def receive_message(self, msg):
+ """
+ Receive and OMCI message from the proxy channel to the OLT.
+
+ Call this from your ONU Adapter on a new OMCI Rx on the proxy channel
+ :param msg: (str) OMCI binary message (used as input to Scapy packet decoder)
+ """
+ if not self.enabled:
+ return
+
+ try:
+ now = arrow.utcnow()
+ d = None
+
+ # NOTE: Since we may need to do an independent ME map on a per-ONU basis
+ # save the current value of the entity_id_to_class_map, then
+ # replace it with our custom one before decode, and then finally
+ # restore it later. Tried other ways but really made the code messy.
+ saved_me_map = omci_entities.entity_id_to_class_map
+ omci_entities.entity_id_to_class_map = self._me_map
+
+ try:
+ rx_frame = msg if isinstance(msg, OmciFrame) else OmciFrame(msg)
+ rx_tid = rx_frame.fields['transaction_id']
+
+ if rx_tid == 0:
+ return self._receive_onu_message(rx_frame)
+
+ # Previously unreachable if this is the very first Rx or we
+ # have been running consecutive errors
+ if self._rx_frames == 0 or self._consecutive_errors != 0:
+ self.reactor.callLater(0, self._publish_connectivity_event, True)
+
+ self._rx_frames += 1
+ self._consecutive_errors = 0
+
+ except KeyError as e:
+ # Unknown, Unsupported, or vendor-specific ME. Key is the unknown classID
+ self.log.debug('frame-decode-key-error', msg=hexlify(msg), e=e)
+ rx_frame = self._decode_unknown_me(msg)
+ self._rx_unknown_me += 1
+ rx_tid = rx_frame.fields.get('transaction_id')
+
+ except Exception as e:
+ self.log.exception('frame-decode', msg=hexlify(msg), e=e)
+ return
+
+ finally:
+ omci_entities.entity_id_to_class_map = saved_me_map # Always restore it.
+
+ try:
+ high_priority = self._tid_is_high_priority(rx_tid)
+ index = self._get_priority_index(high_priority)
+
+ # (timestamp, defer, frame, timeout, retry, delayedCall)
+ last_tx_tuple = self._tx_request[index]
+
+ if last_tx_tuple is None or \
+ last_tx_tuple[OMCI_CC.REQUEST_FRAME].fields.get('transaction_id') != rx_tid:
+ # Possible late Rx on a message that timed-out
+ self._rx_unknown_tid += 1
+ self.log.warn('tx-message-missing', rx_id=rx_tid, msg=hexlify(msg))
+ return
+
+ ts, d, tx_frame, timeout, retry, dc = last_tx_tuple
+ if dc is not None and not dc.cancelled and not dc.called:
+ dc.cancel()
+ self.log.debug("cancel-timeout-called")
+
+ secs = self._update_rx_tx_stats(now, ts)
+
+ # Late arrival?
+ if d.called:
+ self._rx_late += 1
+ return
+
+ except Exception as e:
+ self.log.exception('frame-match', msg=hexlify(msg), e=e)
+ if d is not None:
+ return d.errback(failure.Failure(e))
+ return
+
+ # Extended processing needed. Note 'data' field will be None on some error
+ # status returns
+ omci_msg = rx_frame.fields['omci_message']
+
+ if isinstance(omci_msg, OmciGetResponse) and \
+ omci_msg.fields.get('data') is not None and \
+ 'table_attribute_mask' in omci_msg.fields['data']:
+ # Yes, run in a separate generator
+ reactor.callLater(0, self._process_get_rx_frame, timeout, secs,
+ rx_frame, d, tx_frame, high_priority)
+ else:
+ # Publish Rx event to listeners in a different task
+ reactor.callLater(0, self._publish_rx_frame, tx_frame, rx_frame)
+
+ # begin success callback chain (will cancel timeout and queue next Tx message)
+ from copy import copy
+ original_callbacks = copy(d.callbacks)
+ self._rx_response[index] = rx_frame
+ d.callback(rx_frame)
+
+ except Exception as e:
+ self.log.exception('rx-msg', e=e)
+
+ @inlineCallbacks
+ def _process_get_rx_frame(self, timeout, secs, rx_frame, d, tx_frame, high_priority):
+ """
+ Special handling for Get Requests that may require additional 'get_next' operations
+ if a table attribute was requested.
+ """
+ omci_msg = rx_frame.fields['omci_message']
+ if isinstance(omci_msg, OmciGetResponse) and 'table_attribute_mask' in omci_msg.fields['data']:
+ try:
+ entity_class = omci_msg.fields['entity_class']
+ entity_id = omci_msg.fields['entity_id']
+ table_attributes = omci_msg.fields['data']['table_attribute_mask']
+
+ # Table attribute mask is encoded opposite of managed entity mask.
+ if entity_class in self._me_map:
+ ec = self._me_map[entity_class]
+ for index in xrange(16):
+ attr_mask = 1 << index
+
+ if attr_mask & table_attributes:
+ eca = ec.attributes[15-index]
+ self.log.debug('omcc-get-table-attribute', table_name=eca.field.name)
+
+ seq_no = 0
+ data_buffer = ''
+ count = omci_msg.fields['data'][eca.field.name + '_size']
+
+ if count > MAX_TABLE_ROW_COUNT:
+ self.log.error('omcc-get-table-huge', count=count, name=eca.field.name)
+ raise ValueError('Huge Table Size: {}'.format(count))
+
+ # Original timeout must be chopped up into each individual get-next request
+ # in order for total transaction to complete within the timeframe of the
+ # original get() timeout.
+ number_transactions = 1 + (count + OmciTableField.PDU_SIZE - 1) / OmciTableField.PDU_SIZE
+ timeout /= (1 + number_transactions)
+
+ # Start the loop
+ vals = []
+ for offset in xrange(0, count, OmciTableField.PDU_SIZE):
+ frame = MEFrame(ec, entity_id, {eca.field.name: seq_no}).get_next()
+ seq_no += 1
+
+ max_retries = 3
+ results = yield self.send(frame, min(timeout / max_retries, secs * 3), max_retries)
+
+ omci_getnext_msg = results.fields['omci_message']
+ status = omci_getnext_msg.fields['success_code']
+
+ if status != ReasonCodes.Success.value:
+ raise Exception('get-next-failure table=' + eca.field.name +
+ ' entity_id=' + str(entity_id) +
+ ' sqn=' + str(seq_no) + ' omci-status ' + str(status))
+
+ # Extract the data
+ num_octets = count - offset
+ if num_octets > OmciTableField.PDU_SIZE:
+ num_octets = OmciTableField.PDU_SIZE
+
+ data = omci_getnext_msg.fields['data'][eca.field.name]
+ data_buffer += data[:num_octets]
+
+ while data_buffer:
+ data_buffer, val = eca.field.getfield(None, data_buffer)
+ vals.append(val)
+
+ omci_msg.fields['data'][eca.field.name] = vals
+ del omci_msg.fields['data'][eca.field.name + '_size']
+ self.log.debug('omcc-got-table-attribute-rows', table_name=eca.field.name,
+ row_count=len(vals))
+ del omci_msg.fields['data']['table_attribute_mask']
+
+ except Exception as e:
+ self.log.exception('get-next-error', e=e)
+ d.errback(failure.Failure(e), high_priority)
+ return
+
+ # Notify sender of completed request
+ reactor.callLater(0, d.callback, rx_frame, high_priority)
+
+ # Publish Rx event to listeners in a different task except for internally-consumed get-next-response
+ if not isinstance(omci_msg, OmciGetNextResponse):
+ reactor.callLater(0, self._publish_rx_frame, tx_frame, rx_frame)
+
+ def _decode_unknown_me(self, msg):
+ """
+ Decode an ME for an unsupported class ID. This should only occur for a subset
+ of message types (Get, Set, MIB Upload Next, ...) and they should only be
+ responses as well.
+
+ There are some times below that are commented out. For VOLTHA 2.0, it is
+ expected that any get, set, create, delete for unique (often vendor) MEs
+ will be coded by the ONU utilizing it and supplied to OpenOMCI as a
+ vendor-specific ME during device initialization.
+
+ :param msg: (str) Binary data
+ :return: (OmciFrame) resulting frame
+ """
+ from struct import unpack
+
+ (tid, msg_type, framing) = unpack('!HBB', msg[0:4])
+
+ assert framing == 0xa, 'Only basic OMCI framing supported at this time'
+ msg = msg[4:]
+
+ # TODO: Commented out items below are future work (not expected for VOLTHA v2.0)
+ (msg_class, kwargs) = {
+ # OmciCreateResponse.message_id: (OmciCreateResponse, None),
+ # OmciDeleteResponse.message_id: (OmciDeleteResponse, None),
+ # OmciSetResponse.message_id: (OmciSetResponse, None),
+ # OmciGetResponse.message_id: (OmciGetResponse, None),
+ # OmciGetAllAlarmsNextResponse.message_id: (OmciGetAllAlarmsNextResponse, None),
+ OmciMibUploadNextResponse.message_id: (OmciMibUploadNextResponse,
+ {
+ 'entity_class': unpack('!H', msg[0:2])[0],
+ 'entity_id': unpack('!H', msg[2:4])[0],
+ 'object_entity_class': unpack('!H', msg[4:6])[0],
+ 'object_entity_id': unpack('!H', msg[6:8])[0],
+ 'object_attributes_mask': unpack('!H', msg[8:10])[0],
+ 'object_data': {
+ UNKNOWN_CLASS_ATTRIBUTE_KEY: hexlify(msg[10:-4])
+ },
+ }),
+ # OmciAlarmNotification.message_id: (OmciAlarmNotification, None),
+ OmciAttributeValueChange.message_id: (OmciAttributeValueChange,
+ {
+ 'entity_class': unpack('!H', msg[0:2])[0],
+ 'entity_id': unpack('!H', msg[2:4])[0],
+ 'data': {
+ UNKNOWN_CLASS_ATTRIBUTE_KEY: hexlify(msg[4:-8])
+ },
+ }),
+ # OmciTestResult.message_id: (OmciTestResult, None),
+ }.get(msg_type, None)
+
+ if msg_class is None:
+ raise TypeError('Unsupport Message Type for Unknown Decode: {}',
+ msg_type)
+
+ return OmciFrame(transaction_id=tid, message_type=msg_type,
+ omci_message=msg_class(**kwargs))
+
+ def _publish_rx_frame(self, tx_frame, rx_frame):
+ """
+ Notify listeners of successful response frame
+ :param tx_frame: (OmciFrame) Original request frame
+ :param rx_frame: (OmciFrame) Response frame
+ """
+ if self._enabled and isinstance(rx_frame, OmciFrame):
+ frame_type = rx_frame.fields['omci_message'].message_id
+ event_type = OMCI_CC._frame_to_event_type.get(frame_type)
+
+ if event_type is not None:
+ topic = OMCI_CC.event_bus_topic(self._device_id, event_type)
+ msg = {TX_REQUEST_KEY: tx_frame,
+ RX_RESPONSE_KEY: rx_frame}
+
+ self.event_bus.publish(topic=topic, msg=msg)
+
+ def _publish_connectivity_event(self, connected):
+ """
+ Notify listeners of Rx/Tx connectivity over OMCI
+ :param connected: (bool) True if connectivity transitioned from unreachable
+ to reachable
+ """
+ if self._enabled:
+ topic = OMCI_CC.event_bus_topic(self._device_id,
+ RxEvent.Connectivity)
+ msg = {CONNECTED_KEY: connected}
+ self.event_bus.publish(topic=topic, msg=msg)
+
+ def flush(self):
+ """Flush/cancel in active or pending Tx requests"""
+ requests = []
+
+ for priority in {OMCI_CC.HIGH_PRIORITY, OMCI_CC.LOW_PRIORITY}:
+ next_frame, self._tx_request[priority] = self._tx_request[priority], None
+ if next_frame is not None:
+ requests.append((next_frame[OMCI_CC.REQUEST_DEFERRED], next_frame[OMCI_CC.REQUEST_DELAYED_CALL]))
+
+ requests += [(next_frame[OMCI_CC.PENDING_DEFERRED], None)
+ for next_frame in self._pending[priority]]
+ self._pending[priority] = list()
+
+ # Cancel them...
+ def cleanup_unhandled_error(_):
+ pass # So the cancel below does not flag an unhandled error
+
+ for d, dc in requests:
+ if d is not None and not d.called:
+ d.addErrback(cleanup_unhandled_error)
+ d.cancel()
+
+ if dc is not None and not dc.called and not dc.cancelled:
+ dc.cancel()
+
+ def _get_tx_tid(self, high_priority=False):
+ """
+ Get the next Transaction ID for a tx. Note TID=0 is reserved
+ for autonomously generated messages from an ONU
+
+ :return: (int) TID
+ """
+ if self._extended_messaging or not high_priority:
+ index = OMCI_CC.LOW_PRIORITY
+ min_tid = OMCI_CC.MIN_OMCI_TX_ID_LOW_PRIORITY
+ max_tid = OMCI_CC.MAX_OMCI_TX_ID_LOW_PRIORITY
+ else:
+ index = OMCI_CC.HIGH_PRIORITY
+ min_tid = OMCI_CC.MIN_OMCI_TX_ID_HIGH_PRIORITY
+ max_tid = OMCI_CC.MAX_OMCI_TX_ID_HIGH_PRIORITY
+
+ tx_tid, self._tx_tid[index] = self._tx_tid[index], self._tx_tid[index] + 1
+
+ if self._tx_tid[index] > max_tid:
+ self._tx_tid[index] = min_tid
+
+ return tx_tid
+
+ def _request_failure(self, value, tx_tid, high_priority):
+ """
+ Handle a transmit failure. Rx Timeouts are handled on the 'dc' deferred and
+ will call a different method that may retry if requested. This routine
+ will be called after the final (if any) timeout or other error
+
+ :param value: (Failure) Twisted failure
+ :param tx_tid: (int) Associated Tx TID
+ """
+ index = self._get_priority_index(high_priority)
+
+ if self._tx_request[index] is not None:
+ tx_frame = self._tx_request[index][OMCI_CC.REQUEST_FRAME]
+ tx_frame_tid = tx_frame.fields['transaction_id']
+
+ if tx_frame_tid == tx_tid:
+ timeout = self._tx_request[index][OMCI_CC.REQUEST_TIMEOUT]
+ dc = self._tx_request[index][OMCI_CC.REQUEST_DELAYED_CALL]
+ self._tx_request[index] = None
+
+ if dc is not None and not dc.called and not dc.cancelled:
+ dc.cancel()
+
+ if isinstance(value, failure.Failure):
+ value.trap(CancelledError)
+ self._rx_timeouts += 1
+ self._consecutive_errors += 1
+ if self._consecutive_errors == 1:
+ reactor.callLater(0, self._publish_connectivity_event, False)
+
+ self.log.debug('timeout', tx_id=tx_tid, timeout=timeout)
+ value = failure.Failure(TimeoutError(timeout, "Deferred"))
+ else:
+ # Search pending queue. This may be a cancel coming in from the original
+ # task that requested the Tx. If found, remove
+ # from pending queue
+ for index, request in enumerate(self._pending[index]):
+ req = request.get(OMCI_CC.PENDING_DEFERRED)
+ if req is not None and req.fields['transaction_id'] == tx_tid:
+ self._pending[index].pop(index)
+ break
+
+ self._send_next_request(high_priority)
+ return value
+
+ def _request_success(self, rx_frame, high_priority):
+ """
+ Handle transmit success (a matching Rx was received)
+
+ :param rx_frame: (OmciFrame) OMCI response frame with matching TID
+ :return: (OmciFrame) OMCI response frame with matching TID
+ """
+ index = self._get_priority_index(high_priority)
+
+ if rx_frame is None:
+ rx_frame = self._rx_response[index]
+
+ rx_tid = rx_frame.fields.get('transaction_id')
+
+ if rx_tid is not None:
+ if self._tx_request[index] is not None:
+ tx_frame = self._tx_request[index][OMCI_CC.REQUEST_FRAME]
+ tx_tid = tx_frame.fields['transaction_id']
+
+ if rx_tid == tx_tid:
+ # Remove this request. Next callback in chain initiates next Tx
+ self._tx_request[index] = None
+ else:
+ self._rx_late += 1
+ else:
+ self._rx_late += 1
+
+ self._send_next_request(high_priority)
+
+ # Return rx_frame (to next item in callback list)
+ return rx_frame
+
+ def _request_timeout(self, tx_tid, high_priority):
+ """
+ Tx Request timed out. Resend immediately if there retries is non-zero. A
+ separate deferred (dc) is used on each actual Tx which is not the deferred
+ (d) that is returned to the caller of the 'send()' method.
+
+ :param tx_tid: (int) TID of frame
+ :param high_priority: (bool) True if high-priority queue
+ """
+ self.log.debug("_request_timeout", tx_tid=tx_tid)
+ index = self._get_priority_index(high_priority)
+
+ if self._tx_request[index] is not None:
+ # (0: timestamp, 1: defer, 2: frame, 3: timeout, 4: retry, 5: delayedCall)
+ ts, d, frame, timeout, retry, _dc = self._tx_request[index]
+
+ if frame.fields.get('transaction_id', 0) == tx_tid:
+ self._tx_request[index] = None
+
+ if retry > 0:
+ # Push on front of TX pending queue so that it transmits next with the
+ # original TID
+ self._queue_frame(d, frame, timeout, retry - 1, high_priority, front=True)
+ else:
+ d.errback(failure.Failure(TimeoutError(timeout, "Send OMCI TID -{}".format(tx_tid))))
+
+ self._send_next_request(high_priority)
+
+ def _queue_frame(self, d, frame, timeout, retry, high_priority, front=False):
+ index = self._get_priority_index(high_priority)
+ tx_tuple = (d, frame, timeout, retry) # Pending -> (deferred, tx_frame, timeout, retry)
+
+ if front:
+ self._pending[index].insert(0, tuple)
+ else:
+ self._pending[index].append(tx_tuple)
+
+ # Monitor queue stats
+ qlen = len(self._pending[index])
+
+ if high_priority:
+ if self._max_hp_tx_queue < qlen:
+ self._max_hp_tx_queue = qlen
+
+ elif self._max_lp_tx_queue < qlen:
+ self._max_lp_tx_queue = qlen
+
+ def send(self, frame, timeout=DEFAULT_OMCI_TIMEOUT, retry=0, high_priority=False):
+ """
+ Queue the OMCI Frame for a transmit to the ONU via the proxy_channel
+
+ :param frame: (OMCIFrame) Message to send
+ :param timeout: (int) Rx Timeout. 0=No response needed
+ :param retry: (int) Additional retry attempts on channel failure, default=0
+ :param high_priority: (bool) High Priority requests
+ :return: (deferred) A deferred that fires when the response frame is received
+ or if an error/timeout occurs
+ """
+ if not self.enabled or self._proxy_address is None:
+ # TODO custom exceptions throughout this code would be helpful
+ self._tx_errors += 1
+ return fail(result=failure.Failure(Exception('OMCI is not enabled')))
+
+ timeout = float(timeout)
+ if timeout > float(MAX_OMCI_REQUEST_AGE):
+ self._tx_errors += 1
+ msg = 'Maximum timeout is {} seconds'.format(MAX_OMCI_REQUEST_AGE)
+ return fail(result=failure.Failure(Exception(msg)))
+
+ if not isinstance(frame, OmciFrame):
+ self._tx_errors += 1
+ msg = "Invalid frame class '{}'".format(type(frame))
+ return fail(result=failure.Failure(Exception(msg)))
+ try:
+ index = self._get_priority_index(high_priority)
+ tx_tid = frame.fields['transaction_id']
+
+ if tx_tid is None:
+ tx_tid = self._get_tx_tid(high_priority=high_priority)
+ frame.fields['transaction_id'] = tx_tid
+
+ assert tx_tid not in self._pending[index], 'TX TID {} is already exists'.format(tx_tid)
+ assert tx_tid > 0, 'Invalid Tx TID: {}'.format(tx_tid)
+
+ # Queue it and request next Tx if tx channel is free
+ d = defer.Deferred()
+
+ self._queue_frame(d, frame, timeout, retry, high_priority, front=False)
+ self._send_next_request(high_priority)
+
+ if timeout == 0:
+ self.log.debug("send-timeout-zero", tx_tid=tx_tid)
+ self.reactor.callLater(0, d.callback, 'queued')
+
+ return d
+
+ except Exception as e:
+ self._tx_errors += 1
+ self._consecutive_errors += 1
+
+ if self._consecutive_errors == 1:
+ self.reactor.callLater(0, self._publish_connectivity_event, False)
+
+ self.log.exception('send-omci', e=e)
+ return fail(result=failure.Failure(e))
+
+ def _ok_to_send(self, tx_request, high_priority):
+ """
+ G.988 specifies not to issue a MIB upload or a Software download request
+ when a similar action is in progress on the other channel. To keep the
+ logic here simple, a new upload/download will not be allowed if either a
+ upload/download is going on
+
+ :param tx_request (OmciFrame) Frame to send
+ :param high_priority: (bool) for queue selection
+ :return: True if okay to dequeue and send frame
+ """
+ other = self._get_priority_index(not high_priority)
+
+ if self._tx_request[other] is None:
+ return True
+
+ this_msg_type = tx_request.fields['message_type'] & 0x1f
+ not_allowed = {OP.MibUpload.value,
+ OP.MibUploadNext.value,
+ OP.StartSoftwareDownload.value,
+ OP.DownloadSection.value,
+ OP.EndSoftwareDownload.value}
+
+ if this_msg_type not in not_allowed:
+ return True
+
+ other_msg_type = self._tx_request[other][OMCI_CC.REQUEST_FRAME].fields['message_type'] & 0x1f
+ return other_msg_type not in not_allowed
+
+ def _send_next_request(self, high_priority):
+ """
+ Pull next tx request and send it
+
+ :param high_priority: (bool) True if this was a high priority request
+ :return: results, so callback chain continues if needed
+ """
+ index = self._get_priority_index(high_priority)
+
+ if self._tx_request[index] is None: # TODO or self._tx_request[index][OMCI_CC.REQUEST_DEFERRED].called:
+ d = None
+ try:
+ if len(self._pending[index]) and \
+ not self._ok_to_send(self._pending[index][0][OMCI_CC.PENDING_FRAME],
+ high_priority):
+ reactor.callLater(0.05, self._send_next_request, high_priority)
+ return
+
+ next_frame = self._pending[index].pop(0)
+
+ d = next_frame[OMCI_CC.PENDING_DEFERRED]
+ frame = next_frame[OMCI_CC.PENDING_FRAME]
+ timeout = next_frame[OMCI_CC.PENDING_TIMEOUT]
+ retry = next_frame[OMCI_CC.PENDING_RETRY]
+
+ tx_tid = frame.fields['transaction_id']
+
+ # NOTE: Since we may need to do an independent ME map on a per-ONU basis
+ # save the current value of the entity_id_to_class_map, then
+ # replace it with our custom one before decode, and then finally
+ # restore it later. Tried other ways but really made the code messy.
+ saved_me_map = omci_entities.entity_id_to_class_map
+ omci_entities.entity_id_to_class_map = self._me_map
+
+ ts = arrow.utcnow().float_timestamp
+ try:
+ self._rx_response[index] = None
+ self._adapter_agent.send_proxied_message(self._proxy_address,
+ hexify(str(frame)))
+ finally:
+ omci_entities.entity_id_to_class_map = saved_me_map
+
+ self._tx_frames += 1
+
+ if timeout > 0:
+ # Timeout on internal deferred to support internal retries if requested
+ dc = self.reactor.callLater(timeout, self._request_timeout, tx_tid, high_priority)
+
+ # (timestamp, defer, frame, timeout, retry, delayedCall)
+ self._tx_request[index] = (ts, d, frame, timeout, retry, dc)
+ d.addCallbacks(self._request_success, self._request_failure,
+ callbackArgs=(high_priority,),
+ errbackArgs=(tx_tid, high_priority))
+
+ except IndexError:
+ pass # Nothing pending in this queue
+
+ except Exception as e:
+ self.log.exception('send-proxy-exception', e=e)
+ self._tx_request[index] = None
+ self.reactor.callLater(0, self._send_next_request, high_priority)
+
+ if d is not None:
+ d.errback(failure.Failure(e))
+
+ ###################################################################################
+ # MIB Action shortcuts
+
+ def send_mib_reset(self, timeout=DEFAULT_OMCI_TIMEOUT, high_priority=False):
+ """
+ Perform a MIB Reset
+ """
+ self.log.debug('send-mib-reset')
+
+ frame = OntDataFrame().mib_reset()
+ return self.send(frame, timeout=timeout, high_priority=high_priority)
+
+ def send_mib_upload(self, timeout=DEFAULT_OMCI_TIMEOUT, high_priority=False):
+ self.log.debug('send-mib-upload')
+
+ frame = OntDataFrame().mib_upload()
+ return self.send(frame, timeout=timeout, high_priority=high_priority)
+
+ def send_mib_upload_next(self, seq_no, timeout=DEFAULT_OMCI_TIMEOUT, high_priority=False):
+ self.log.debug('send-mib-upload-next')
+
+ frame = OntDataFrame(sequence_number=seq_no).mib_upload_next()
+ return self.send(frame, timeout=timeout, high_priority=high_priority)
+
+ def send_reboot(self, timeout=DEFAULT_OMCI_TIMEOUT, high_priority=False):
+ """
+ Send an ONU Device reboot request (ONU-G ME).
+
+ NOTICE: This method is being deprecated and replaced with a tasks to preform this function
+ """
+ self.log.debug('send-mib-reboot')
+
+ frame = OntGFrame().reboot()
+ return self.send(frame, timeout=timeout, high_priority=high_priority)
+
+ def send_get_all_alarm(self, alarm_retrieval_mode=0, timeout=DEFAULT_OMCI_TIMEOUT, high_priority=False):
+ self.log.debug('send_get_alarm')
+
+ frame = OntDataFrame().get_all_alarm(alarm_retrieval_mode)
+ return self.send(frame, timeout=timeout, high_priority=high_priority)
+
+ def send_get_all_alarm_next(self, seq_no, timeout=DEFAULT_OMCI_TIMEOUT, high_priority=False):
+ self.log.debug('send_get_alarm_next')
+
+ frame = OntDataFrame().get_all_alarm_next(seq_no)
+ return self.send(frame, timeout=timeout, high_priority=high_priority)
+
+ def send_start_software_download(self, image_inst_id, image_size, window_size, timeout=DEFAULT_OMCI_TIMEOUT, high_priority=False):
+ frame = SoftwareImageFrame(image_inst_id).start_software_download(image_size, window_size-1)
+ return self.send(frame, timeout, 3, high_priority=high_priority)
+
+ def send_download_section(self, image_inst_id, section_num, data, size=DEFAULT_OMCI_DOWNLOAD_SECTION_SIZE, timeout=0, high_priority=False):
+ """
+ # timeout=0 indicates no repons needed
+ """
+ # self.log.debug("send_download_section", instance_id=image_inst_id, section=section_num, timeout=timeout)
+ if timeout > 0:
+ frame = SoftwareImageFrame(image_inst_id).download_section(True, section_num, data)
+ else:
+ frame = SoftwareImageFrame(image_inst_id).download_section(False, section_num, data)
+ return self.send(frame, timeout, high_priority=high_priority)
+
+ # if timeout > 0:
+ # self.reactor.callLater(0, self.sim_receive_download_section_resp,
+ # frame.fields["transaction_id"],
+ # frame.fields["omci_message"].fields["section_number"])
+ # return d
+
+ def send_end_software_download(self, image_inst_id, crc32, image_size, timeout=DEFAULT_OMCI_TIMEOUT, high_priority=False):
+ frame = SoftwareImageFrame(image_inst_id).end_software_download(crc32, image_size)
+ return self.send(frame, timeout, high_priority=high_priority)
+ # self.reactor.callLater(0, self.sim_receive_end_software_download_resp, frame.fields["transaction_id"])
+ # return d
+
+ def send_active_image(self, image_inst_id, flag=0, timeout=DEFAULT_OMCI_TIMEOUT, high_priority=False):
+ frame = SoftwareImageFrame(image_inst_id).activate_image(flag)
+ return self.send(frame, timeout, high_priority=high_priority)
+
+ def send_commit_image(self, image_inst_id, timeout=DEFAULT_OMCI_TIMEOUT, high_priority=False):
+ frame = SoftwareImageFrame(image_inst_id).commit_image()
+ return self.send(frame, timeout, high_priority=high_priority)
+
diff --git a/python/adapters/extensions/omci/omci_defs.py b/python/adapters/extensions/omci/omci_defs.py
new file mode 100644
index 0000000..64fefc5
--- /dev/null
+++ b/python/adapters/extensions/omci/omci_defs.py
@@ -0,0 +1,100 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from enum import Enum, IntEnum
+
+class OmciUninitializedFieldError(Exception):
+ pass
+
+
+class OmciInvalidTypeError(Exception):
+ pass
+
+def bitpos_from_mask(mask, lsb_pos=0, increment=1):
+ """
+ Turn a decimal value (bitmask) into a list of indices where each
+ index value corresponds to the bit position of a bit that was set (1)
+ in the mask. What numbers are assigned to the bit positions is controlled
+ by lsb_pos and increment, as explained below.
+ :param mask: a decimal value used as a bit mask
+ :param lsb_pos: The decimal value associated with the LSB bit
+ :param increment: If this is +i, then the bit next to LSB will take
+ the decimal value of lsb_pos + i.
+ :return: List of bit positions where the bit was set in mask
+ """
+ out = []
+ while mask:
+ if mask & 0x01:
+ out.append(lsb_pos)
+ lsb_pos += increment
+ mask >>= 1
+ return sorted(out)
+
+
+class AttributeAccess(Enum):
+ Readable = 1
+ R = 1
+ Writable = 2
+ W = 2
+ SetByCreate = 3
+ SBC = 3
+
+
+OmciNullPointer = 0xffff
+OmciSectionDataSize = 31
+
+class EntityOperations(Enum):
+ # keep these numbers match msg_type field per OMCI spec
+ Create = 4
+ CreateComplete = 5
+ Delete = 6
+ Set = 8
+ Get = 9
+ GetComplete = 10
+ GetAllAlarms = 11
+ GetAllAlarmsNext = 12
+ MibUpload = 13
+ MibUploadNext = 14
+ MibReset = 15
+ AlarmNotification = 16
+ AttributeValueChange = 17
+ Test = 18
+ StartSoftwareDownload = 19
+ DownloadSection = 20
+ EndSoftwareDownload = 21
+ ActivateSoftware = 22
+ CommitSoftware = 23
+ SynchronizeTime = 24
+ Reboot = 25
+ GetNext = 26
+ TestResult = 27
+ GetCurrentData = 28
+ SetTable = 29 # Defined in Extended Message Set Only
+
+
+class ReasonCodes(IntEnum):
+ # OMCI Result and reason codes
+ Success = 0, # Command processed successfully
+ ProcessingError = 1, # Command processing error
+ NotSupported = 2, # Command not supported
+ ParameterError = 3, # Parameter error
+ UnknownEntity = 4, # Unknown managed entity
+ UnknownInstance = 5, # Unknown managed entity instance
+ DeviceBusy = 6, # Device busy
+ InstanceExists = 7, # Instance Exists
+ AttributeFailure = 9, # Attribute(s) failed or unknown
+
+ OperationCancelled = 255 # Proprietary defined for internal use
+
diff --git a/python/adapters/extensions/omci/omci_entities.py b/python/adapters/extensions/omci/omci_entities.py
new file mode 100644
index 0000000..95e6581
--- /dev/null
+++ b/python/adapters/extensions/omci/omci_entities.py
@@ -0,0 +1,1564 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import inspect
+
+import sys
+from binascii import hexlify
+from bitstring import BitArray
+import json
+from scapy.fields import ByteField, ShortField, MACField, BitField, IPField
+from scapy.fields import IntField, StrFixedLenField, LongField, FieldListField, PacketLenField
+from scapy.packet import Packet
+
+from voltha.extensions.omci.omci_defs import OmciUninitializedFieldError, \
+ AttributeAccess, OmciNullPointer, EntityOperations, OmciInvalidTypeError
+from voltha.extensions.omci.omci_fields import OmciSerialNumberField, OmciTableField
+from voltha.extensions.omci.omci_defs import bitpos_from_mask
+
+
+class EntityClassAttribute(object):
+
+ def __init__(self, fld, access=set(), optional=False, range_check=None,
+ avc=False, tca=False, counter=False, deprecated=False):
+ """
+ Initialize an Attribute for a Managed Entity Class
+
+ :param fld: (Field) Scapy field type
+ :param access: (AttributeAccess) Allowed access
+ :param optional: (boolean) If true, attribute is option, else mandatory
+ :param range_check: (callable) None, Lambda, or Function to validate value
+ :param avc: (boolean) If true, an AVC notification can occur for the attribute
+ :param tca: (boolean) If true, a threshold crossing alert alarm notification can occur
+ for the attribute
+ :param counter: (boolean) If true, this attribute is a PM counter
+ :param deprecated: (boolean) If true, this attribute is deprecated and
+ only 'read' operations (if-any) performed.
+ """
+ self._fld = fld
+ self._access = access
+ self._optional = optional
+ self._range_check = range_check
+ self._avc = avc
+ self._tca = tca
+ self._counter = counter
+ self._deprecated = deprecated
+
+ @property
+ def field(self):
+ return self._fld
+
+ @property
+ def access(self):
+ return self._access
+
+ @property
+ def optional(self):
+ return self._optional
+
+ @property
+ def is_counter(self):
+ return self._counter
+
+ @property
+ def range_check(self):
+ return self._range_check
+
+ @property
+ def avc_allowed(self):
+ return self._avc
+
+ @property
+ def deprecated(self):
+ return self._deprecated
+
+ _type_checker_map = {
+ 'ByteField': lambda val: isinstance(val, (int, long)) and 0 <= val <= 0xFF,
+ 'ShortField': lambda val: isinstance(val, (int, long)) and 0 <= val <= 0xFFFF,
+ 'IntField': lambda val: isinstance(val, (int, long)) and 0 <= val <= 0xFFFFFFFF,
+ 'LongField': lambda val: isinstance(val, (int, long)) and 0 <= val <= 0xFFFFFFFFFFFFFFFF,
+ 'StrFixedLenField': lambda val: isinstance(val, basestring),
+ 'MACField': lambda val: True, # TODO: Add a constraint for this field type
+ 'BitField': lambda val: True, # TODO: Add a constraint for this field type
+ 'IPField': lambda val: True, # TODO: Add a constraint for this field type
+ 'OmciTableField': lambda val: True,
+
+ # TODO: As additional Scapy field types are used, add constraints
+ }
+
+ def valid(self, value):
+ def _isa_lambda_function(v):
+ import inspect
+ return callable(v) and len(inspect.getargspec(v).args) == 1
+
+ field_type = self.field.__class__.__name__
+ type_check = EntityClassAttribute._type_checker_map.get(field_type,
+ lambda val: True)
+
+ # TODO: Currently StrFixedLenField is used heavily for both bit fields as
+ # and other 'byte/octet' related strings that are NOT textual. Until
+ # all of these are corrected, 'StrFixedLenField' cannot test the type
+ # of the value provided
+
+ if field_type != 'StrFixedLenField' and not type_check(value):
+ return False
+
+ if _isa_lambda_function(self.range_check):
+ return self.range_check(value)
+ return True
+
+
+class EntityClassMeta(type):
+ """
+ Metaclass for EntityClass to generate secondary class attributes
+ for class attributes of the derived classes.
+ """
+ def __init__(cls, name, bases, dct):
+ super(EntityClassMeta, cls).__init__(name, bases, dct)
+
+ # initialize attribute_name_to_index_map
+ cls.attribute_name_to_index_map = dict(
+ (a._fld.name, idx) for idx, a in enumerate(cls.attributes))
+
+
+class EntityClass(object):
+
+ class_id = 'to be filled by subclass'
+ attributes = []
+ mandatory_operations = set()
+ optional_operations = set()
+ notifications = set()
+ alarms = dict() # Alarm Number -> Alarm Name
+ hidden = False # If true, this attribute is not reported by a MIB upload.
+ # This attribute is needed to be able to properly perform
+ # MIB Audits.
+
+ # will be map of attr_name -> index in attributes, initialized by metaclass
+ attribute_name_to_index_map = None
+ __metaclass__ = EntityClassMeta
+
+ def __init__(self, **kw):
+ assert(isinstance(kw, dict))
+ for k, v in kw.iteritems():
+ assert(k in self.attribute_name_to_index_map)
+ self._data = kw
+
+ def serialize(self, mask=None, operation=None):
+ octets = ''
+
+ # generate ordered list of attribute indices needed to be processed
+ # if mask is provided, we use that explicitly
+ # if mask is not provided, we determine attributes from the self._data
+ # content also taking into account the type of operation in hand
+ if mask is not None:
+ attribute_indices = EntityClass.attribute_indices_from_mask(mask)
+ else:
+ attribute_indices = self.attribute_indices_from_data()
+
+ # Serialize each indexed field (ignoring entity id)
+ for index in attribute_indices:
+ eca = self.attributes[index]
+ field = eca.field
+ try:
+ value = self._data[field.name]
+
+ if not eca.valid(value):
+ raise OmciInvalidTypeError(
+ 'Value "{}" for Entity field "{}" is not valid'.format(value,
+ field.name))
+ except KeyError:
+ raise OmciUninitializedFieldError(
+ 'Entity field "{}" not set'.format(field.name))
+
+ octets = field.addfield(None, octets, value)
+
+ return octets
+
+ def attribute_indices_from_data(self):
+ return sorted(
+ self.attribute_name_to_index_map[attr_name]
+ for attr_name in self._data.iterkeys())
+
+ byte1_mask_to_attr_indices = dict(
+ (m, bitpos_from_mask(m, 8, -1)) for m in range(256))
+ byte2_mask_to_attr_indices = dict(
+ (m, bitpos_from_mask(m, 16, -1)) for m in range(256))
+
+ @classmethod
+ def attribute_indices_from_mask(cls, mask):
+ # each bit in the 2-byte field denote an attribute index; we use a
+ # lookup table to make lookup a bit faster
+ return \
+ cls.byte1_mask_to_attr_indices[(mask >> 8) & 0xff] + \
+ cls.byte2_mask_to_attr_indices[(mask & 0xff)]
+
+ @classmethod
+ def mask_for(cls, *attr_names):
+ """
+ Return mask value corresponding to given attributes names
+ :param attr_names: Attribute names
+ :return: integer mask value
+ """
+ mask = 0
+ for attr_name in attr_names:
+ index = cls.attribute_name_to_index_map[attr_name]
+ mask |= (1 << (16 - index))
+ return mask
+
+
+# abbreviations
+ECA = EntityClassAttribute
+AA = AttributeAccess
+OP = EntityOperations
+
+
+class OntData(EntityClass):
+ class_id = 2
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R},
+ range_check=lambda x: x == 0),
+ # Only 1 octet used if GET/SET operation
+ ECA(ShortField("mib_data_sync", 0), {AA.R, AA.W})
+ ]
+ mandatory_operations = {OP.Get, OP.Set,
+ OP.GetAllAlarms, OP.GetAllAlarmsNext,
+ OP.MibReset, OP.MibUpload, OP.MibUploadNext}
+
+
+class Cardholder(EntityClass):
+ class_id = 5
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R},
+ range_check=lambda x: 0 <= x < 255 or 256 <= x < 511,
+ avc=True),
+ ECA(ByteField("actual_plugin_unit_type", None), {AA.R}),
+ ECA(ByteField("expected_plugin_unit_type", None), {AA.R, AA.W}),
+ ECA(ByteField("expected_port_count", None), {AA.R, AA.W},
+ optional=True),
+ ECA(StrFixedLenField("expected_equipment_id", None, 20), {AA.R, AA.W},
+ optional=True, avc=True),
+ ECA(StrFixedLenField("actual_equipment_id", None, 20), {AA.R},
+ optional=True),
+ ECA(ByteField("protection_profile_pointer", None), {AA.R},
+ optional=True),
+ ECA(ByteField("invoke_protection_switch", None), {AA.R, AA.W},
+ optional=True, range_check=lambda x: 0 <= x <= 3),
+ ECA(ByteField("alarm_reporting_control", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1, optional=True, avc=True),
+ ECA(ByteField("arc_interval", 0), {AA.R, AA.W}, optional=True),
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+ notifications = {OP.AttributeValueChange, OP.AlarmNotification}
+ alarms = {
+ 0: 'Plug-in circuit pack missing',
+ 1: 'Plug-in type mismatch alarm',
+ 2: 'Improper card removal',
+ 3: 'Plug-in equipment ID mismatch alarm',
+ 4: 'Protection switch',
+ }
+
+
+class CircuitPack(EntityClass):
+ class_id = 6
+ attributes = [
+ ECA(StrFixedLenField("managed_entity_id", None, 22), {AA.R, AA.SBC},
+ range_check=lambda x: 0 <= x < 255 or 256 <= x < 511),
+ ECA(ByteField("type", None), {AA.R, AA.SBC}),
+ ECA(ByteField("number_of_ports", None), {AA.R}, optional=True),
+ ECA(OmciSerialNumberField("serial_number"), {AA.R}),
+ ECA(StrFixedLenField("version", None, 14), {AA.R}),
+ ECA(StrFixedLenField("vendor_id", None, 4), {AA.R}),
+ ECA(ByteField("administrative_state", None), {AA.R, AA.W}),
+ ECA(ByteField("operational_state", None), {AA.R}, optional=True, avc=True),
+ ECA(ByteField("bridged_or_ip_ind", None), {AA.R, AA.W}, optional=True,
+ range_check=lambda x: 0 <= x <= 2),
+ ECA(StrFixedLenField("equipment_id", None, 20), {AA.R}, optional=True),
+ ECA(ByteField("card_configuration", None), {AA.R, AA.W, AA.SBC},
+ optional=True), # not really mandatory, see spec ITU-T G.988, 9.1.6
+ ECA(ByteField("total_tcont_buffer_number", None), {AA.R},
+ optional=True), # not really mandatory, see spec ITU-T G.988, 9.1.6
+ ECA(ByteField("total_priority_queue_number", None), {AA.R},
+ optional=True), # not really mandatory, see spec ITU-T G.988, 9.1.6
+ ECA(ByteField("total_traffic_scheduler_number", None), {AA.R},
+ optional=True), # not really mandatory, see spec ITU-T G.988, 9.1.6
+ ECA(IntField("power_shed_override", None), {AA.R, AA.W},
+ optional=True)
+ ]
+ mandatory_operations = {OP.Get, OP.Set, OP.Reboot}
+ optional_operations = {OP.Create, OP.Delete, OP.Test}
+ notifications = {OP.AttributeValueChange, OP.AlarmNotification}
+ alarms = {
+ 0: 'Equipment alarm',
+ 1: 'Powering alarm',
+ 2: 'Self-test failure',
+ 3: 'Laser end of life',
+ 4: 'Temperature yellow',
+ 5: 'Temperature red',
+ }
+
+class SoftwareImage(EntityClass):
+ class_id = 7
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R},
+ range_check=lambda x: 0 <= x/256 <= 254 or 0 <= x % 256 <= 1),
+ ECA(StrFixedLenField("version", None, 14), {AA.R}, avc=True),
+ ECA(ByteField("is_committed", None), {AA.R}, avc=True,
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("is_active", None), {AA.R}, avc=True,
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("is_valid", None), {AA.R}, avc=True,
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(StrFixedLenField("product_code", None, 25), {AA.R}, optional=True, avc=True),
+ ECA(StrFixedLenField("image_hash", None, 16), {AA.R}, optional=True, avc=True),
+ ]
+ mandatory_operations = {OP.Get, OP.StartSoftwareDownload, OP.DownloadSection,
+ OP.EndSoftwareDownload, OP.ActivateSoftware,
+ OP.CommitSoftware}
+ notifications = {OP.AttributeValueChange}
+
+
+class PptpEthernetUni(EntityClass):
+ class_id = 11
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R}),
+ ECA(ByteField("expected_type", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 254),
+ ECA(ByteField("sensed_type", 0), {AA.R}, optional=True, avc=True),
+ # TODO: For sensed_type AVC, see note in AT&T OMCI Specification, V3.0, page 123
+ ECA(ByteField("auto_detection_configuration", 0), {AA.R, AA.W},
+ range_check=lambda x: x in [0, 1, 2, 3, 4, 5,
+ 0x10, 0x11, 0x12, 0x13, 0x14,
+ 0x20, 0x30], optional=True), # See ITU-T G.988
+ ECA(ByteField("ethernet_loopback_configuration", 0), {AA.R, AA.W},
+ range_check=lambda x: x in [0, 3]),
+ ECA(ByteField("administrative_state", 1), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("operational_state", 1), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1, optional=True, avc=True),
+ ECA(ByteField("configuration_ind", 0), {AA.R},
+ range_check=lambda x: x in [0, 1, 2, 3, 4, 0x11, 0x12, 0x13]),
+ ECA(ShortField("max_frame_size", 1518), {AA.R, AA.W}, optional=True),
+ ECA(ByteField("dte_or_dce_ind", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 2),
+ ECA(ShortField("pause_time", 0), {AA.R, AA.W}, optional=True),
+ ECA(ByteField("bridged_or_ip_ind", 2), {AA.R, AA.W},
+ optional=True, range_check=lambda x: 0 <= x <= 2),
+ ECA(ByteField("arc", 0), {AA.R, AA.W}, optional=True,
+ range_check=lambda x: 0 <= x <= 1, avc=True),
+ ECA(ByteField("arc_interval", 0), {AA.R, AA.W}, optional=True),
+ ECA(ByteField("pppoe_filter", 0), {AA.R, AA.W}, optional=True),
+ ECA(ByteField("power_control", 0), {AA.R, AA.W}, optional=True),
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+ notifications = {OP.AttributeValueChange, OP.AlarmNotification}
+ alarms = {
+ 0: 'LAN Loss Of Signal',
+ }
+
+
+class MacBridgeServiceProfile(EntityClass):
+ class_id = 45
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("spanning_tree_ind", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("learning_ind", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("port_bridging_ind", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("priority", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("max_age", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0x0600 <= x <= 0x2800),
+ ECA(ShortField("hello_time", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0x0100 <= x <= 0x0A00),
+ ECA(ShortField("forward_delay", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0x0400 <= x <= 0x1E00),
+ ECA(ByteField("unknown_mac_address_discard", None),
+ {AA.R, AA.W, AA.SBC}, range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("mac_learning_depth", None),
+ {AA.R, AA.W, AA.SBC}, optional=True),
+ ECA(ByteField("dynamic_filtering_ageing_time", None),
+ {AA.R, AA.W, AA.SBC}, optional=True),
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+
+
+class MacBridgePortConfigurationData(EntityClass):
+ class_id = 47
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ShortField("bridge_id_pointer", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("port_num", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("tp_type", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 1 <= x <= 12),
+ ECA(ShortField("tp_pointer", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("port_priority", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("port_path_cost", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("port_spanning_tree_in", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("encapsulation_methods", None), {AA.R, AA.W, AA.SBC},
+ optional=True, deprecated=True),
+ ECA(ByteField("lan_fcs_ind", None), {AA.R, AA.W, AA.SBC},
+ optional=True, deprecated=True),
+ ECA(MACField("port_mac_address", None), {AA.R}, optional=True),
+ ECA(ShortField("outbound_td_pointer", None), {AA.R, AA.W},
+ optional=True),
+ ECA(ShortField("inbound_td_pointer", None), {AA.R, AA.W},
+ optional=True),
+ # TODO:
+ ECA(ByteField("mac_learning_depth", 0), {AA.R, AA.W, AA.SBC},
+ optional=True),
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 0: 'Port blocking',
+ }
+
+
+class MacBridgePortFilterPreAssignTable(EntityClass):
+ class_id = 79
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ShortField("ipv4_multicast", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("ipv6_multicast", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("ipv4_broadcast", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("rarp", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("ipx", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("netbeui", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("appletalk", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("bridge_management_information", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("arp", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("pppoe_broadcast", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1)
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+
+
+class VlanTaggingFilterData(EntityClass):
+ class_id = 84
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(FieldListField("vlan_filter_list", None,
+ ShortField('', 0), count_from=lambda _: 12),
+ {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("forward_operation", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0x00 <= x <= 0x21),
+ ECA(ByteField("number_of_entries", None), {AA.R, AA.W, AA.SBC})
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+
+
+class Ieee8021pMapperServiceProfile(EntityClass):
+ class_id = 130
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ShortField("tp_pointer", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("interwork_tp_pointer_for_p_bit_priority_0",
+ OmciNullPointer), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("interwork_tp_pointer_for_p_bit_priority_1",
+ OmciNullPointer), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("interwork_tp_pointer_for_p_bit_priority_2",
+ OmciNullPointer), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("interwork_tp_pointer_for_p_bit_priority_3",
+ OmciNullPointer), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("interwork_tp_pointer_for_p_bit_priority_4",
+ OmciNullPointer), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("interwork_tp_pointer_for_p_bit_priority_5",
+ OmciNullPointer), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("interwork_tp_pointer_for_p_bit_priority_6",
+ OmciNullPointer), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("interwork_tp_pointer_for_p_bit_priority_7",
+ OmciNullPointer), {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("unmarked_frame_option", None),
+ {AA.R, AA.W, AA.SBC}, range_check=lambda x: 0 <= x <= 1),
+ ECA(StrFixedLenField("dscp_to_p_bit_mapping", None, length=24),
+ {AA.R, AA.W}), # TODO: Would a custom 3-bit group bitfield work better?
+ ECA(ByteField("default_p_bit_marking", None),
+ {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("tp_type", None), {AA.R, AA.W, AA.SBC},
+ optional=True, range_check=lambda x: 0 <= x <= 8)
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+
+
+class OltG(EntityClass):
+ class_id = 131
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R},
+ range_check=lambda x: x == 0),
+ ECA(StrFixedLenField("olt_vendor_id", None, 4), {AA.R, AA.W}),
+ ECA(StrFixedLenField("equipment_id", None, 20), {AA.R, AA.W}),
+ ECA(StrFixedLenField("version", None, 14), {AA.R, AA.W}),
+ ECA(StrFixedLenField("time_of_day_information", None, 14), {AA.R, AA.W})
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+
+
+class OntPowerShedding(EntityClass):
+ class_id = 133
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R},
+ range_check=lambda x: x == 0),
+ ECA(ShortField("restore_power_timer_reset_interval", 0),
+ {AA.R, AA.W}),
+ ECA(ShortField("data_class_shedding_interval", 0), {AA.R, AA.W}),
+ ECA(ShortField("voice_class_shedding_interval", 0), {AA.R, AA.W}),
+ ECA(ShortField("video_overlay_class_shedding_interval", 0), {AA.R, AA.W}),
+ ECA(ShortField("video_return_class_shedding_interval", 0), {AA.R, AA.W}),
+ ECA(ShortField("dsl_class_shedding_interval", 0), {AA.R, AA.W}),
+ ECA(ShortField("atm_class_shedding_interval", 0), {AA.R, AA.W}),
+ ECA(ShortField("ces_class_shedding_interval", 0), {AA.R, AA.W}),
+ ECA(ShortField("frame_class_shedding_interval", 0), {AA.R, AA.W}),
+ ECA(ShortField("sonet_class_shedding_interval", 0), {AA.R, AA.W}),
+ ECA(ShortField("shedding_status", None), {AA.R, AA.W}, optional=True,
+ avc=True),
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+ notifications = {OP.AttributeValueChange}
+
+
+class IpHostConfigData(EntityClass):
+ class_id = 134
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R}),
+ ECA(BitField("ip_options", 0, size=8), {AA.R, AA.W}),
+ ECA(MACField("mac_address", None), {AA.R}),
+ ECA(StrFixedLenField("onu_identifier", None, 25), {AA.R, AA.W}),
+ ECA(IPField("ip_address", None), {AA.R, AA.W}),
+ ECA(IPField("mask", None), {AA.R, AA.W}),
+ ECA(IPField("gateway", None), {AA.R, AA.W}),
+ ECA(IPField("primary_dns", None), {AA.R, AA.W}),
+ ECA(IPField("secondary_dns", None), {AA.R, AA.W}),
+ ECA(IPField("current_address", None), {AA.R}, avc=True),
+ ECA(IPField("current_mask", None), {AA.R}, avc=True),
+ ECA(IPField("current_gateway", None), {AA.R}, avc=True),
+ ECA(IPField("current_primary_dns", None), {AA.R}, avc=True),
+ ECA(IPField("current_secondary_dns", None), {AA.R}, avc=True),
+ ECA(StrFixedLenField("domain_name", None, 25), {AA.R}, avc=True),
+ ECA(StrFixedLenField("host_name", None, 25), {AA.R}, avc=True),
+ ECA(ShortField("relay_agent_options", None), {AA.R, AA.W},
+ optional=True),
+ ]
+ mandatory_operations = {OP.Get, OP.Set, OP.Test}
+ notifications = {OP.AttributeValueChange}
+
+
+class VlanTaggingOperation(Packet):
+ name = "VlanTaggingOperation"
+ fields_desc = [
+ BitField("filter_outer_priority", 0, 4),
+ BitField("filter_outer_vid", 0, 13),
+ BitField("filter_outer_tpid_de", 0, 3),
+ BitField("pad1", 0, 12),
+
+ BitField("filter_inner_priority", 0, 4),
+ BitField("filter_inner_vid", 0, 13),
+ BitField("filter_inner_tpid_de", 0, 3),
+ BitField("pad2", 0, 8),
+ BitField("filter_ether_type", 0, 4),
+
+ BitField("treatment_tags_to_remove", 0, 2),
+ BitField("pad3", 0, 10),
+ BitField("treatment_outer_priority", 0, 4),
+ BitField("treatment_outer_vid", 0, 13),
+ BitField("treatment_outer_tpid_de", 0, 3),
+
+ BitField("pad4", 0, 12),
+ BitField("treatment_inner_priority", 0, 4),
+ BitField("treatment_inner_vid", 0, 13),
+ BitField("treatment_inner_tpid_de", 0, 3),
+ ]
+
+ def to_json(self):
+ return json.dumps(self.fields, separators=(',', ':'))
+
+ @staticmethod
+ def json_from_value(value):
+ bits = BitArray(hex=hexlify(value))
+ temp = VlanTaggingOperation(
+ filter_outer_priority=bits[0:4].uint, # 4 <-size
+ filter_outer_vid=bits[4:17].uint, # 13
+ filter_outer_tpid_de=bits[17:20].uint, # 3
+ # pad 12
+ filter_inner_priority=bits[32:36].uint, # 4
+ filter_inner_vid=bits[36:49].uint, # 13
+ filter_inner_tpid_de=bits[49:52].uint, # 3
+ # pad 8
+ filter_ether_type=bits[60:64].uint, # 4
+ treatment_tags_to_remove=bits[64:66].uint, # 2
+ # pad 10
+ treatment_outer_priority=bits[76:80].uint, # 4
+ treatment_outer_vid=bits[80:93].uint, # 13
+ treatment_outer_tpid_de=bits[93:96].uint, # 3
+ # pad 12
+ treatment_inner_priority=bits[108:112].uint, # 4
+ treatment_inner_vid=bits[112:125].uint, # 13
+ treatment_inner_tpid_de=bits[125:128].uint, # 3
+ )
+ return json.dumps(temp.fields, separators=(',', ':'))
+
+ def index(self):
+ return '{:02}'.format(self.fields.get('filter_outer_priority',0)) + \
+ '{:03}'.format(self.fields.get('filter_outer_vid',0)) + \
+ '{:01}'.format(self.fields.get('filter_outer_tpid_de',0)) + \
+ '{:03}'.format(self.fields.get('filter_inner_priority',0)) + \
+ '{:04}'.format(self.fields.get('filter_inner_vid',0)) + \
+ '{:01}'.format(self.fields.get('filter_inner_tpid_de',0)) + \
+ '{:02}'.format(self.fields.get('filter_ether_type',0))
+
+ def is_delete(self):
+ return self.fields.get('treatment_tags_to_remove',0) == 0x3 and \
+ self.fields.get('pad3',0) == 0x3ff and \
+ self.fields.get('treatment_outer_priority',0) == 0xf and \
+ self.fields.get('treatment_outer_vid',0) == 0x1fff and \
+ self.fields.get('treatment_outer_tpid_de',0) == 0x7 and \
+ self.fields.get('pad4',0) == 0xfff and \
+ self.fields.get('treatment_inner_priority',0) == 0xf and \
+ self.fields.get('treatment_inner_vid',0) == 0x1fff and \
+ self.fields.get('treatment_inner_tpid_de',0) == 0x7
+
+ def delete(self):
+ self.fields['treatment_tags_to_remove'] = 0x3
+ self.fields['pad3'] = 0x3ff
+ self.fields['treatment_outer_priority'] = 0xf
+ self.fields['treatment_outer_vid'] = 0x1fff
+ self.fields['treatment_outer_tpid_de'] = 0x7
+ self.fields['pad4'] = 0xfff
+ self.fields['treatment_inner_priority'] = 0xf
+ self.fields['treatment_inner_vid'] = 0x1fff
+ self.fields['treatment_inner_tpid_de'] = 0x7
+ return self
+
+
+class ExtendedVlanTaggingOperationConfigurationData(EntityClass):
+ class_id = 171
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("association_type", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0 <= x <= 11),
+ ECA(ShortField("received_vlan_tagging_operation_table_max_size", None),
+ {AA.R}),
+ ECA(ShortField("input_tpid", None), {AA.R, AA.W}),
+ ECA(ShortField("output_tpid", None), {AA.R, AA.W}),
+ ECA(ByteField("downstream_mode", None), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 8),
+ ECA(OmciTableField(
+ PacketLenField("received_frame_vlan_tagging_operation_table", None,
+ VlanTaggingOperation, length_from=lambda pkt: 16)), {AA.R, AA.W}),
+ ECA(ShortField("associated_me_pointer", None), {AA.R, AA.W, AA.SBC}),
+ ECA(FieldListField("dscp_to_p_bit_mapping", None,
+ BitField('', 0, size=3), count_from=lambda _: 64),
+ {AA.R, AA.W}),
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Set, OP.Get, OP.GetNext}
+ optional_operations = {OP.SetTable}
+
+
+class OntG(EntityClass):
+ class_id = 256
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R},
+ range_check=lambda x: x == 0),
+ ECA(StrFixedLenField("vendor_id", None, 4), {AA.R}),
+ ECA(StrFixedLenField("version", None, 14), {AA.R}),
+ ECA(OmciSerialNumberField("serial_number"), {AA.R}),
+ ECA(ByteField("traffic_management_option", None), {AA.R},
+ range_check=lambda x: 0 <= x <= 2),
+ ECA(ByteField("deprecated", 0), {AA.R},
+ optional=True, deprecated=True),
+ ECA(ByteField("battery_backup", None), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("administrative_state", None), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("operational_state", None), {AA.R}, optional=True,
+ range_check=lambda x: 0 <= x <= 1, avc=True),
+ ECA(ByteField("ont_survival_time", None), {AA.R}, optional=True),
+ ECA(StrFixedLenField("logical_onu_id", None, 24), {AA.R},
+ optional=True, avc=True),
+ ECA(StrFixedLenField("logical_password", None, 12), {AA.R},
+ optional=True, avc=True),
+ ECA(ByteField("credentials_status", None), {AA.R, AA.W},
+ optional=True, range_check=lambda x: 0 <= x <= 4),
+ ECA(BitField("extended_tc_layer_options", None, size=16), {AA.R},
+ optional=True),
+ ]
+ mandatory_operations = {
+ OP.Get, OP.Set, OP.Reboot, OP.Test, OP.SynchronizeTime}
+ notifications = {OP.TestResult, OP.AttributeValueChange,
+ OP.AlarmNotification}
+ alarms = {
+ 0: 'Equipment alarm',
+ 1: 'Powering alarm',
+ 2: 'Battery missing',
+ 3: 'Battery failure',
+ 4: 'Battery low',
+ 5: 'Physical intrusion',
+ 6: 'Self-test failure',
+ 7: 'Dying gasp',
+ 8: 'Temperature yellow',
+ 9: 'Temperature red',
+ 10: 'Voltage yellow',
+ 11: 'Voltage red',
+ 12: 'ONU manual power off',
+ 13: 'Invalid image',
+ 14: 'PSE overload yellow',
+ 15: 'PSE overload red',
+ }
+
+
+class Ont2G(EntityClass):
+ class_id = 257
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R},
+ range_check=lambda x: x == 0),
+ ECA(StrFixedLenField("equipment_id", None, 20), {AA.R}),
+ ECA(ByteField("omcc_version", None), {AA.R}, avc=True),
+ ECA(ShortField("vendor_product_code", None), {AA.R}),
+ ECA(ByteField("security_capability", None), {AA.R},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("security_mode", None), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("total_priority_queue_number", None), {AA.R}),
+ ECA(ByteField("total_traffic_scheduler_number", None), {AA.R}),
+ ECA(ByteField("deprecated", None), {AA.R}, deprecated=True),
+ ECA(ShortField("total_gem_port_id_number", None), {AA.R}),
+ ECA(IntField("sys_uptime", None), {AA.R}),
+ ECA(BitField("connectivity_capability", None, size=16), {AA.R}),
+ ECA(ByteField("current_connectivity_mode", None), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 7),
+ ECA(BitField("qos_configuration_flexibility", None, size=16),
+ {AA.R}, optional=True),
+ ECA(ShortField("priority_queue_scale_factor", None), {AA.R, AA.W},
+ optional=True),
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+ notifications = {OP.AttributeValueChange}
+
+
+class Tcont(EntityClass):
+ class_id = 262
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R}),
+ ECA(ShortField("alloc_id", None), {AA.R, AA.W}),
+ ECA(ByteField("deprecated", 1), {AA.R}, deprecated=True),
+ ECA(ByteField("policy", None), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 2),
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+
+
+class AniG(EntityClass):
+ class_id = 263
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R}),
+ ECA(ByteField("sr_indication", None), {AA.R}),
+ ECA(ShortField("total_tcont_number", None), {AA.R}),
+ ECA(ShortField("gem_block_length", None), {AA.R, AA.W}),
+ ECA(ByteField("piggyback_dba_reporting", None), {AA.R},
+ range_check=lambda x: 0 <= x <= 4),
+ ECA(ByteField("deprecated", None), {AA.R}, deprecated=True),
+ ECA(ByteField("sf_threshold", 5), {AA.R, AA.W}),
+ ECA(ByteField("sd_threshold", 9), {AA.R, AA.W}),
+ ECA(ByteField("arc", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1, avc=True),
+ ECA(ByteField("arc_interval", 0), {AA.R, AA.W}),
+ ECA(ShortField("optical_signal_level", None), {AA.R}),
+ ECA(ByteField("lower_optical_threshold", 0xFF), {AA.R, AA.W}),
+ ECA(ByteField("upper_optical_threshold", 0xFF), {AA.R, AA.W}),
+ ECA(ShortField("onu_response_time", None), {AA.R}),
+ ECA(ShortField("transmit_optical_level", None), {AA.R}),
+ ECA(ByteField("lower_transmit_power_threshold", 0x81), {AA.R, AA.W}),
+ ECA(ByteField("upper_transmit_power_threshold", 0x81), {AA.R, AA.W}),
+ ]
+ mandatory_operations = {OP.Get, OP.Set, OP.Test}
+ notifications = {OP.AttributeValueChange, OP.AlarmNotification}
+ alarms = {
+ 0: 'Low received optical power',
+ 1: 'High received optical power',
+ 2: 'Signal fail',
+ 3: 'Signal degrade',
+ 4: 'Low transmit optical power',
+ 5: 'High transmit optical power',
+ 6: 'Laser bias current',
+ }
+
+
+class UniG(EntityClass):
+ class_id = 264
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R}),
+ ECA(ShortField("deprecated", None), {AA.R, AA.W}, deprecated=True),
+ ECA(ByteField("administrative_state", None), {AA.R, AA.W}),
+ ECA(ByteField("management_capability", None), {AA.R},
+ range_check=lambda x: 0 <= x <= 2),
+ ECA(ShortField("non_omci_management_identifier", None), {AA.R, AA.W}),
+ ECA(ShortField("relay_agent_options", None), {AA.R, AA.W},
+ optional=True),
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+
+
+class GemInterworkingTp(EntityClass):
+ class_id = 266
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ShortField("gem_port_network_ctp_pointer", None),
+ {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("interworking_option", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0 <= x <= 7),
+ ECA(ShortField("service_profile_pointer", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("interworking_tp_pointer", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("pptp_counter", None), {AA.R}, optional=True),
+ ECA(ByteField("operational_state", None), {AA.R}, optional=True,
+ range_check=lambda x: 0 <= x <= 1, avc=True),
+ ECA(ShortField("gal_profile_pointer", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("gal_loopback_configuration", 0),
+ {AA.R, AA.W}, range_check=lambda x: 0 <= x <= 1),
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+ notifications = {OP.AttributeValueChange, OP.AlarmNotification}
+ alarms = {
+ 6: 'Operational state change',
+ }
+
+
+class GemPortNetworkCtp(EntityClass):
+ class_id = 268
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ShortField("port_id", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("tcont_pointer", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("direction", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 1 <= x <= 3),
+ ECA(ShortField("traffic_management_pointer_upstream", None),
+ {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("traffic_descriptor_profile_pointer", None),
+ {AA.R, AA.W, AA.SBC}, optional=True),
+ ECA(ByteField("uni_counter", None), {AA.R}, optional=True),
+ ECA(ShortField("priority_queue_pointer_downstream", None),
+ {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("encryption_state", None), {AA.R}, optional=True),
+ ECA(ShortField("traffic_desc_profile_pointer_downstream", None),
+ {AA.R, AA.W, AA.SBC}, optional=True),
+ ECA(ShortField("encryption_key_ring", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0 <= x <= 3)
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 5: 'End-to-end loss of continuity',
+ }
+
+
+class GalEthernetProfile(EntityClass):
+ class_id = 272
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ShortField("max_gem_payload_size", None), {AA.R, AA.W, AA.SBC}),
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+
+
+class PriorityQueueG(EntityClass):
+ class_id = 277
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R}),
+ ECA(ByteField("queue_configuration_option", None), {AA.R},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("maximum_queue_size", None), {AA.R}),
+ ECA(ShortField("allocated_queue_size", None), {AA.R, AA.W}),
+ ECA(ShortField("discard_block_counter_reset_interval", None), {AA.R, AA.W}),
+ ECA(ShortField("threshold_value_for_discarded_blocks", None), {AA.R, AA.W}),
+ ECA(IntField("related_port", None), {AA.R}),
+ ECA(ShortField("traffic_scheduler_pointer", 0), {AA.R, AA.W}),
+ ECA(ByteField("weight", 1), {AA.R, AA.W}),
+ ECA(ShortField("back_pressure_operation", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(IntField("back_pressure_time", 0), {AA.R, AA.W}),
+ ECA(ShortField("back_pressure_occur_queue_threshold", None), {AA.R, AA.W}),
+ ECA(ShortField("back_pressure_clear_queue_threshold", None), {AA.R, AA.W}),
+ # TODO: Custom field of 4 2-byte values would help below
+ ECA(LongField("packet_drop_queue_thresholds", None), {AA.R, AA.W},
+ optional=True),
+ ECA(ShortField("packet_drop_max_p", 0xFFFF), {AA.R, AA.W}, optional=True),
+ ECA(ByteField("queue_drop_w_q", 9), {AA.R, AA.W}, optional=True),
+ ECA(ByteField("drop_precedence_colour_marking", 0), {AA.R, AA.W},
+ optional=True, range_check=lambda x: 0 <= x <= 7),
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 0: 'Block loss',
+ }
+
+
+class TrafficSchedulerG(EntityClass):
+ class_id = 278
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R}),
+ ECA(ShortField("tcont_pointer", None), {AA.R}),
+ ECA(ShortField("traffic_scheduler_pointer", None), {AA.R}),
+ ECA(ByteField("policy", None), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 2),
+ ECA(ByteField("priority_weight", 0), {AA.R, AA.W}),
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+
+
+class MulticastGemInterworkingTp(EntityClass):
+ class_id = 281
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC},
+ range_check=lambda x: x != OmciNullPointer),
+ ECA(ShortField("gem_port_network_ctp_pointer", None), {AA.R, AA.SBC}),
+ ECA(ByteField("interworking_option", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: x in [0, 1, 3, 5]),
+ ECA(ShortField("service_profile_pointer", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("interworking_tp_pointer", 0), {AA.R, AA.W, AA.SBC},
+ deprecated=True),
+ ECA(ByteField("pptp_counter", None), {AA.R}),
+ ECA(ByteField("operational_state", None), {AA.R}, avc=True,
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("gal_profile_pointer", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("gal_loopback_configuration", None), {AA.R, AA.W, AA.SBC},
+ deprecated=True),
+ # TODO add multicast_address_table here (page 85 of spec.)
+ # ECA(...("multicast_address_table", None), {AA.R, AA.W})
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.GetNext, OP.Set}
+ optional_operations = {OP.SetTable}
+ notifications = {OP.AttributeValueChange, OP.AlarmNotification}
+ alarms = {
+ 0: 'Deprecated',
+ }
+
+
+class AccessControlRow0(Packet):
+ name = "AccessControlRow0"
+ fields_desc = [
+ BitField("set_ctrl", 0, 2),
+ BitField("row_part_id", 0, 3),
+ BitField("test", 0, 1),
+ BitField("row_key", 0, 10),
+
+ ShortField("gem_port_id", None),
+ ShortField("vlan_id", None),
+ IPField("src_ip", None),
+ IPField("dst_ip_start", None),
+ IPField("dst_ip_end", None),
+ IntField("ipm_group_bw", None),
+ ShortField("reserved0", 0)
+ ]
+
+ def to_json(self):
+ return json.dumps(self.fields, separators=(',', ':'))
+
+
+class AccessControlRow1(Packet):
+ name = "AccessControlRow1"
+ fields_desc = [
+ BitField("set_ctrl", 0, 2),
+ BitField("row_part_id", 0, 3),
+ BitField("test", 0, 1),
+ BitField("row_key", 0, 10),
+
+ StrFixedLenField("ipv6_src_addr_start_bytes", None, 12),
+ ShortField("preview_length", None),
+ ShortField("preview_repeat_time", None),
+ ShortField("preview_repeat_count", None),
+ ShortField("preview_reset_time", None),
+ ShortField("reserved1", 0)
+ ]
+
+ def to_json(self):
+ return json.dumps(self.fields, separators=(',', ':'))
+
+
+class AccessControlRow2(Packet):
+ name = "AccessControlRow2"
+ fields_desc = [
+ BitField("set_ctrl", 0, 2),
+ BitField("row_part_id", 0, 3),
+ BitField("test", 0, 1),
+ BitField("row_key", 0, 10),
+
+ StrFixedLenField("ipv6_dst_addr_start_bytes", None, 12),
+ StrFixedLenField("reserved2", None, 10)
+ ]
+
+ def to_json(self):
+ return json.dumps(self.fields, separators=(',', ':'))
+
+
+class DownstreamIgmpMulticastTci(Packet):
+ name = "DownstreamIgmpMulticastTci"
+ fields_desc = [
+ ByteField("ctrl_type", None),
+ ShortField("tci", None)
+ ]
+
+ def to_json(self):
+ return json.dumps(self.fields, separators=(',', ':'))
+
+
+class MulticastOperationsProfile(EntityClass):
+ class_id = 309
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC},
+ range_check=lambda x: x != 0 and x != OmciNullPointer),
+ ECA(ByteField("igmp_version", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: x in [1, 2, 3, 16, 17]),
+ ECA(ByteField("igmp_function", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0 <= x <= 2),
+ ECA(ByteField("immediate_leave", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("us_igmp_tci", None), {AA.R, AA.W, AA.SBC}, optional=True),
+ ECA(ByteField("us_igmp_tag_ctrl", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0 <= x <= 3, optional=True),
+ ECA(IntField("us_igmp_rate", None), {AA.R, AA.W, AA.SBC}, optional=True),
+ # TODO: need to make table and add column data
+ ECA(StrFixedLenField(
+ "dynamic_access_control_list_table", None, 24), {AA.R, AA.W}),
+ # TODO: need to make table and add column data
+ ECA(StrFixedLenField(
+ "static_access_control_list_table", None, 24), {AA.R, AA.W}),
+ # TODO: need to make table and add column data
+ ECA(StrFixedLenField("lost_groups_list_table", None, 10), {AA.R}),
+ ECA(ByteField("robustness", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("querier_ip", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("query_interval", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("querier_max_response_time", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("last_member_response_time", 10), {AA.R, AA.W}),
+ ECA(ByteField("unauthorized_join_behaviour", None), {AA.R, AA.W}),
+ ECA(StrFixedLenField("ds_igmp_mcast_tci", None, 3), {AA.R, AA.W, AA.SBC}, optional=True)
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Set, OP.Get, OP.GetNext}
+ optional_operations = {OP.SetTable}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 0: 'Lost multicast group',
+ }
+
+
+class MulticastServicePackage(Packet):
+ name = "MulticastServicePackage"
+ fields_desc = [
+ BitField("set_ctrl", 0, 2),
+ BitField("reserved0", 0, 4),
+ BitField("row_key", 0, 10),
+
+ ShortField("vid_uni", None),
+ ShortField("max_simultaneous_groups", None),
+ IntField("max_multicast_bw", None),
+ ShortField("mcast_operations_profile_pointer", None),
+ StrFixedLenField("reserved1", None, 8)
+ ]
+
+ def to_json(self):
+ return json.dumps(self.fields, separators=(',', ':'))
+
+
+class AllowedPreviewGroupsRow0(Packet):
+ name = "AllowedPreviewGroupsRow0"
+ fields_desc = [
+ BitField("set_ctrl", 0, 2),
+ BitField("row_part_id", 0, 3),
+ BitField("reserved0", 0, 1),
+ BitField("row_key", 0, 10),
+
+ StrFixedLenField("ipv6_pad", 0, 12),
+ IPField("src_ip", None),
+ ShortField("vlan_id_ani", None),
+ ShortField("vlan_id_uni", None)
+ ]
+
+ def to_json(self):
+ return json.dumps(self.fields, separators=(',', ':'))
+
+
+class AllowedPreviewGroupsRow1(Packet):
+ name = "AllowedPreviewGroupsRow1"
+ fields_desc = [
+ BitField("set_ctrl", 0, 2),
+ BitField("row_part_id", 0, 3),
+ BitField("reserved0", 0, 1),
+ BitField("row_key", 0, 10),
+
+ StrFixedLenField("ipv6_pad", 0, 12),
+ IPField("dst_ip", None),
+ ShortField("duration", None),
+ ShortField("time_left", None)
+ ]
+
+ def to_json(self):
+ return json.dumps(self.fields, separators=(',', ':'))
+
+
+class MulticastSubscriberConfigInfo(EntityClass):
+ class_id = 310
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("me_type", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ShortField("mcast_operations_profile_pointer", None),
+ {AA.R, AA.W, AA.SBC}),
+ ECA(ShortField("max_simultaneous_groups", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("max_multicast_bandwidth", None), {AA.R, AA.W, AA.SBC}),
+ ECA(ByteField("bandwidth_enforcement", None), {AA.R, AA.W, AA.SBC},
+ range_check=lambda x: 0 <= x <= 1),
+ # TODO: need to make table and add column data
+ ECA(StrFixedLenField(
+ "multicast_service_package_table", None, 20), {AA.R, AA.W}),
+ # TODO: need to make table and add column data
+ ECA(StrFixedLenField(
+ "allowed_preview_groups_table", None, 22), {AA.R, AA.W}),
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Set, OP.Get, OP.GetNext,
+ OP.SetTable}
+
+
+class VirtualEthernetInterfacePt(EntityClass):
+ class_id = 329
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R},
+ range_check=lambda x: x != 0 and x != OmciNullPointer),
+ ECA(ByteField("administrative_state", None), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("operational_state", None), {AA.R}, avc=True,
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(StrFixedLenField(
+ "interdomain_name", None, 25), {AA.R, AA.W}, optional=True),
+ ECA(ShortField("tcp_udp_pointer", None), {AA.R, AA.W}, optional=True),
+ ECA(ShortField("iana_assigned_port", None), {AA.R}),
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+ notifications = {OP.AttributeValueChange, OP.AlarmNotification}
+ alarms = {
+ 0: 'Connecting function fail',
+ }
+
+
+class Omci(EntityClass):
+ class_id = 287
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R},
+ range_check=lambda x: x == 0),
+
+ # TODO: Can this be expressed better in SCAPY, probably not?
+ # On the initial, Get request for either the me_type or message_type
+ # attributes, you will receive a 4 octet value (big endian) that is
+ # the number of octets to 'get-next' to fully load the desired
+ # attribute. For a basic OMCI formatted message, that will be 29
+ # octets per get-request.
+ #
+ # For the me_type_table, these are 16-bit values (ME Class IDs)
+ #
+ # For the message_type_table, these are 8-bit values (Actions)
+
+ ECA(FieldListField("me_type_table", None, ByteField('', 0),
+ count_from=lambda _: 29), {AA.R}),
+ ECA(FieldListField("message_type_table", None, ByteField('', 0),
+ count_from=lambda _: 29), {AA.R}),
+ ]
+ mandatory_operations = {OP.Get, OP.GetNext}
+
+
+class EnhSecurityControl(EntityClass):
+ class_id = 332
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R}),
+ ECA(BitField("olt_crypto_capabilities", None, 16*8), {AA.W}),
+ # TODO: need to make table and add column data
+ ECA(StrFixedLenField(
+ "olt_random_challenge_table", None, 17), {AA.R, AA.W}),
+ ECA(ByteField("olt_challenge_status", 0), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("onu_selected_crypto_capabilities", None), {AA.R}),
+ # TODO: need to make table and add column data
+ ECA(StrFixedLenField(
+ "onu_random_challenge_table", None, 16), {AA.R}, avc=True),
+ # TODO: need to make table and add column data
+ ECA(StrFixedLenField(
+ "onu_authentication_result_table", None, 16), {AA.R}, avc=True),
+ # TODO: need to make table and add column data
+ ECA(StrFixedLenField(
+ "olt_authentication_result_table", None, 17), {AA.W}),
+ ECA(ByteField("olt_result_status", None), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("onu_authentication_status", None), {AA.R}, avc=True,
+ range_check=lambda x: 0 <= x <= 5),
+ ECA(StrFixedLenField(
+ "master_session_key_name", None, 16), {AA.R}),
+ ECA(StrFixedLenField(
+ "broadcast_key_table", None, 18), {AA.R, AA.W}),
+ ECA(ShortField("effective_key_length", None), {AA.R}),
+
+ ]
+ mandatory_operations = {OP.Set, OP.Get, OP.GetNext}
+ notifications = {OP.AttributeValueChange}
+
+
+class EthernetPMMonitoringHistoryData(EntityClass):
+ class_id = 24
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("interval_end_time", None), {AA.R}),
+ ECA(ShortField("threshold_data_1_2_id", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("fcs_errors", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("excessive_collision_counter", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("late_collision_counter", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("frames_too_long", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("buffer_overflows_on_rx", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("buffer_overflows_on_tx", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("single_collision_frame_counter", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("multiple_collisions_frame_counter", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("sqe_counter", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("deferred_tx_counter", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("internal_mac_tx_error_counter", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("carrier_sense_error_counter", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("alignment_error_counter", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("internal_mac_rx_error_counter", None), {AA.R}, tca=True, counter=True)
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set, OP.GetCurrentData}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 0: 'FCS errors',
+ 1: 'Excessive collision counter',
+ 2: 'Late collision counter',
+ 3: 'Frames too long',
+ 4: 'Buffer overflows on receive',
+ 5: 'Buffer overflows on transmit',
+ 6: 'Single collision frame counter',
+ 7: 'Multiple collision frame counter',
+ 8: 'SQE counter',
+ 9: 'Deferred transmission counter',
+ 10: 'Internal MAC transmit error counter',
+ 11: 'Carrier sense error counter',
+ 12: 'Alignment error counter',
+ 13: 'Internal MAC receive error counter',
+ }
+
+
+class FecPerformanceMonitoringHistoryData(EntityClass):
+ class_id = 312
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("interval_end_time", None), {AA.R}),
+ ECA(ShortField("threshold_data_1_2_id", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("corrected_bytes", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("corrected_code_words", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("uncorrectable_code_words", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("total_code_words", None), {AA.R}, counter=True),
+ ECA(ShortField("fec_seconds", None), {AA.R}, tca=True, counter=True)
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set, OP.GetCurrentData}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 0: 'Corrected bytes',
+ 1: 'Corrected code words',
+ 2: 'Uncorrectable code words',
+ 4: 'FEC seconds',
+ }
+
+
+class EthernetFrameDownstreamPerformanceMonitoringHistoryData(EntityClass):
+ class_id = 321
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("interval_end_time", None), {AA.R}),
+ ECA(ShortField("threshold_data_1_2_id", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("drop_events", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("octets", None), {AA.R}, counter=True),
+ ECA(IntField("packets", None), {AA.R}, counter=True),
+ ECA(IntField("broadcast_packets", None), {AA.R}, counter=True),
+ ECA(IntField("multicast_packets", None), {AA.R}, counter=True),
+ ECA(IntField("crc_errored_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("undersize_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("oversize_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("64_octets", None), {AA.R}, counter=True),
+ ECA(IntField("65_to_127_octets", None), {AA.R}, counter=True),
+ ECA(IntField("128_to_255_octets", None), {AA.R}, counter=True),
+ ECA(IntField("256_to_511_octets", None), {AA.R}, counter=True),
+ ECA(IntField("512_to_1023_octets", None), {AA.R}, counter=True),
+ ECA(IntField("1024_to_1518_octets", None), {AA.R}, counter=True)
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set, OP.GetCurrentData}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 0: 'Drop events',
+ 1: 'CRC errored packets',
+ 2: 'Undersize packets',
+ 3: 'Oversize packets',
+ }
+
+
+class EthernetFrameUpstreamPerformanceMonitoringHistoryData(EntityClass):
+ class_id = 322
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("interval_end_time", None), {AA.R}),
+ ECA(ShortField("threshold_data_1_2_id", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("drop_events", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("octets", None), {AA.R}, counter=True),
+ ECA(IntField("packets", None), {AA.R}, counter=True),
+ ECA(IntField("broadcast_packets", None), {AA.R}, counter=True),
+ ECA(IntField("multicast_packets", None), {AA.R}, counter=True),
+ ECA(IntField("crc_errored_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("undersize_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("oversize_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("64_octets", None), {AA.R}, counter=True),
+ ECA(IntField("65_to_127_octets", None), {AA.R}, counter=True),
+ ECA(IntField("128_to_255_octets", None), {AA.R}, counter=True),
+ ECA(IntField("256_to_511_octets", None), {AA.R}, counter=True),
+ ECA(IntField("512_to_1023_octets", None), {AA.R}, counter=True),
+ ECA(IntField("1024_to_1518_octets", None), {AA.R}, counter=True)
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set, OP.GetCurrentData}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 0: 'Drop events',
+ 1: 'CRC errored packets',
+ 2: 'Undersize packets',
+ 3: 'Oversize packets',
+ }
+
+
+class VeipUni(EntityClass):
+ class_id = 329
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R}),
+ ECA(ByteField("administrative_state", 1), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1),
+ ECA(ByteField("operational_state", 1), {AA.R, AA.W},
+ range_check=lambda x: 0 <= x <= 1, optional=True, avc=True),
+ ECA(StrFixedLenField("interdomain_name", None, 25), {AA.R, AA.W},
+ optional=True),
+ ECA(ShortField("tcp_udp_pointer", None), {AA.R, AA.W}, optional=True),
+ ECA(ShortField("iana_assigned_port", 0xFFFF), {AA.R})
+ ]
+ mandatory_operations = {OP.Get, OP.Set}
+ notifications = {OP.AttributeValueChange, OP.AlarmNotification}
+ alarms = {
+ 0: 'Connecting function fail'
+ }
+
+
+class EthernetFrameExtendedPerformanceMonitoring(EntityClass):
+ class_id = 334
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("interval_end_time", None), {AA.R}),
+ # 2-octet field -> Threshold data 1/2 ID
+ # 2-octet field -> Parent ME Class
+ # 2-octet field -> Parent ME Instance
+ # 2-octet field -> Accumulation disable
+ # 2-octet field -> TCA Disable
+ # 2-octet field -> Control fields bitmap
+ # 2-octet field -> TCI
+ # 2-octet field -> Reserved
+ ECA(FieldListField("control_block", None, ShortField('', 0),
+ count_from=lambda _: 8), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("drop_events", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("octets", None), {AA.R}, counter=True),
+ ECA(IntField("packets", None), {AA.R}, counter=True),
+ ECA(IntField("broadcast_packets", None), {AA.R}, counter=True),
+ ECA(IntField("multicast_packets", None), {AA.R}, counter=True),
+ ECA(IntField("crc_errored_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("undersize_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("oversize_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("64_octets", None), {AA.R}, counter=True),
+ ECA(IntField("65_to_127_octets", None), {AA.R}, counter=True),
+ ECA(IntField("128_to_255_octets", None), {AA.R}, counter=True),
+ ECA(IntField("256_to_511_octets", None), {AA.R}, counter=True),
+ ECA(IntField("512_to_1023_octets", None), {AA.R}, counter=True),
+ ECA(IntField("1024_to_1518_octets", None), {AA.R}, counter=True)
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+ optional_operations = {OP.GetCurrentData}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 0: 'Drop events',
+ 1: 'CRC errored packets',
+ 2: 'Undersize packets',
+ 3: 'Oversize packets',
+ }
+
+
+class EthernetFrameExtendedPerformanceMonitoring64Bit(EntityClass):
+ class_id = 426
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("interval_end_time", None), {AA.R}),
+ # 2-octet field -> Threshold data 1/2 ID
+ # 2-octet field -> Parent ME Class
+ # 2-octet field -> Parent ME Instance
+ # 2-octet field -> Accumulation disable
+ # 2-octet field -> TCA Disable
+ # 2-octet field -> Control fields bitmap
+ # 2-octet field -> TCI
+ # 2-octet field -> Reserved
+ ECA(FieldListField("control_block", None, ShortField('', 0),
+ count_from=lambda _: 8), {AA.R, AA.W, AA.SBC}),
+ ECA(LongField("drop_events", None), {AA.R}, tca=True, counter=True),
+ ECA(LongField("octets", None), {AA.R}, counter=True),
+ ECA(LongField("packets", None), {AA.R}, counter=True),
+ ECA(LongField("broadcast_packets", None), {AA.R}, counter=True),
+ ECA(LongField("multicast_packets", None), {AA.R}, counter=True),
+ ECA(LongField("crc_errored_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(LongField("undersize_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(LongField("oversize_packets", None), {AA.R}, tca=True, counter=True),
+ ECA(LongField("64_octets", None), {AA.R}, counter=True),
+ ECA(LongField("65_to_127_octets", None), {AA.R}, counter=True),
+ ECA(LongField("128_to_255_octets", None), {AA.R}, counter=True),
+ ECA(LongField("256_to_511_octets", None), {AA.R}, counter=True),
+ ECA(LongField("512_to_1023_octets", None), {AA.R}, counter=True),
+ ECA(LongField("1024_to_1518_octets", None), {AA.R}, counter=True)
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+ optional_operations = {OP.GetCurrentData}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 0: 'Drop events',
+ 1: 'CRC errored packets',
+ 2: 'Undersize packets',
+ 3: 'Oversize packets',
+ }
+
+
+class GemPortNetworkCtpMonitoringHistoryData(EntityClass):
+ class_id = 341
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("interval_end_time", None), {AA.R}),
+ ECA(ShortField("threshold_data_1_2_id", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("transmitted_gem_frames", None), {AA.R}, counter=True),
+ ECA(IntField("received_gem_frames", None), {AA.R}, counter=True),
+ ECA(LongField("received_payload_bytes", None), {AA.R}, counter=True),
+ ECA(LongField("transmitted_payload_bytes", None), {AA.R}, counter=True),
+ ECA(IntField("encryption_key_errors", None), {AA.R}, tca=True, counter=True)
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set, OP.GetCurrentData}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 1: 'Encryption key errors',
+ }
+
+
+class XgPonTcPerformanceMonitoringHistoryData(EntityClass):
+ class_id = 344
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("interval_end_time", None), {AA.R}),
+ ECA(ShortField("threshold_data_1_2_id", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("psbd_hec_error_count", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("xgtc_hec_error_count", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("unknown_profile_count", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("transmitted_xgem_frames", None), {AA.R}, counter=True),
+ ECA(IntField("fragment_xgem_frames", None), {AA.R}, counter=True),
+ ECA(IntField("xgem_hec_lost_words_count", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("xgem_key_errors", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("xgem_hec_error_count", None), {AA.R}, tca=True, counter=True)
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+ optional_operations = {OP.GetCurrentData}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 1: 'PSBd HEC error count',
+ 2: 'XGTC HEC error count',
+ 3: 'Unknown profile count',
+ 4: 'XGEM HEC loss count',
+ 5: 'XGEM key errors',
+ 6: 'XGEM HEC error count',
+ }
+
+
+class XgPonDownstreamPerformanceMonitoringHistoryData(EntityClass):
+ class_id = 345
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("interval_end_time", None), {AA.R},),
+ ECA(ShortField("threshold_data_1_2_id", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("ploam_mic_error_count", None), {AA.R}, tca=True, counter=True),
+ ECA(IntField("downstream_ploam_messages_count", None), {AA.R}, counter=True),
+ ECA(IntField("profile_messages_received", None), {AA.R}, counter=True),
+ ECA(IntField("ranging_time_messages_received", None), {AA.R}, counter=True),
+ ECA(IntField("deactivate_onu_id_messages_received", None), {AA.R}, counter=True),
+ ECA(IntField("disable_serial_number_messages_received", None), {AA.R}, counter=True),
+ ECA(IntField("request_registration_messages_received", None), {AA.R}, counter=True),
+ ECA(IntField("assign_alloc_id_messages_received", None), {AA.R}, counter=True),
+ ECA(IntField("key_control_messages_received", None), {AA.R}, counter=True),
+ ECA(IntField("sleep_allow_messages_received", None), {AA.R}, counter=True),
+ ECA(IntField("baseline_omci_messages_received_count", None), {AA.R}, counter=True),
+ ECA(IntField("extended_omci_messages_received_count", None), {AA.R}, counter=True),
+ ECA(IntField("assign_onu_id_messages_received", None), {AA.R}, counter=True),
+ ECA(IntField("omci_mic_error_count", None), {AA.R}, tca=True, counter=True),
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+ optional_operations = {OP.GetCurrentData}
+ notifications = {OP.AlarmNotification}
+ alarms = {
+ 1: 'PLOAM MIC error count',
+ 2: 'OMCI MIC error count',
+ }
+
+
+class XgPonUpstreamPerformanceMonitoringHistoryData(EntityClass):
+ class_id = 346
+ hidden = True
+ attributes = [
+ ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
+ ECA(ByteField("interval_end_time", None), {AA.R}),
+ ECA(ShortField("threshold_data_1_2_id", None), {AA.R, AA.W, AA.SBC}),
+ ECA(IntField("upstream_ploam_message_count", None), {AA.R}, counter=True),
+ ECA(IntField("serial_number_onu_message_count", None), {AA.R}, counter=True),
+ ECA(IntField("registration_message_count", None), {AA.R}, counter=True),
+ ECA(IntField("key_report_message_count", None), {AA.R}, counter=True),
+ ECA(IntField("acknowledge_message_count", None), {AA.R}, counter=True),
+ ECA(IntField("sleep_request_message_count", None), {AA.R}, counter=True),
+ ]
+ mandatory_operations = {OP.Create, OP.Delete, OP.Get, OP.Set}
+ optional_operations = {OP.GetCurrentData}
+
+
+# entity class lookup table from entity_class values
+entity_classes_name_map = dict(
+ inspect.getmembers(sys.modules[__name__],
+ lambda o: inspect.isclass(o) and \
+ issubclass(o, EntityClass) and \
+ o is not EntityClass)
+)
+
+entity_classes = [c for c in entity_classes_name_map.itervalues()]
+entity_id_to_class_map = dict((c.class_id, c) for c in entity_classes)
diff --git a/python/adapters/extensions/omci/omci_fields.py b/python/adapters/extensions/omci/omci_fields.py
new file mode 100644
index 0000000..56e985b
--- /dev/null
+++ b/python/adapters/extensions/omci/omci_fields.py
@@ -0,0 +1,242 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import binascii
+import json
+from scapy.fields import Field, StrFixedLenField, PadField, IntField, FieldListField, ByteField, StrField, \
+ StrFixedLenField, PacketField
+from scapy.packet import Raw
+
+class FixedLenField(PadField):
+ """
+ This Pad field limits parsing of its content to its size
+ """
+ def __init__(self, fld, align, padwith='\x00'):
+ super(FixedLenField, self).__init__(fld, align, padwith)
+
+ def getfield(self, pkt, s):
+ remain, val = self._fld.getfield(pkt, s[:self._align])
+ if isinstance(val.payload, Raw) and \
+ not val.payload.load.replace(self._padwith, ''):
+ # raw payload is just padding
+ val.remove_payload()
+ return remain + s[self._align:], val
+
+
+class StrCompoundField(Field):
+ __slots__ = ['flds']
+
+ def __init__(self, name, flds):
+ super(StrCompoundField, self).__init__(name=name, default=None, fmt='s')
+ self.flds = flds
+ for fld in self.flds:
+ assert not fld.holds_packets, 'compound field cannot have packet field members'
+
+ def addfield(self, pkt, s, val):
+ for fld in self.flds:
+ # run though fake add/get to consume the relevant portion of the input value for this field
+ x, extracted = fld.getfield(pkt, fld.addfield(pkt, '', val))
+ l = len(extracted)
+ s = fld.addfield(pkt, s, val[0:l])
+ val = val[l:]
+ return s;
+
+ def getfield(self, pkt, s):
+ data = ''
+ for fld in self.flds:
+ s, value = fld.getfield(pkt, s)
+ if not isinstance(value, str):
+ value = fld.i2repr(pkt, value)
+ data += value
+ return s, data
+
+
+class XStrFixedLenField(StrFixedLenField):
+ """
+ XStrFixedLenField which value is printed as hexadecimal.
+ """
+ def i2m(self, pkt, x):
+ l = self.length_from(pkt) * 2
+ return None if x is None else binascii.a2b_hex(x)[0:l+1]
+
+ def m2i(self, pkt, x):
+ return None if x is None else binascii.b2a_hex(x)
+
+
+class MultipleTypeField(object):
+ """MultipleTypeField are used for fields that can be implemented by
+ various Field subclasses, depending on conditions on the packet.
+
+ It is initialized with `flds` and `default`.
+
+ `default` is the default field type, to be used when none of the
+ conditions matched the current packet.
+
+ `flds` is a list of tuples (`fld`, `cond`), where `fld` if a field
+ type, and `cond` a "condition" to determine if `fld` is the field type
+ that should be used.
+
+ `cond` is either:
+
+ - a callable `cond_pkt` that accepts one argument (the packet) and
+ returns True if `fld` should be used, False otherwise.
+
+ - a tuple (`cond_pkt`, `cond_pkt_val`), where `cond_pkt` is the same
+ as in the previous case and `cond_pkt_val` is a callable that
+ accepts two arguments (the packet, and the value to be set) and
+ returns True if `fld` should be used, False otherwise.
+
+ See scapy.layers.l2.ARP (type "help(ARP)" in Scapy) for an example of
+ use.
+ """
+
+ __slots__ = ["flds", "default", "name"]
+
+ def __init__(self, flds, default):
+ self.flds = flds
+ self.default = default
+ self.name = self.default.name
+
+ def _find_fld_pkt(self, pkt):
+ """Given a Packet instance `pkt`, returns the Field subclass to be
+ used. If you know the value to be set (e.g., in .addfield()), use
+ ._find_fld_pkt_val() instead.
+ """
+ for fld, cond in self.flds:
+ if isinstance(cond, tuple):
+ cond = cond[0]
+ if cond(pkt):
+ return fld
+ return self.default
+
+ def _find_fld_pkt_val(self, pkt, val):
+ """Given a Packet instance `pkt` and the value `val` to be set,
+ returns the Field subclass to be used.
+ """
+ for fld, cond in self.flds:
+ if isinstance(cond, tuple):
+ if cond[1](pkt, val):
+ return fld
+ elif cond(pkt):
+ return fld
+ return self.default
+
+ def getfield(self, pkt, s):
+ return self._find_fld_pkt(pkt).getfield(pkt, s)
+
+ def addfield(self, pkt, s, val):
+ return self._find_fld_pkt_val(pkt, val).addfield(pkt, s, val)
+
+ def any2i(self, pkt, val):
+ return self._find_fld_pkt_val(pkt, val).any2i(pkt, val)
+
+ def h2i(self, pkt, val):
+ return self._find_fld_pkt_val(pkt, val).h2i(pkt, val)
+
+ def i2h(self, pkt, val):
+ return self._find_fld_pkt_val(pkt, val).i2h(pkt, val)
+
+ def i2m(self, pkt, val):
+ return self._find_fld_pkt_val(pkt, val).i2m(pkt, val)
+
+ def i2len(self, pkt, val):
+ return self._find_fld_pkt_val(pkt, val).i2len(pkt, val)
+
+ def i2repr(self, pkt, val):
+ return self._find_fld_pkt_val(pkt, val).i2repr(pkt, val)
+
+ def register_owner(self, cls):
+ for fld, _ in self.flds:
+ fld.owners.append(cls)
+ self.dflt.owners.append(cls)
+
+ def __getattr__(self, attr):
+ return getattr(self._find_fld(), attr)
+
+class OmciSerialNumberField(StrCompoundField):
+ def __init__(self, name, default=None):
+ assert default is None or (isinstance(default, str) and len(default) == 12), 'invalid default serial number'
+ vendor_default = default[0:4] if default is not None else None
+ vendor_serial_default = default[4:12] if default is not None else None
+ super(OmciSerialNumberField, self).__init__(name,
+ [StrFixedLenField('vendor_id', vendor_default, 4),
+ XStrFixedLenField('vendor_serial_number', vendor_serial_default, 4)])
+
+class OmciTableField(MultipleTypeField):
+ def __init__(self, tblfld):
+ assert isinstance(tblfld, PacketField)
+ assert hasattr(tblfld.cls, 'index'), 'No index() method defined for OmciTableField row object'
+ assert hasattr(tblfld.cls, 'is_delete'), 'No delete() method defined for OmciTableField row object'
+ super(OmciTableField, self).__init__(
+ [
+ (IntField('table_length', 0), (self.cond_pkt, self.cond_pkt_val)),
+ (PadField(StrField('me_type_table', None), OmciTableField.PDU_SIZE),
+ (self.cond_pkt2, self.cond_pkt_val2))
+ ], tblfld)
+
+ PDU_SIZE = 29 # Baseline message set raw get-next PDU size
+ OmciGetResponseMessageId = 0x29 # Ugh circular dependency
+ OmciGetNextResponseMessageId = 0x3a # Ugh circular dependency
+
+ def cond_pkt(self, pkt):
+ return pkt is not None and pkt.message_id == self.OmciGetResponseMessageId
+
+ def cond_pkt_val(self, pkt, val):
+ return pkt is not None and pkt.message_id == self.OmciGetResponseMessageId
+
+ def cond_pkt2(self, pkt):
+ return pkt is not None and pkt.message_id == self.OmciGetNextResponseMessageId
+
+ def cond_pkt_val2(self, pkt, val):
+ return pkt is not None and pkt.message_id == self.OmciGetNextResponseMessageId
+
+ def to_json(self, new_values, old_values_json):
+ if not isinstance(new_values, list): new_values = [new_values] # If setting a scalar, augment the old table
+ else: old_values_json = None # If setting a vector of new values, erase all old_values
+
+ key_value_pairs = dict()
+
+ old_table = self.load_json(old_values_json)
+ for old in old_table:
+ index = old.index()
+ key_value_pairs[index] = old
+ for new in new_values:
+ index = new.index()
+ if new.is_delete():
+ del key_value_pairs[index]
+ else:
+ key_value_pairs[index] = new
+
+ new_table = []
+ for k, v in sorted(key_value_pairs.iteritems()):
+ assert isinstance(v, self.default.cls), 'object type for Omci Table row object invalid'
+ new_table.append(v.fields)
+
+ str_values = json.dumps(new_table, separators=(',', ':'))
+
+ return str_values
+
+ def load_json(self, json_str):
+ if json_str is None: json_str = '[]'
+ json_values = json.loads(json_str)
+ key_value_pairs = dict()
+ for json_value in json_values:
+ v = self.default.cls(**json_value)
+ index = v.index()
+ key_value_pairs[index] = v
+ table = []
+ for k, v in sorted(key_value_pairs.iteritems()):
+ table.append(v)
+ return table
\ No newline at end of file
diff --git a/python/adapters/extensions/omci/omci_frame.py b/python/adapters/extensions/omci/omci_frame.py
new file mode 100644
index 0000000..c0d7d4a
--- /dev/null
+++ b/python/adapters/extensions/omci/omci_frame.py
@@ -0,0 +1,207 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from scapy.fields import ByteField, PacketField, IntField
+from scapy.fields import ShortField, ConditionalField
+from scapy.packet import Packet
+
+from voltha.extensions.omci.omci_fields import FixedLenField
+from voltha.extensions.omci.omci_messages import OmciCreate, OmciDelete, \
+ OmciDeleteResponse, OmciSet, OmciSetResponse, OmciGet, OmciGetResponse, \
+ OmciGetAllAlarms, OmciGetAllAlarmsResponse, OmciGetAllAlarmsNext, \
+ OmciMibResetResponse, OmciMibReset, OmciMibUploadNextResponse, \
+ OmciMibUploadNext, OmciMibUploadResponse, OmciMibUpload, \
+ OmciGetAllAlarmsNextResponse, OmciAttributeValueChange, \
+ OmciTestResult, OmciAlarmNotification, \
+ OmciReboot, OmciRebootResponse, OmciGetNext, OmciGetNextResponse, \
+ OmciSynchronizeTime, OmciSynchronizeTimeResponse, OmciGetCurrentData, \
+ OmciGetCurrentDataResponse, OmciStartSoftwareDownload, OmciStartSoftwareDownloadResponse, \
+ OmciDownloadSection, OmciDownloadSectionLast, OmciDownloadSectionResponse, \
+ OmciEndSoftwareDownload, OmciEndSoftwareDownloadResponse, \
+ OmciActivateImage, OmciActivateImageResponse, \
+ OmciCommitImage, OmciCommitImageResponse
+
+from voltha.extensions.omci.omci_messages import OmciCreateResponse
+
+
+class OmciFrame(Packet):
+ name = "OmciFrame"
+ fields_desc = [
+ ShortField("transaction_id", 0),
+ ByteField("message_type", None),
+ ByteField("omci", 0x0a),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciCreate), align=36),
+ lambda pkt: pkt.message_type == OmciCreate.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciCreateResponse), align=36),
+ lambda pkt: pkt.message_type == OmciCreateResponse.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciDelete), align=36),
+ lambda pkt: pkt.message_type == OmciDelete.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciDeleteResponse), align=36),
+ lambda pkt: pkt.message_type == OmciDeleteResponse.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciSet), align=36),
+ lambda pkt: pkt.message_type == OmciSet.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciSetResponse), align=36),
+ lambda pkt: pkt.message_type == OmciSetResponse.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciGet), align=36),
+ lambda pkt: pkt.message_type == OmciGet.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciGetResponse), align=36),
+ lambda pkt: pkt.message_type == OmciGetResponse.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciGetAllAlarms), align=36),
+ lambda pkt: pkt.message_type == OmciGetAllAlarms.message_id),
+ ConditionalField(FixedLenField(
+ PacketField(
+ "omci_message", None, OmciGetAllAlarmsResponse), align=36),
+ lambda pkt:
+ pkt.message_type == OmciGetAllAlarmsResponse.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciGetAllAlarmsNext), align=36),
+ lambda pkt: pkt.message_type == OmciGetAllAlarmsNext.message_id),
+ ConditionalField(FixedLenField(
+ PacketField(
+ "omci_message", None, OmciGetAllAlarmsNextResponse), align=36),
+ lambda pkt:
+ pkt.message_type == OmciGetAllAlarmsNextResponse.message_id),
+
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciMibUpload), align=36),
+ lambda pkt: pkt.message_type == OmciMibUpload.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciMibUploadResponse), align=36),
+ lambda pkt: pkt.message_type == OmciMibUploadResponse.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciMibUploadNext), align=36),
+ lambda pkt:
+ pkt.message_type == OmciMibUploadNext.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciMibUploadNextResponse), align=36),
+ lambda pkt: pkt.message_type == OmciMibUploadNextResponse.message_id),
+
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciMibReset), align=36),
+ lambda pkt: pkt.message_type == OmciMibReset.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciMibResetResponse), align=36),
+ lambda pkt: pkt.message_type == OmciMibResetResponse.message_id),
+
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciAlarmNotification), align=36),
+ lambda pkt: pkt.message_type == OmciAlarmNotification.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciAttributeValueChange), align=36),
+ lambda pkt: pkt.message_type == OmciAttributeValueChange.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciTestResult), align=36),
+ lambda pkt: pkt.message_type == OmciTestResult.message_id),
+
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciReboot), align=36),
+ lambda pkt: pkt.message_type == OmciReboot.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciRebootResponse), align=36),
+ lambda pkt: pkt.message_type == OmciRebootResponse.message_id),
+
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciGetNext), align=36),
+ lambda pkt: pkt.message_type == OmciGetNext.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciGetNextResponse), align=36),
+ lambda pkt: pkt.message_type == OmciGetNextResponse.message_id),
+
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciSynchronizeTime), align=36),
+ lambda pkt: pkt.message_type == OmciSynchronizeTime.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciSynchronizeTimeResponse), align=36),
+ lambda pkt: pkt.message_type == OmciSynchronizeTimeResponse.message_id),
+
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciGetCurrentData), align=36),
+ lambda pkt: pkt.message_type == OmciGetCurrentData.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciGetCurrentDataResponse), align=36),
+ lambda pkt: pkt.message_type == OmciGetCurrentDataResponse.message_id),
+
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciStartSoftwareDownload), align=36),
+ lambda pkt: pkt.message_type == OmciStartSoftwareDownload.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciStartSoftwareDownloadResponse), align=36),
+ lambda pkt: pkt.message_type == OmciStartSoftwareDownloadResponse.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciDownloadSection), align=36),
+ lambda pkt: pkt.message_type == OmciDownloadSection.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciDownloadSectionLast), align=36),
+ lambda pkt: pkt.message_type == OmciDownloadSectionLast.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciDownloadSectionResponse), align=36),
+ lambda pkt: pkt.message_type == OmciDownloadSectionResponse.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciEndSoftwareDownload), align=36),
+ lambda pkt: pkt.message_type == OmciEndSoftwareDownload.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciEndSoftwareDownloadResponse), align=36),
+ lambda pkt: pkt.message_type == OmciEndSoftwareDownloadResponse.message_id),
+
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciActivateImage), align=36),
+ lambda pkt: pkt.message_type == OmciActivateImage.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciActivateImageResponse), align=36),
+ lambda pkt: pkt.message_type == OmciActivateImageResponse.message_id),
+
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciCommitImage), align=36),
+ lambda pkt: pkt.message_type == OmciCommitImage.message_id),
+ ConditionalField(FixedLenField(
+ PacketField("omci_message", None, OmciCommitImageResponse), align=36),
+ lambda pkt: pkt.message_type == OmciCommitImageResponse.message_id),
+
+ # TODO add entries for remaining OMCI message types
+
+ IntField("omci_trailer", 0x00000028)
+ ]
+
+ # We needed to patch the do_dissect(...) method of Packet, because
+ # it wiped out already dissected conditional fields with None if they
+ # referred to the same field name. We marked the only new line of code
+ # with "Extra condition added".
+ def do_dissect(self, s):
+ raw = s
+ self.raw_packet_cache_fields = {}
+ for f in self.fields_desc:
+ if not s:
+ break
+ s, fval = f.getfield(self, s)
+ # We need to track fields with mutable values to discard
+ # .raw_packet_cache when needed.
+ if f.islist or f.holds_packets:
+ self.raw_packet_cache_fields[f.name] = f.do_copy(fval)
+ # Extra condition added
+ if fval is not None or f.name not in self.fields:
+ self.fields[f.name] = fval
+ assert(raw.endswith(s))
+ self.raw_packet_cache = raw[:-len(s)] if s else raw
+ self.explicit = 1
+ return s
diff --git a/python/adapters/extensions/omci/omci_me.py b/python/adapters/extensions/omci/omci_me.py
new file mode 100644
index 0000000..a8a2d05
--- /dev/null
+++ b/python/adapters/extensions/omci/omci_me.py
@@ -0,0 +1,939 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""
+OMCI Managed Entity Frame support
+"""
+from voltha.extensions.omci.omci import *
+from voltha.extensions.omci.me_frame import MEFrame
+
+
+class CardholderFrame(MEFrame):
+ """
+ This managed entity represents fixed equipment slot configuration
+ for the ONU
+ """
+ def __init__(self, single, slot_number, attributes):
+ """
+ :param single:(bool) True if the ONU is a single piece of integrated equipment,
+ False if the ONU contains pluggable equipment modules
+ :param slot_number: (int) slot number (0..254)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ # Validate
+ MEFrame.check_type(single, bool)
+ MEFrame.check_type(slot_number, int)
+ if not 0 <= slot_number <= 254:
+ raise ValueError('slot_number should be 0..254')
+
+ entity_id = 256 + slot_number if single else slot_number
+
+ super(CardholderFrame, self).__init__(Cardholder, entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class CircuitPackFrame(MEFrame):
+ """
+ This managed entity models a real or virtual circuit pack that is equipped in
+ a real or virtual ONU slot.
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. Its value is the same as that
+ of the cardholder managed entity containing this
+ circuit pack instance. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(CircuitPackFrame, self).__init__(CircuitPack, entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class ExtendedVlanTaggingOperationConfigurationDataFrame(MEFrame):
+ """
+ This managed entity organizes data associated with VLAN tagging. Regardless
+ of its point of attachment, the specified tagging operations refer to the
+ upstream direction.
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. Its value is the same as that
+ of the cardholder managed entity containing this
+ circuit pack instance. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(ExtendedVlanTaggingOperationConfigurationDataFrame,
+ self).__init__(ExtendedVlanTaggingOperationConfigurationData,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class IpHostConfigDataFrame(MEFrame):
+ """
+ The IP host config data configures IPv4 based services offered on the ONU.
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(IpHostConfigDataFrame, self).__init__(IpHostConfigData,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class GalEthernetProfileFrame(MEFrame):
+ """
+ This managed entity organizes data that describe the GTC adaptation layer
+ processing functions of the ONU for Ethernet services.
+ """
+ def __init__(self, entity_id, max_gem_payload_size=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param max_gem_payload_size: (int) This attribute defines the maximum payload
+ size generated in the associated GEM interworking
+ termination point managed entity. (0..65535
+ """
+ MEFrame.check_type(max_gem_payload_size, (int, type(None)))
+ if max_gem_payload_size is not None and not 0 <= max_gem_payload_size <= 0xFFFF: # TODO: verify min/max
+ raise ValueError('max_gem_payload_size should be 0..0xFFFF')
+
+ data = None if max_gem_payload_size is None else\
+ {
+ 'max_gem_payload_size': max_gem_payload_size
+ }
+ super(GalEthernetProfileFrame, self).__init__(GalEthernetProfile,
+ entity_id,
+ data)
+
+
+class GemInterworkingTpFrame(MEFrame):
+ """
+ An instance of this managed entity represents a point in the ONU where the
+ interworking of a bearer service (usually Ethernet) to the GEM layer takes
+ place.
+ """
+ def __init__(self, entity_id,
+ gem_port_network_ctp_pointer=None,
+ interworking_option=None,
+ service_profile_pointer=None,
+ interworking_tp_pointer=None,
+ pptp_counter=None,
+ gal_profile_pointer=None,
+ attributes=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param gem_port_network_ctp_pointer: (int) This attribute points to an instance of
+ the GEM port network CTP. (0..65535)
+
+ :param interworking_option: (int) This attribute identifies the type
+ of non-GEM function that is being interworked.
+ The options are:
+ 0 Circuit-emulated TDM
+ 1 MAC bridged LAN
+ 2 Reserved
+ 3 Reserved
+ 4 Video return path
+ 5 IEEE 802.1p mapper
+ 6 Downstream broadcast
+ 7 MPLS PW TDM service
+
+ :param service_profile_pointer: (int) This attribute points to an instance of
+ a service profile.
+ CES service profile if interworking option = 0
+ MAC bridge service profile if interworking option = 1
+ Video return path service profile if interworking option = 4
+ IEEE 802.1p mapper service profile if interworking option = 5
+ Null pointer if interworking option = 6
+ CES service profile if interworking option = 7
+
+ :param interworking_tp_pointer: (int) This attribute is used for the circuit
+ emulation service and IEEE 802.1p mapper
+ service without a MAC bridge.
+
+ :param gal_profile_pointer: (int) This attribute points to an instance of
+ a service profile.
+
+ :param attributes: (basestring, list, set, dict) additional ME attributes.
+ not specifically specified as a parameter. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified..
+ """
+ # Validate
+ self.check_type(gem_port_network_ctp_pointer, (int, type(None)))
+ self.check_type(interworking_option, (int, type(None)))
+ self.check_type(service_profile_pointer, (int, type(None)))
+ self.check_type(interworking_tp_pointer,(int, type(None)))
+ self.check_type(pptp_counter,(int, type(None)))
+ self.check_type(gal_profile_pointer, (int, type(None)))
+
+ if gem_port_network_ctp_pointer is not None and not 0 <= gem_port_network_ctp_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('gem_port_network_ctp_pointer should be 0..0xFFFE')
+
+ if interworking_option is not None and not 0 <= interworking_option <= 7:
+ raise ValueError('interworking_option should be 0..7')
+
+ if service_profile_pointer is not None and not 0 <= service_profile_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('service_profile_pointer should be 0..0xFFFE')
+
+ if interworking_tp_pointer is not None and not 0 <= interworking_tp_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('interworking_tp_pointer should be 0..0xFFFE')
+
+ if pptp_counter is not None and not 0 <= pptp_counter <= 255: # TODO: Verify max
+ raise ValueError('pptp_counter should be 0..255')
+
+ if gal_profile_pointer is not None and not 0 <= gal_profile_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('gal_profile_pointer should be 0..0xFFFE')
+
+ data = MEFrame._attr_to_data(attributes)
+
+ if gem_port_network_ctp_pointer is not None or \
+ interworking_option is not None or \
+ service_profile_pointer is not None or \
+ interworking_tp_pointer is not None or \
+ gal_profile_pointer is not None:
+
+ data = data or dict()
+
+ if gem_port_network_ctp_pointer is not None:
+ data['gem_port_network_ctp_pointer'] = gem_port_network_ctp_pointer
+
+ if interworking_option is not None:
+ data['interworking_option'] = interworking_option
+
+ if service_profile_pointer is not None:
+ data['service_profile_pointer'] = service_profile_pointer
+
+ if interworking_tp_pointer is not None:
+ data['interworking_tp_pointer'] = interworking_tp_pointer
+
+ if gal_profile_pointer is not None:
+ data['gal_profile_pointer'] = gal_profile_pointer
+
+ super(GemInterworkingTpFrame, self).__init__(GemInterworkingTp,
+ entity_id,
+ data)
+
+
+class GemPortNetworkCtpFrame(MEFrame):
+ """
+ This managed entity represents the termination of a GEM port on an ONU.
+ """
+ def __init__(self, entity_id, port_id=None, tcont_id=None,
+ direction=None, upstream_tm=None, attributes=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param port_id: (int) This attribute is the port-ID of the GEM port associated
+ with this CTP
+
+ :param tcont_id: (int) This attribute points to a T-CONT instance
+
+ :param direction: (string) Data direction. Valid values are:
+ 'upstream' - UNI-to-ANI
+ 'downstream' - ANI-to-UNI
+ 'bi-directional' - guess :-)
+
+ :param upstream_tm: (int) If the traffic management option attribute in
+ the ONU-G ME is 0 (priority controlled) or 2
+ (priority and rate controlled), this pointer
+ specifies the priority queue ME serving this GEM
+ port network CTP. If the traffic management
+ option attribute is 1 (rate controlled), this
+ attribute redundantly points to the T-CONT serving
+ this GEM port network CTP.
+
+ :param attributes: (basestring, list, set, dict) additional ME attributes.
+ not specifically specified as a parameter. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ _directions = {"upstream": 1, "downstream": 2, "bi-directional": 3}
+
+ # Validate
+ self.check_type(port_id, (int, type(None)))
+ self.check_type(tcont_id, (int, type(None)))
+ self.check_type(direction, (basestring, type(None)))
+ self.check_type(upstream_tm, (int, type(None)))
+
+ if port_id is not None and not 0 <= port_id <= 0xFFFE: # TODO: Verify max
+ raise ValueError('port_id should be 0..0xFFFE')
+
+ if tcont_id is not None and not 0 <= tcont_id <= 0xFFFE: # TODO: Verify max
+ raise ValueError('tcont_id should be 0..0xFFFE')
+
+ if direction is not None and str(direction).lower() not in _directions:
+ raise ValueError('direction should one of {}'.format(_directions.keys()))
+
+ if upstream_tm is not None and not 0 <= upstream_tm <= 0xFFFE: # TODO: Verify max
+ raise ValueError('upstream_tm should be 0..0xFFFE')
+
+ data = MEFrame._attr_to_data(attributes)
+
+ if port_id is not None or tcont_id is not None or\
+ direction is not None or upstream_tm is not None:
+
+ data = data or dict()
+
+ if port_id is not None:
+ data['port_id'] = port_id
+ if tcont_id is not None:
+ data['tcont_pointer'] = tcont_id
+ if direction is not None:
+ data['direction'] = _directions[str(direction).lower()]
+ if upstream_tm is not None:
+ data['traffic_management_pointer_upstream'] = upstream_tm
+
+ super(GemPortNetworkCtpFrame, self).__init__(GemPortNetworkCtp,
+ entity_id,
+ data)
+
+
+class Ieee8021pMapperServiceProfileFrame(MEFrame):
+ """
+ This managed entity associates the priorities of IEEE 802.1p [IEEE
+ 802.1D] priority tagged frames with specific connections.
+ """
+ def __init__(self, entity_id, tp_pointer=None, interwork_tp_pointers=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param tp_pointer: (int) This attribute points to an instance of the
+ associated termination point. (0..65535)
+
+ :param interwork_tp_pointers: (list) List of 1 to 8 interworking termination
+ point IDs. The first entry is assigned
+ got p-bit priority 0. If less than 8 IDs
+ are provided, the last ID is used for
+ the remaining items.
+ """
+ if tp_pointer is None and interwork_tp_pointers is None:
+ data = dict(
+ tp_pointer=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_0=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_1=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_2=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_3=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_4=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_5=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_6=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_7=OmciNullPointer
+ )
+ else:
+ self.check_type(tp_pointer, (list, type(None)))
+ self.check_type(interwork_tp_pointers, (list, type(None)))
+
+ data = dict()
+
+ if tp_pointer is not None:
+ data['tp_pointer'] = tp_pointer
+
+ if interwork_tp_pointers is not None:
+ assert all(isinstance(tp, int) and 0 <= tp <= 0xFFFF
+ for tp in interwork_tp_pointers),\
+ 'Interworking TP IDs must be 0..0xFFFF'
+ assert 1 <= len(interwork_tp_pointers) <= 8, \
+ 'Invalid number of Interworking TP IDs. Must be 1..8'
+
+ data = dict()
+ for pbit in range(0, len(interwork_tp_pointers)):
+ data['interwork_tp_pointer_for_p_bit_priority_{}'.format(pbit)] = \
+ interwork_tp_pointers[pbit]
+
+ for pbit in range(len(interwork_tp_pointers), 8):
+ data['interwork_tp_pointer_for_p_bit_priority_{}'.format(pbit)] = \
+ interwork_tp_pointers[len(interwork_tp_pointers) - 1]
+
+ super(Ieee8021pMapperServiceProfileFrame, self).__init__(Ieee8021pMapperServiceProfile,
+ entity_id,
+ data)
+
+
+class MacBridgePortConfigurationDataFrame(MEFrame):
+ """
+ This managed entity represents the ONU as equipment.
+ """
+ def __init__(self, entity_id, bridge_id_pointer=None, port_num=None,
+ tp_type=None, tp_pointer=None, attributes=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param bridge_id_pointer: (int) This attribute points to an instance of the
+ MAC bridge service profile. (0..65535)
+
+ :param port_num: (int) This attribute is the bridge port number. (0..255)
+
+ :param tp_type: (int) This attribute identifies the type of termination point
+ associated with this MAC bridge port. Valid values are:
+ 1 Physical path termination point Ethernet UNI
+ 2 Interworking VCC termination point
+ 3 IEEE 802.1p mapper service profile
+ 4 IP host config data or IPv6 host config data
+ 5 GEM interworking termination point
+ 6 Multicast GEM interworking termination point
+ 7 Physical path termination point xDSL UNI part 1
+ 8 Physical path termination point VDSL UNI
+ 9 Ethernet flow termination point
+ 10 Reserved
+ 11 Virtual Ethernet interface point
+ 12 Physical path termination point MoCA UNI
+
+ :param tp_pointer: (int) This attribute points to the termination point
+ associated with this MAC bridge por. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) additional ME attributes.
+ not specifically specified as a parameter. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ # Validate
+ self.check_type(bridge_id_pointer, (int, type(None)))
+ self.check_type(port_num, (int, type(None)))
+ self.check_type(tp_type, (int, type(None)))
+ self.check_type(tp_pointer, (int, type(None)))
+
+ if bridge_id_pointer is not None and not 0 <= bridge_id_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('bridge_id_pointer should be 0..0xFFFE')
+
+ if port_num is not None and not 0 <= port_num <= 255:
+ raise ValueError('port_num should be 0..255') # TODO: Verify min,max
+
+ if tp_type is not None and not 1 <= tp_type <= 12:
+ raise ValueError('service_profile_pointer should be 1..12')
+
+ if tp_pointer is not None and not 0 <= tp_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('interworking_tp_pointer should be 0..0xFFFE')
+
+ data = MEFrame._attr_to_data(attributes)
+
+ if bridge_id_pointer is not None or \
+ port_num is not None or \
+ tp_type is not None or \
+ tp_pointer is not None:
+
+ data = data or dict()
+
+ if bridge_id_pointer is not None:
+ data['bridge_id_pointer'] = bridge_id_pointer
+
+ if port_num is not None:
+ data['port_num'] = port_num
+
+ if tp_type is not None:
+ data['tp_type'] = tp_type
+
+ if tp_pointer is not None:
+ data['tp_pointer'] = tp_pointer
+
+ super(MacBridgePortConfigurationDataFrame, self).\
+ __init__(MacBridgePortConfigurationData, entity_id, data)
+
+
+class MacBridgeServiceProfileFrame(MEFrame):
+ """
+ This managed entity models a MAC bridge in its entirety; any number
+ of ports may be associated with the bridge through pointers to the
+ MAC bridge service profile managed entity.
+ """
+ def __init__(self, entity_id, attributes=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(MacBridgeServiceProfileFrame, self).__init__(MacBridgeServiceProfile,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class OntGFrame(MEFrame):
+ """
+ This managed entity represents the ONU as equipment.
+ """
+ def __init__(self, attributes=None):
+ """
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(OntGFrame, self).__init__(OntG, 0,
+ MEFrame._attr_to_data(attributes))
+
+
+class Ont2GFrame(MEFrame):
+ """
+ This managed entity contains additional attributes associated with a PON ONU.
+ """
+ def __init__(self, attributes=None):
+ """
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ # Only one managed entity instance (Entity ID=0)
+ super(Ont2GFrame, self).__init__(Ont2G, 0,
+ MEFrame._attr_to_data(attributes))
+
+
+class PptpEthernetUniFrame(MEFrame):
+ """
+ This managed entity represents the point at an Ethernet UNI where the physical path
+ terminates and Ethernet physical level functions are performed.
+ """
+ def __init__(self, entity_id, attributes=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(PptpEthernetUniFrame, self).__init__(PptpEthernetUni, entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class VeipUniFrame(MEFrame):
+ """
+ This managed entity represents the point a virtual UNI interfaces to a non omci management domain
+ This is typically seen in RG+ONU all-in-one type devices
+ """
+ def __init__(self, entity_id, attributes=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(VeipUniFrame, self).__init__(VeipUni, entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class SoftwareImageFrame(MEFrame):
+ """
+ This managed entity models an executable software image stored in the ONU.
+ """
+ def __init__(self, entity_id):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+ """
+ super(SoftwareImageFrame, self).__init__(SoftwareImage, entity_id, None)
+
+
+class TcontFrame(MEFrame):
+ """
+ An instance of the traffic container managed entity T-CONT represents a
+ logical connection group associated with a G-PON PLOAM layer alloc-ID.
+ """
+ def __init__(self, entity_id, alloc_id=None, policy=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param alloc_id: (int) This attribute links the T-CONT with the alloc-ID
+ assigned by the OLT in the assign_alloc-ID PLOAM
+ message (0..0xFFF) or 0xFFFF to mark as free
+
+ :param policy: (int) This attribute indicates the T-CONT's traffic scheduling
+ policy. Valid values:
+ 0 - Null
+ 1 - Strict priority
+ 2 - WRR - Weighted round robin
+ """
+ # Validate
+ self.check_type(alloc_id, (int, type(None)))
+ self.check_type(policy, (int, type(None)))
+
+ if alloc_id is not None and not (0 <= alloc_id <= 0xFFF or alloc_id == 0xFFFF):
+ raise ValueError('alloc_id should be 0..0xFFF or 0xFFFF to mark it as free')
+
+ if policy is not None and not 0 <= policy <= 2:
+ raise ValueError('policy should be 0..2')
+
+ if alloc_id is None and policy is None:
+ data = None
+ else:
+ data = dict()
+
+ if alloc_id is not None:
+ data['alloc_id'] = alloc_id
+
+ if policy is not None:
+ data['policy'] = policy
+
+ super(TcontFrame, self).__init__(Tcont, entity_id, data)
+
+
+class VlanTaggingFilterDataFrame(MEFrame):
+ """
+ An instance of this managed entity represents a point in the ONU where the
+ interworking of a bearer service (usually Ethernet) to the GEM layer takes
+ place.
+ """
+ def __init__(self, entity_id, vlan_tcis=None, forward_operation=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param vlan_tcis: (list) This attribute is a list of provisioned TCI values
+ for the bridge port. (0..0xFFFF)
+
+ :param forward_operation: (int) What to do. See ITU spec for more information
+
+ """
+ # Validate
+ self.check_type(vlan_tcis, (list, type(None)))
+ self.check_type(forward_operation, (int, type(None)))
+
+ if forward_operation is not None and not 0 <= forward_operation <= 0x21:
+ raise ValueError('forward_operation should be 0..0x21')
+
+ if vlan_tcis is None and forward_operation is None:
+ data = None
+
+ else:
+ data = dict()
+
+ if vlan_tcis is not None:
+ num_tcis = len(vlan_tcis)
+
+ assert 0 <= num_tcis <= 12, 'Number of VLAN TCI values is 0..12'
+ assert all(isinstance(tci, int) and 0 <= tci <= 0xFFFF
+ for tci in vlan_tcis), "VLAN TCI's are 0..0xFFFF"
+
+ if num_tcis > 0:
+ vlan_filter_list = [0] * 12
+ for index in range(0, num_tcis):
+ vlan_filter_list[index] = vlan_tcis[index]
+
+ data['vlan_filter_list'] = vlan_filter_list
+ data['number_of_entries'] = num_tcis
+
+ if forward_operation is not None:
+ assert 0 <= forward_operation <= 0x21, \
+ 'forwarding_operation must be 0x00..0x21'
+ data['forward_operation'] = forward_operation
+
+ super(VlanTaggingFilterDataFrame, self).__init__(VlanTaggingFilterData,
+ entity_id,
+ data)
+
+
+class OntDataFrame(MEFrame):
+ """
+ This managed entity models the MIB itself
+ """
+ def __init__(self, mib_data_sync=None, sequence_number=None, ignore_arc=None):
+ """
+ For 'get', 'MIB reset', 'MIB upload', pass no value
+ For 'set' actions, pass mib_data_sync value (0..255)
+ For 'MIB upload next',and 'Get all alarms next' pass sequence_number value (0..65535)
+ For 'Get all alarms", set ignore_arc to True to get all alarms regadrless
+ of ARC status or False to get all alarms not currently
+ under ARC
+
+ :param mib_data_sync: (int) This attribute is used to check the alignment
+ of the MIB of the ONU with the corresponding MIB
+ in the OLT. (0..0xFF)
+ :param sequence_number: (int) This is used for MIB Upload Next (0..0xFFFF)
+ :param ignore_arc: (bool) None for all but 'get_all_alarm' commands
+ """
+ self.check_type(mib_data_sync, (int, type(None)))
+ if mib_data_sync is not None and not 0 <= mib_data_sync <= 0xFF:
+ raise ValueError('mib_data_sync should be 0..0xFF')
+
+ if sequence_number is not None and not 0 <= sequence_number <= 0xFFFF:
+ raise ValueError('sequence_number should be 0..0xFFFF')
+
+ if ignore_arc is not None and not isinstance(ignore_arc, bool):
+ raise TypeError('ignore_arc should be a boolean')
+
+ if mib_data_sync is not None:
+ # Note: Currently the Scapy decode/encode is 16-bits since we need
+ # the data field that large in order to support MIB and Alarm Upload Next
+ # commands. Push our 8-bit MDS value into the upper 8-bits so that
+ # it is encoded properly into the ONT_Data 'set' frame
+ data = {'mib_data_sync': mib_data_sync << 8}
+
+ elif sequence_number is not None:
+ data = {'mib_data_sync': sequence_number}
+
+ elif ignore_arc is not None:
+ data = {'mib_data_sync': 0 if ignore_arc else 1}
+
+ else:
+ data = {'mib_data_sync'} # Make Get's happy
+
+ super(OntDataFrame, self).__init__(OntData, 0, data)
+
+
+class OmciFrame(MEFrame):
+ """
+ This managed entity describes the ONU's general level of support for OMCI managed
+ entities and messages. This ME is not included in a MIB upload.
+ """
+ def __init__(self, me_type_table=None, message_type_table=None):
+ """
+ For 'get' request, set the type of table count you wish by
+ setting either me_me_type_table or message_type_table to
+ a boolean 'True' value
+
+ For 'get-next' requests, set the sequence number for the
+ table you wish to retrieve by setting either me_me_type_table or message_type_table to
+ a integer value.
+ """
+ if not isinstance(me_type_table, (bool, int, type(None))):
+ raise TypeError('Parameters must be a boolean or integer')
+
+ if not isinstance(message_type_table, (bool, int, type(None))):
+ raise TypeError('Parameters must be a boolean or integer')
+
+ if me_type_table is not None:
+ if isinstance(me_type_table, bool):
+ data = {'me_type_table'}
+ else:
+ data = {'me_type_table': me_type_table}
+
+ elif message_type_table is not None:
+ if isinstance('message_type_table', bool):
+ data = {'message_type_table'}
+ else:
+ data = {'message_type_table': message_type_table}
+ else:
+ raise NotImplemented('Unknown request')
+
+ super(OmciFrame, self).__init__(Omci, 0, data)
+
+
+class EthernetPMMonitoringHistoryDataFrame(MEFrame):
+ """
+ This managed entity collects some of the performance monitoring data for a physical
+ Ethernet interface
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. Through an identical ID, this
+ managed entity is implicitly linked to an instance
+ of the physical path termination point Ethernet UNI
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(EthernetPMMonitoringHistoryDataFrame, self).__init__(
+ EthernetPMMonitoringHistoryData,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class FecPerformanceMonitoringHistoryDataFrame(MEFrame):
+ """
+ This managed entity collects performance monitoring data associated with PON
+ downstream FEC counters.
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. Through an identical ID, this
+ managed entity is implicitly linked to an instance of
+ the ANI-G
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(FecPerformanceMonitoringHistoryDataFrame, self).__init__(
+ FecPerformanceMonitoringHistoryData,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class EthernetFrameDownstreamPerformanceMonitoringHistoryDataFrame(MEFrame):
+ """
+ This managed entity collects performance monitoring data associated with downstream
+ Ethernet frame delivery. It is based on the Etherstats group of [IETF RFC 2819].
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. Through an identical ID, this
+ managed entity is implicitly linked to an instance of
+ a MAC bridge port configuration data
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(EthernetFrameDownstreamPerformanceMonitoringHistoryDataFrame, self).__init__(
+ EthernetFrameDownstreamPerformanceMonitoringHistoryData,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class EthernetFrameUpstreamPerformanceMonitoringHistoryDataFrame(MEFrame):
+ """
+ This managed entity collects performance monitoring data associated with upstream
+ Ethernet frame delivery. It is based on the Etherstats group of [IETF RFC 2819].
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. Through an identical ID, this
+ managed entity is implicitly linked to an instance of
+ a MAC bridge port configuration data
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(EthernetFrameUpstreamPerformanceMonitoringHistoryDataFrame, self).__init__(
+ EthernetFrameUpstreamPerformanceMonitoringHistoryData,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class GemPortNetworkCtpMonitoringHistoryDataFrame(MEFrame):
+ """
+ This managed entity collects GEM frame performance monitoring data associated
+ with a GEM port network CTP
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. Through an identical ID, this
+ managed entity is implicitly linked to an instance
+ of the GEM port network CTP.
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(GemPortNetworkCtpMonitoringHistoryDataFrame, self).__init__(
+ GemPortNetworkCtpMonitoringHistoryData,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class XgPonTcPerformanceMonitoringHistoryDataFrame(MEFrame):
+ """
+ This managed entity collects performance monitoring data associated with
+ the XG-PON transmission convergence layer, as defined in [ITU-T G.987.3]
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. Through an identical ID, this
+ managed entity is implicitly linked to an instance of
+ the ANI-G.
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(XgPonTcPerformanceMonitoringHistoryDataFrame, self).__init__(
+ XgPonTcPerformanceMonitoringHistoryData, entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class XgPonDownstreamPerformanceMonitoringHistoryDataFrame(MEFrame):
+ """
+ This managed entity collects performance monitoring data associated with
+ the XG-PON ined in [ITU-T G.987.3]
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. Through an identical ID, this
+ managed entity is implicitly linked to an instance of
+ the ANI-G.
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(XgPonDownstreamPerformanceMonitoringHistoryDataFrame, self).__init__(
+ XgPonDownstreamPerformanceMonitoringHistoryData,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class XgPonUpstreamPerformanceMonitoringHistoryDataFrame(MEFrame):
+ """
+ This managed entity collects performance monitoring data associated with
+ the XG-PON transmission convergence layer, as defined in [ITU-T G.987.3]
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) TThis attribute uniquely identifies each instance of
+ this managed entity. Through an identical ID, this
+ managed entity is implicitly linked to an instance of
+ the ANI-G.
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(XgPonUpstreamPerformanceMonitoringHistoryDataFrame, self).__init__(
+ XgPonUpstreamPerformanceMonitoringHistoryData,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
diff --git a/python/adapters/extensions/omci/omci_messages.py b/python/adapters/extensions/omci/omci_messages.py
new file mode 100644
index 0000000..04d3e83
--- /dev/null
+++ b/python/adapters/extensions/omci/omci_messages.py
@@ -0,0 +1,551 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import structlog
+from scapy.fields import ByteField, ThreeBytesField, StrFixedLenField, ConditionalField, IntField, Field
+from scapy.fields import ShortField, BitField
+from scapy.packet import Packet
+
+from voltha.extensions.omci.omci_defs import AttributeAccess, OmciSectionDataSize
+from voltha.extensions.omci.omci_fields import OmciTableField
+import voltha.extensions.omci.omci_entities as omci_entities
+
+
+log = structlog.get_logger()
+
+
+class OmciData(Field):
+
+ __slots__ = Field.__slots__ + ['_entity_class']
+
+ def __init__(self, name, entity_class="entity_class"):
+ Field.__init__(self, name=name, default=None, fmt='s')
+ self._entity_class = entity_class
+
+ def addfield(self, pkt, s, val):
+ class_id = getattr(pkt, self._entity_class)
+ entity_class = omci_entities.entity_id_to_class_map.get(class_id)
+ for attribute in entity_class.attributes:
+ if AttributeAccess.SetByCreate not in attribute.access:
+ continue
+ if attribute.field.name == 'managed_entity_id':
+ continue
+ fld = attribute.field
+ s = fld.addfield(pkt, s, val.get(fld.name, fld.default))
+ return s
+
+ def getfield(self, pkt, s):
+ """Extract an internal value from a string"""
+ class_id = getattr(pkt, self._entity_class)
+ entity_class = omci_entities.entity_id_to_class_map.get(class_id)
+ data = {}
+ for attribute in entity_class.attributes:
+ if AttributeAccess.SetByCreate not in attribute.access:
+ continue
+ if attribute.field.name == 'managed_entity_id':
+ continue
+ fld = attribute.field
+ s, value = fld.getfield(pkt, s)
+ data[fld.name] = value
+ return s, data
+
+
+class OmciMaskedData(Field):
+
+ __slots__ = Field.__slots__ + ['_entity_class', '_attributes_mask']
+
+ def __init__(self, name, entity_class="entity_class",
+ attributes_mask="attributes_mask"):
+ Field.__init__(self, name=name, default=None, fmt='s')
+ self._entity_class = entity_class
+ self._attributes_mask = attributes_mask
+
+ def addfield(self, pkt, s, val):
+ class_id = getattr(pkt, self._entity_class)
+ attribute_mask = getattr(pkt, self._attributes_mask)
+ entity_class = omci_entities.entity_id_to_class_map.get(class_id)
+ indices = entity_class.attribute_indices_from_mask(attribute_mask)
+ for index in indices:
+ fld = entity_class.attributes[index].field
+ s = fld.addfield(pkt, s, val[fld.name])
+ return s
+
+ def getfield(self, pkt, s):
+ """Extract an internal value from a string"""
+ class_id = getattr(pkt, self._entity_class)
+ attribute_mask = getattr(pkt, self._attributes_mask)
+ entity_class = omci_entities.entity_id_to_class_map[class_id]
+ indices = entity_class.attribute_indices_from_mask(attribute_mask)
+ data = {}
+ table_attribute_mask = 0
+ for index in indices:
+ try:
+ fld = entity_class.attributes[index].field
+ except IndexError, e:
+ log.error("attribute-decode-failure", attribute_index=index,
+ entity_class=entity_class, e=e)
+ continue
+ try:
+ s, value = fld.getfield(pkt, s)
+ except Exception, _e:
+ raise
+ if isinstance(pkt, OmciGetResponse) and isinstance(fld, OmciTableField):
+ data[fld.name + '_size'] = value
+ table_attribute_mask = table_attribute_mask | (1 << (15 - index))
+ else:
+ data[fld.name] = value
+ if table_attribute_mask:
+ data['table_attribute_mask'] = table_attribute_mask
+ return s, data
+
+
+class OmciMessage(Packet):
+ name = "OmciMessage"
+ message_id = None # OMCI message_type value, filled by derived classes
+ fields_desc = []
+
+
+class OmciCreate(OmciMessage):
+ name = "OmciCreate"
+ message_id = 0x44
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ OmciData("data")
+ ]
+
+
+class OmciCreateResponse(OmciMessage):
+ name = "OmciCreateResponse"
+ message_id = 0x24
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", None),
+ ByteField("success_code", 0),
+ ShortField("parameter_error_attributes_mask", None),
+ ]
+
+
+class OmciDelete(OmciMessage):
+ name = "OmciDelete"
+ message_id = 0x46
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", None),
+ ]
+
+
+class OmciDeleteResponse(OmciMessage):
+ name = "OmciDeleteResponse"
+ message_id = 0x26
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", None),
+ ByteField("success_code", 0),
+ ]
+
+
+class OmciSet(OmciMessage):
+ name = "OmciSet"
+ message_id = 0x48
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ ShortField("attributes_mask", None),
+ OmciMaskedData("data")
+ ]
+
+
+class OmciSetResponse(OmciMessage):
+ name = "OmciSetResponse"
+ message_id = 0x28
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", None),
+ ByteField("success_code", 0),
+ ShortField("unsupported_attributes_mask", None),
+ ShortField("failed_attributes_mask", None),
+ ]
+
+
+class OmciGet(OmciMessage):
+ name = "OmciGet"
+ message_id = 0x49
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ ShortField("attributes_mask", None)
+ ]
+
+
+class OmciGetResponse(OmciMessage):
+ name = "OmciGetResponse"
+ message_id = 0x29
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ ByteField("success_code", 0),
+ ShortField("attributes_mask", None),
+ ConditionalField(
+ OmciMaskedData("data"), lambda pkt: pkt.success_code == 0)
+ ]
+
+
+class OmciGetAllAlarms(OmciMessage):
+ name = "OmciGetAllAlarms"
+ message_id = 0x4b
+ fields_desc = [
+ ShortField("entity_class", 2), # Always 2 (ONT data)
+ ShortField("entity_id", 0), # Always 0 (ONT instance)
+ ByteField("alarm_retrieval_mode", 0) # 0 or 1
+ ]
+
+
+class OmciGetAllAlarmsResponse(OmciMessage):
+ name = "OmciGetAllAlarmsResponse"
+ message_id = 0x2b
+ fields_desc = [
+ ShortField("entity_class", 2), # Always 2 (ONT data)
+ ShortField("entity_id", 0),
+ ShortField("number_of_commands", None)
+ ]
+
+
+class OmciGetAllAlarmsNext(OmciMessage):
+ name = "OmciGetAllAlarmsNext"
+ message_id = 0x4c
+ fields_desc = [
+ ShortField("entity_class", 2), # Always 2 (ONT data)
+ ShortField("entity_id", 0),
+ ShortField("command_sequence_number", None)
+ ]
+
+
+class OmciGetAllAlarmsNextResponse(OmciMessage):
+ name = "OmciGetAllAlarmsNextResponse"
+ message_id = 0x2c
+ fields_desc = [
+ ShortField("entity_class", 2), # Always 2 (ONT data)
+ ShortField("entity_id", 0),
+ ShortField("alarmed_entity_class", None),
+ ShortField("alarmed_entity_id", 0),
+ BitField("alarm_bit_map", None, 224)
+ ]
+
+
+class OmciMibUpload(OmciMessage):
+ name = "OmciMibUpload"
+ message_id = 0x4d
+ fields_desc = [
+ ShortField("entity_class", 2), # Always 2 (ONT data)
+ ShortField("entity_id", 0),
+ ]
+
+
+class OmciMibUploadResponse(OmciMessage):
+ name = "OmciMibUploadResponse"
+ message_id = 0x2d
+ fields_desc = [
+ ShortField("entity_class", 2), # Always 2 (ONT data)
+ ShortField("entity_id", 0),
+ ShortField("number_of_commands", None)
+ ]
+
+
+class OmciMibUploadNext(OmciMessage):
+ name = "OmciMibUploadNext"
+ message_id = 0x4e
+ fields_desc = [
+ ShortField("entity_class", 2), # Always 2 (ONT data)
+ ShortField("entity_id", 0),
+ ShortField("command_sequence_number", None)
+ ]
+
+
+class OmciMibUploadNextResponse(OmciMessage):
+ name = "OmciMibUploadNextResponse"
+ message_id = 0x2e
+ fields_desc = [
+ ShortField("entity_class", 2), # Always 2 (ONT data)
+ ShortField("entity_id", 0),
+ ShortField("object_entity_class", None),
+ ShortField("object_entity_id", 0),
+ ShortField("object_attributes_mask", None),
+ OmciMaskedData("object_data", entity_class='object_entity_class',
+ attributes_mask='object_attributes_mask')
+ ]
+
+
+class OmciMibReset(OmciMessage):
+ name = "OmciMibReset"
+ message_id = 0x4f
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0)
+ ]
+
+
+class OmciMibResetResponse(OmciMessage):
+ name = "OmciMibResetResponse"
+ message_id = 0x2f
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ ByteField("success_code", 0)
+ ]
+
+
+class OmciAlarmNotification(OmciMessage):
+ name = "AlarmNotification"
+ message_id = 0x10
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ BitField("alarm_bit_map", 0, 224),
+ ThreeBytesField("zero_padding", 0),
+ ByteField("alarm_sequence_number", None)
+ ]
+
+
+class OmciAttributeValueChange(OmciMessage):
+ name = "AttributeValueChange"
+ message_id = 0x11
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ ShortField("attributes_mask", None),
+ OmciMaskedData("data")
+ ]
+
+
+class OmciTestResult(OmciMessage):
+ name = "TestResult"
+ message_id = 0x1B
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0)
+ # ME Test specific message contents starts here
+ # TODO: Can this be coded easily with scapy?
+ ]
+
+
+class OmciReboot(OmciMessage):
+ name = "OmciOnuReboot"
+ message_id = 0x59
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ ByteField("reboot_code", 0)
+ ]
+
+
+class OmciRebootResponse(OmciMessage):
+ name = "OmciOnuRebootResponse"
+ message_id = 0x39
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ ByteField("success_code", 0)
+ ]
+
+
+class OmciGetNext(OmciMessage):
+ name = "OmciGetNext"
+ message_id = 0x5A
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ ShortField("attributes_mask", None),
+ ShortField("command_sequence_number", None)
+ ]
+
+
+class OmciGetNextResponse(OmciMessage):
+ name = "OmciGetNextResponse"
+ message_id = 0x3A
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ ByteField("success_code", 0),
+ ShortField("attributes_mask", None),
+ ConditionalField(OmciMaskedData("data"),
+ lambda pkt: pkt.success_code == 0)
+ ]
+
+
+class OmciSynchronizeTime(OmciMessage):
+ name = "OmciSynchronizeTime"
+ message_id = 0x58
+ fields_desc = [
+ ShortField("entity_class", 256), # OntG
+ ShortField("entity_id", 0),
+ ShortField("year", 0), # eg) 2018
+ ByteField("month", 0), # 1..12
+ ByteField("day", 0), # 1..31
+ ByteField("hour", 0), # 0..23
+ ByteField("minute", 0), # 0..59
+ ByteField("second", 0) # 0..59
+ ]
+
+
+class OmciSynchronizeTimeResponse(OmciMessage):
+ name = "OmciSynchronizeTimeResponse"
+ message_id = 0x38
+ fields_desc = [
+ ShortField("entity_class", 256), # OntG
+ ShortField("entity_id", 0),
+ ByteField("success_code", 0),
+ ConditionalField(ShortField("success_info", None),
+ lambda pkt: pkt.success_code == 0)
+ ]
+
+
+class OmciGetCurrentData(OmciMessage):
+ name = "OmciGetCurrentData"
+ message_id = 0x5C
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ ShortField("attributes_mask", None),
+ ]
+
+
+class OmciGetCurrentDataResponse(OmciMessage):
+ name = "OmciGetCurrentDataResponse"
+ message_id = 0x3C
+ fields_desc = [
+ ShortField("entity_class", None),
+ ShortField("entity_id", 0),
+ ByteField("success_code", 0),
+ ShortField("attributes_mask", None),
+ ShortField("unsupported_attributes_mask", None),
+ ShortField("failed_attributes_mask", None),
+ ConditionalField(
+ OmciMaskedData("data"), lambda pkt: pkt.success_code == 0)
+ ]
+
+class OmciStartSoftwareDownload(OmciMessage):
+ name = "OmciStartSoftwareDownload"
+ message_id = 0x53
+ fields_desc = [
+ ShortField("entity_class", 7), # Always 7 (Software image)
+ ShortField("entity_id", None),
+ ByteField("window_size", 0),
+ IntField("image_size", 0),
+ ByteField("image_number", 1), # Always only 1 in parallel
+ ShortField("instance_id", None) # should be same as "entity_id"
+ ]
+
+class OmciStartSoftwareDownloadResponse(OmciMessage):
+ name = "OmciStartSoftwareDownloadResponse"
+ message_id = 0x33
+ fields_desc = [
+ ShortField("entity_class", 7), # Always 7 (Software image)
+ ShortField("entity_id", None),
+ ByteField("result", 0),
+ ByteField("window_size", 0),
+ ByteField("image_number", 1), # Always only 1 in parallel
+ ShortField("instance_id", None) # should be same as "entity_id"
+ ]
+
+class OmciEndSoftwareDownload(OmciMessage):
+ name = "OmciEndSoftwareDownload"
+ message_id = 0x55
+ fields_desc = [
+ ShortField("entity_class", 7), # Always 7 (Software image)
+ ShortField("entity_id", None),
+ IntField("crc32", 0),
+ IntField("image_size", 0),
+ ByteField("image_number", 1), # Always only 1 in parallel
+ ShortField("instance_id", None),# should be same as "entity_id"
+ ]
+
+class OmciEndSoftwareDownloadResponse(OmciMessage):
+ name = "OmciEndSoftwareDownload"
+ message_id = 0x35
+ fields_desc = [
+ ShortField("entity_class", 7), # Always 7 (Software image)
+ ShortField("entity_id", None),
+ ByteField("result", 0),
+ ByteField("image_number", 1), # Always only 1 in parallel
+ ShortField("instance_id", None),# should be same as "entity_id"
+ ByteField("result0", 0) # same as result
+ ]
+
+class OmciDownloadSection(OmciMessage):
+ name = "OmciDownloadSection"
+ message_id = 0x14
+ fields_desc = [
+ ShortField("entity_class", 7), # Always 7 (Software image)
+ ShortField("entity_id", None),
+ ByteField("section_number", 0), # Always only 1 in parallel
+ StrFixedLenField("data", 0, length=OmciSectionDataSize) # section data
+ ]
+
+class OmciDownloadSectionLast(OmciMessage):
+ name = "OmciDownloadSection"
+ message_id = 0x54
+ fields_desc = [
+ ShortField("entity_class", 7), # Always 7 (Software image)
+ ShortField("entity_id", None),
+ ByteField("section_number", 0), # Always only 1 in parallel
+ StrFixedLenField("data", 0, length=OmciSectionDataSize) # section data
+ ]
+
+class OmciDownloadSectionResponse(OmciMessage):
+ name = "OmciDownloadSectionResponse"
+ message_id = 0x34
+ fields_desc = [
+ ShortField("entity_class", 7), # Always 7 (Software image)
+ ShortField("entity_id", None),
+ ByteField("result", 0),
+ ByteField("section_number", 0), # Always only 1 in parallel
+ ]
+
+class OmciActivateImage(OmciMessage):
+ name = "OmciActivateImage"
+ message_id = 0x56
+ fields_desc = [
+ ShortField("entity_class", 7), # Always 7 (Software image)
+ ShortField("entity_id", None),
+ ByteField("activate_flag", 0) # Activate image unconditionally
+ ]
+
+class OmciActivateImageResponse(OmciMessage):
+ name = "OmciActivateImageResponse"
+ message_id = 0x36
+ fields_desc = [
+ ShortField("entity_class", 7), # Always 7 (Software image)
+ ShortField("entity_id", None),
+ ByteField("result", 0) # Activate image unconditionally
+ ]
+
+class OmciCommitImage(OmciMessage):
+ name = "OmciCommitImage"
+ message_id = 0x57
+ fields_desc = [
+ ShortField("entity_class", 7), # Always 7 (Software image)
+ ShortField("entity_id", None),
+ ]
+
+class OmciCommitImageResponse(OmciMessage):
+ name = "OmciCommitImageResponse"
+ message_id = 0x37
+ fields_desc = [
+ ShortField("entity_class", 7), # Always 7 (Software image)
+ ShortField("entity_id", None),
+ ByteField("result", 0) # Activate image unconditionally
+ ]
+
diff --git a/python/adapters/extensions/omci/onu_configuration.py b/python/adapters/extensions/omci/onu_configuration.py
new file mode 100644
index 0000000..1fa00fe
--- /dev/null
+++ b/python/adapters/extensions/omci/onu_configuration.py
@@ -0,0 +1,509 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import structlog
+
+from voltha.protos.device_pb2 import Image
+from omci_entities import *
+from database.mib_db_api import *
+from enum import IntEnum
+
+
+class OMCCVersion(IntEnum):
+ Unknown = 0 # Unknown or unsupported version
+ G_984_4 = 0x80 # (06/04)
+ G_984_4_2005_Amd_1 = 0x81 # Amd.1 (06/05)
+ G_984_4_2006_Amd_2 = 0x82 # Amd.2 (03/06)
+ G_984_4_2006_Amd_3 = 0x83 # Amd.3 (12/06)
+ G_984_4_2008 = 0x84 # (02/08)
+ G_984_4_2009_Amd_1 = 0x85 # Amd.1 (06/09)
+ G_984_4_2009_Amd_2_Base = 0x86 # Amd.2 (2009) Baseline message set only, w/o the extended message set option
+ G_984_4_2009_Amd_2 = 0x96 # Amd.2 (2009) Extended message set option + baseline message set.
+ G_988_2010_Base = 0xA0 # (2010) Baseline message set only, w/o the extended message set option
+ G_988_2011_Amd_1_Base = 0xA1 # Amd.1 (2011) Baseline message set only
+ G_988_2012_Amd_2_Base = 0xA2 # Amd.2 (2012) Baseline message set only
+ G_988_2012_Base = 0xA3 # (2012) Baseline message set only
+ G_988_2010 = 0xB0 # (2010) Baseline and extended message set
+ G_988_2011_Amd_1 = 0xB1 # Amd.1 (2011) Baseline and extended message set
+ G_988_2012_Amd_2 = 0xB2 # Amd.2 (2012) Baseline and extended message set
+ G_988_2012 = 0xB3 # (2012)Baseline and extended message set
+
+ @staticmethod
+ def values():
+ return {OMCCVersion[member].value for member in OMCCVersion.__members__.keys()}
+
+ @staticmethod
+ def to_enum(value):
+ return next((v for k, v in OMCCVersion.__members__.items()
+ if v.value == value), OMCCVersion.Unknown)
+
+
+class OnuConfiguration(object):
+ """
+ Utility class to query OMCI MIB Database for various ONU/OMCI Configuration
+ and capabilties. These capabilities revolve around read-only MEs discovered
+ during the MIB Upload process.
+
+ There is also a 'omci_onu_capabilities' State Machine and an
+ 'onu_capabilities_task.py' OMCI Task that will query the ONU, via the
+ OMCI (ME#287) Managed entity to get the full list of supported OMCI ME's
+ and available actions/message-types supported.
+
+ NOTE: Currently this class is optimized/tested for ONUs that support the
+ OpenOMCI implementation.
+ """
+ def __init__(self, omci_agent, device_id):
+ """
+ Initialize this instance of the OnuConfiguration class
+
+ :param omci_agent: (OpenOMCIAgent) agent reference
+ :param device_id: (str) ONU Device ID
+
+ :raises KeyError: If ONU Device is not registered with OpenOMCI
+ """
+ self.log = structlog.get_logger(device_id=device_id)
+ self._device_id = device_id
+ self._onu_device = omci_agent.get_device(device_id)
+
+ # The capabilities
+ self._attributes = None
+ self.reset()
+
+ def _get_capability(self, attr, class_id, instance_id=None):
+ """
+ Get the OMCI capabilities for this device
+
+ :param attr: (str) OnuConfiguration attribute field
+ :param class_id: (int) ME Class ID
+ :param instance_id: (int) Instance ID. If not provided, all instances of the
+ specified class ID are returned if present in the DB.
+
+ :return: (dict) Class and/or Instances. None is returned if the CLASS is not present
+ """
+ try:
+ assert self._onu_device.mib_synchronizer.last_mib_db_sync is not None, \
+ 'MIB Database for ONU {} has never been synchronized'.format(self._device_id)
+
+ # Get the requested information
+ if self._attributes[attr] is None:
+ value = self._onu_device.query_mib(class_id, instance_id=instance_id)
+
+ if isinstance(value, dict) and len(value) > 0:
+ self._attributes[attr] = value
+
+ return self._attributes[attr]
+
+ except Exception as e:
+ self.log.exception('onu-capabilities', e=e, class_id=class_id,
+ instance_id=instance_id)
+ raise
+
+ def reset(self):
+ """
+ Reset the cached database entries to None. This method should be
+ called after any communications loss to the ONU (reboot, PON down, ...)
+ in case a new software load with different capabilities is available.
+ """
+ self._attributes = {
+ '_ont_g': None,
+ '_ont_2g': None,
+ '_ani_g': None,
+ '_uni_g': None,
+ '_cardholder': None,
+ '_circuit_pack': None,
+ '_software': None,
+ '_pptp': None,
+ '_veip': None
+ }
+
+ @property
+ def version(self):
+ """
+ This attribute identifies the version of the ONU as defined by the vendor
+ """
+ ontg = self._get_capability('_ont_g', OntG.class_id, 0)
+ if ontg is None or ATTRIBUTES_KEY not in ontg:
+ return None
+
+ return ontg[ATTRIBUTES_KEY].get('version')
+
+ @property
+ def serial_number(self):
+ """
+ The serial number is unique for each ONU
+ """
+ ontg = self._get_capability('_ont_g', OntG.class_id, 0)
+ if ontg is None or ATTRIBUTES_KEY not in ontg:
+ return None
+
+ return ontg[ATTRIBUTES_KEY].get('serial_number')
+
+ @property
+ def traffic_management_option(self):
+ """
+ This attribute identifies the upstream traffic management function
+ implemented in the ONU. There are three options:
+
+ 0 Priority controlled and flexibly scheduled upstream traffic. The traffic
+ scheduler and priority queue mechanism are used for upstream traffic.
+
+ 1 Rate controlled upstream traffic. The maximum upstream traffic of each
+ individual connection is guaranteed by shaping.
+
+ 2 Priority and rate controlled. The traffic scheduler and priority queue
+ mechanism are used for upstream traffic. The maximum upstream traffic
+ of each individual connection is guaranteed by shaping.
+ """
+ ontg = self._get_capability('_ont_g', OntG.class_id, 0)
+ if ontg is None or ATTRIBUTES_KEY not in ontg:
+ return None
+
+ return ontg[ATTRIBUTES_KEY].get('traffic_management_option')
+
+ @property
+ def onu_survival_time(self):
+ """
+ This attribute indicates the minimum guaranteed time in milliseconds
+ between the loss of external power and the silence of the ONU. This does not
+ include survival time attributable to a backup battery. The value zero implies that
+ the actual time is not known.
+
+ Optional
+ """
+ ontg = self._get_capability('_ont_g', OntG.class_id, 0)
+ if ontg is None or ATTRIBUTES_KEY not in ontg:
+ return None
+
+ return ontg[ATTRIBUTES_KEY].get('onu_survival_time', 0)
+
+ @property
+ def equipment_id(self):
+ """
+ This attribute may be used to identify the specific type of ONU. In some
+ environments, this attribute may include the equipment CLEI code.
+ """
+ ont2g = self._get_capability('_ont_2g', Ont2G.class_id, 0)
+ if ont2g is None or ATTRIBUTES_KEY not in ont2g:
+ return None
+
+ return ont2g[ATTRIBUTES_KEY].get('equipment_id')
+
+ @property
+ def omcc_version(self):
+ """
+ This attribute identifies the version of the OMCC protocol being used by the
+ ONU. This allows the OLT to manage a network with ONUs that support different
+ OMCC versions. Release levels of [ITU-T G.984.4] are supported with code
+ points of the form 0x8y and 0x9y, where y is a hexadecimal digit in the range
+ 0..F. Support for continuing revisions of this Recommendation is defined in
+ the 0xAy range.
+ """
+ ont2g = self._get_capability('_ont_2g', Ont2G.class_id, 0)
+ if ont2g is None or ATTRIBUTES_KEY not in ont2g:
+ return None
+
+ return OMCCVersion.to_enum(ont2g[ATTRIBUTES_KEY].get('omcc_version', 0))
+
+ @property
+ def vendor_product_code(self):
+ """
+ This attribute contains a vendor-specific product code for the ONU
+ """
+ ont2g = self._get_capability('_ont_2g', Ont2G.class_id, 0)
+ if ont2g is None or ATTRIBUTES_KEY not in ont2g:
+ return None
+
+ return ont2g[ATTRIBUTES_KEY].get('vendor_product_code')
+
+ @property
+ def total_priority_queues(self):
+ """
+ This attribute reports the total number of upstream priority queues
+ that are not associated with a circuit pack, but with the ONU in its entirety
+ """
+ ont2g = self._get_capability('_ont_2g', Ont2G.class_id, 0)
+ if ont2g is None or ATTRIBUTES_KEY not in ont2g:
+ return None
+
+ return ont2g[ATTRIBUTES_KEY].get('total_priority_queue_number')
+
+ @property
+ def total_traffic_schedulers(self):
+ """
+ This attribute reports the total number of traffic schedulers that
+ are not associated with a circuit pack, but with the ONU in its entirety.
+ """
+ ont2g = self._get_capability('_ont_2g', Ont2G.class_id, 0)
+ if ont2g is None or ATTRIBUTES_KEY not in ont2g:
+ return None
+
+ return ont2g[ATTRIBUTES_KEY].get('total_traffic_scheduler_number')
+
+ @property
+ def total_gem_ports(self):
+ """
+ This attribute reports the total number of GEM port-IDs supported
+ by the ONU.
+ """
+ ont2g = self._get_capability('_ont_2g', Ont2G.class_id, 0)
+ if ont2g is None or ATTRIBUTES_KEY not in ont2g:
+ return None
+
+ return ont2g[ATTRIBUTES_KEY].get('total_gem_port_id_number')
+
+ @property
+ def uptime(self):
+ """
+ This attribute counts 10 ms intervals since the ONU was last initialized.
+ It rolls over to 0 when full
+ """
+ ont2g = self._get_capability('_ont_2g', Ont2G.class_id, 0)
+ if ont2g is None or ATTRIBUTES_KEY not in ont2g:
+ return None
+
+ return ont2g[ATTRIBUTES_KEY].get('sys_uptime')
+
+ @property
+ def connectivity_capability(self):
+ """
+ This attribute indicates the Ethernet connectivity models that the ONU
+ can support. The value 0 indicates that the capability is not supported; 1 signifies
+ support.
+
+ Bit Model [Figure reference ITU-T 988]
+ 1 (LSB) N:1 bridging, Figure 8.2.2-3
+ 2 1:M mapping, Figure 8.2.2-4
+ 3 1:P filtering, Figure 8.2.2-5
+ 4 N:M bridge-mapping, Figure 8.2.2-6
+ 5 1:MP map-filtering, Figure 8.2.2-7
+ 6 N:P bridge-filtering, Figure 8.2.2-8
+ 7 to refer to N:MP bridge-map-filtering, Figure 8.2.2-9
+ 8...16 Reserved
+ """
+ ont2g = self._get_capability('_ont_2g', Ont2G.class_id, 0)
+ if ont2g is None or ATTRIBUTES_KEY not in ont2g:
+ return None
+
+ return ont2g[ATTRIBUTES_KEY].get('connectivity_capability')
+
+ @property
+ def qos_configuration_flexibility(self):
+ """
+ This attribute reports whether various managed entities in the
+ ONU are fixed by the ONU's architecture or whether they are configurable. For
+ backward compatibility, and if the ONU does not support this attribute, all such
+ attributes are understood to be hard-wired.
+
+ Bit Interpretation when bit value = 1
+ 1 (LSB) Priority queue ME: Port field of related port attribute is
+ read-write and can point to any T-CONT or UNI port in the
+ same slot
+ 2 Priority queue ME: The traffic scheduler pointer is permitted
+ to refer to any other traffic scheduler in the same slot
+ 3 Traffic scheduler ME: T-CONT pointer is read-write
+ 4 Traffic scheduler ME: Policy attribute is read-write
+ 5 T-CONT ME: Policy attribute is read-write
+ 6 Priority queue ME: Priority field of related port attribute is
+ read-write
+ 7..16 Reserved
+ """
+ ont2g = self._get_capability('_ont_2g', Ont2G.class_id, 0)
+ if ont2g is None or ATTRIBUTES_KEY not in ont2g:
+ return None
+
+ return ont2g[ATTRIBUTES_KEY].get('qos_configuration_flexibility')
+
+ @property
+ def priority_queue_scale_factor(self):
+ """
+ This specifies the scale factor of several attributes of the priority
+ queue managed entity of section 5.2.8
+ """
+ ont2g = self._get_capability('_ont_2g', Ont2G.class_id, 0)
+ if ont2g is None or ATTRIBUTES_KEY not in ont2g:
+ return None
+
+ return ont2g[ATTRIBUTES_KEY].get('priority_queue_scale_factor', 1)
+
+ @property
+ def cardholder_entities(self):
+ """
+ Return a dictionary containing some overall information on the CardHolder
+ instances for this ONU.
+ """
+ ch = self._get_capability('_cardholder', Cardholder.class_id)
+ results = dict()
+
+ if ch is not None:
+ for inst, inst_data in ch.items():
+ if isinstance(inst, int):
+ results[inst] = {
+ 'entity-id': inst,
+ 'is-single-piece': inst >= 256,
+ 'slot-number': inst & 0xff,
+ 'actual-plug-in-type': inst_data[ATTRIBUTES_KEY].get('actual_plugin_unit_type', 0),
+ 'actual-equipment-id': inst_data[ATTRIBUTES_KEY].get('actual_equipment_id', 0),
+ 'protection-profile-ptr': inst_data[ATTRIBUTES_KEY].get('protection_profile_pointer', 0),
+ }
+ return results if len(results) else None
+
+ @property
+ def circuitpack_entities(self):
+ """
+ This specifies the scale factor of several attributes of the priority
+ queue managed entity of section 5.2.8
+ """
+ cp = self._get_capability('_circuit_pack', CircuitPack.class_id)
+ results = dict()
+
+ if cp is not None:
+ for inst, inst_data in cp.items():
+ if isinstance(inst, int):
+ results[inst] = {
+ 'entity-id': inst,
+ 'number-of-ports': inst_data[ATTRIBUTES_KEY].get('number_of_ports', 0),
+ 'serial-number': inst_data[ATTRIBUTES_KEY].get('serial_number', 0),
+ 'version': inst_data[ATTRIBUTES_KEY].get('version', 0),
+ 'vendor-id': inst_data[ATTRIBUTES_KEY].get('vendor_id', 0),
+ 'total-tcont-count': inst_data[ATTRIBUTES_KEY].get('total_tcont_buffer_number', 0),
+ 'total-priority-queue-count': inst_data[ATTRIBUTES_KEY].get('total_priority_queue_number', 0),
+ 'total-traffic-sched-count': inst_data[ATTRIBUTES_KEY].get('total_traffic_scheduler_number', 0),
+ }
+
+ return results if len(results) else None
+
+ @property
+ def software_images(self):
+ """
+ Get a list of software image information for the ONU. The information is provided
+ so that it may be directly added to the protobuf Device information software list.
+ """
+ sw = self._get_capability('_software', SoftwareImage.class_id)
+ images = list()
+
+ if sw is not None:
+ for inst, inst_data in sw.items():
+ if isinstance(inst, int):
+ is_active = inst_data[ATTRIBUTES_KEY].get('is_active', False)
+
+ images.append(Image(name='running-revision' if is_active else 'candidate-revision',
+ version=str(inst_data[ATTRIBUTES_KEY].get('version',
+ 'Not Available').rstrip('\0')),
+ is_active=is_active,
+ is_committed=inst_data[ATTRIBUTES_KEY].get('is_committed',
+ False),
+ is_valid=inst_data[ATTRIBUTES_KEY].get('is_valid',
+ False),
+ install_datetime='Not Available',
+ hash=str(inst_data[ATTRIBUTES_KEY].get('image_hash',
+ 'Not Available').rstrip('\0'))))
+ return images if len(images) else None
+
+ @property
+ def ani_g_entities(self):
+ """
+ This managed entity organizes data associated with each access network
+ interface supported by a G-PON ONU. The ONU automatically creates one
+ instance of this managed entity for each PON physical port.
+ """
+ ag = self._get_capability('_ani_g', AniG.class_id)
+ results = dict()
+
+ if ag is not None:
+ for inst, inst_data in ag.items():
+ if isinstance(inst, int):
+ results[inst] = {
+ 'entity-id': inst,
+ 'slot-number': (inst >> 8) & 0xff,
+ 'port-number': inst & 0xff,
+ 'total-tcont-count': inst_data[ATTRIBUTES_KEY].get('total_tcont_number', 0),
+ 'piggyback-dba-reporting': inst_data[ATTRIBUTES_KEY].get('piggyback_dba_reporting', 0),
+ }
+ return results if len(results) else None
+
+ @property
+ def uni_g_entities(self):
+ """
+ This managed entity organizes data associated with user network interfaces
+ (UNIs) supported by GEM. One instance of the UNI-G managed entity exists
+ for each UNI supported by the ONU.
+
+ The ONU automatically creates or deletes instances of this managed entity
+ upon the creation or deletion of a real or virtual circuit pack managed
+ entity, one per port.
+ """
+ ug = self._get_capability('_uni_g', UniG.class_id)
+ results = dict()
+
+ if ug is not None:
+ for inst, inst_data in ug.items():
+ if isinstance(inst, int):
+ results[inst] = {
+ 'entity-id': inst,
+ 'management-capability': inst_data[ATTRIBUTES_KEY].get('management_capability', 0)
+ }
+ return results if len(results) else None
+
+ @property
+ def pptp_entities(self):
+ """
+ Returns discovered PPTP Ethernet entities. TODO more detail here
+ """
+ pptp = self._get_capability('_pptp', PptpEthernetUni.class_id)
+ results = dict()
+
+ if pptp is not None:
+ for inst, inst_data in pptp.items():
+ if isinstance(inst, int):
+ results[inst] = {
+ 'entity-id': inst,
+ 'expected-type': inst_data[ATTRIBUTES_KEY].get('expected_type', 0),
+ 'sensed-type': inst_data[ATTRIBUTES_KEY].get('sensed_type', 0),
+ 'autodetection-config': inst_data[ATTRIBUTES_KEY].get('auto_detection_configuration', 0),
+ 'ethernet-loopback-config': inst_data[ATTRIBUTES_KEY].get('ethernet_loopback_configuration', 0),
+ 'administrative-state': inst_data[ATTRIBUTES_KEY].get('administrative_state', 0),
+ 'operational-state': inst_data[ATTRIBUTES_KEY].get('operational_state', 0),
+ 'config-ind': inst_data[ATTRIBUTES_KEY].get('configuration_ind', 0),
+ 'max-frame-size': inst_data[ATTRIBUTES_KEY].get('max_frame_size', 0),
+ 'dte-dce-ind': inst_data[ATTRIBUTES_KEY].get('dte_or_dce_ind', 0),
+ 'pause-time': inst_data[ATTRIBUTES_KEY].get('pause_time', 0),
+ 'bridged-ip-ind': inst_data[ATTRIBUTES_KEY].get('bridged_or_ip_ind', 0),
+ 'arc': inst_data[ATTRIBUTES_KEY].get('arc', 0),
+ 'arc-interval': inst_data[ATTRIBUTES_KEY].get('arc_interval', 0),
+ 'pppoe-filter': inst_data[ATTRIBUTES_KEY].get('ppoe_filter', 0),
+ 'power-control': inst_data[ATTRIBUTES_KEY].get('power_control', 0)
+ }
+ return results if len(results) else None
+
+ @property
+ def veip_entities(self):
+ """
+ Returns discovered VEIP entities. TODO more detail here
+ """
+ veip = self._get_capability('_veip', VeipUni.class_id)
+ results = dict()
+
+ if veip is not None:
+ for inst, inst_data in veip.items():
+ if isinstance(inst, int):
+ results[inst] = {
+ 'entity-id': inst,
+ 'administrative-state': inst_data[ATTRIBUTES_KEY].get('administrative_state', 0),
+ 'operational-state': inst_data[ATTRIBUTES_KEY].get('operational_state', 0),
+ 'interdomain-name': inst_data[ATTRIBUTES_KEY].get('interdomain_name', ""),
+ 'tcp-udp-pointer': inst_data[ATTRIBUTES_KEY].get('tcp_udp_pointer', 0),
+ 'iana-assigned-port': inst_data[ATTRIBUTES_KEY].get('iana_assigned_port', 0)
+ }
+ return results if len(results) else None
diff --git a/python/adapters/extensions/omci/onu_device_entry.py b/python/adapters/extensions/omci/onu_device_entry.py
new file mode 100644
index 0000000..7a0c439
--- /dev/null
+++ b/python/adapters/extensions/omci/onu_device_entry.py
@@ -0,0 +1,635 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import structlog
+from copy import deepcopy
+from voltha.protos.device_pb2 import ImageDownload
+from voltha.extensions.omci.omci_defs import EntityOperations, ReasonCodes
+import voltha.extensions.omci.omci_entities as omci_entities
+from voltha.extensions.omci.omci_cc import OMCI_CC
+from common.event_bus import EventBusClient
+from voltha.extensions.omci.tasks.task_runner import TaskRunner
+from voltha.extensions.omci.onu_configuration import OnuConfiguration
+from voltha.extensions.omci.tasks.reboot_task import OmciRebootRequest, RebootFlags
+from voltha.extensions.omci.tasks.omci_modify_request import OmciModifyRequest
+from voltha.extensions.omci.omci_me import OntGFrame
+from voltha.extensions.omci.state_machines.image_agent import ImageAgent
+
+from twisted.internet import reactor, defer
+from enum import IntEnum
+
+OP = EntityOperations
+RC = ReasonCodes
+
+ACTIVE_KEY = 'active'
+IN_SYNC_KEY = 'in-sync'
+LAST_IN_SYNC_KEY = 'last-in-sync-time'
+SUPPORTED_MESSAGE_ENTITY_KEY = 'managed-entities'
+SUPPORTED_MESSAGE_TYPES_KEY = 'message-type'
+
+
+class OnuDeviceEvents(IntEnum):
+ # Events of interest to Device Adapters and OpenOMCI State Machines
+ DeviceStatusEvent = 0 # OnuDeviceEntry running status changed
+ MibDatabaseSyncEvent = 1 # MIB database sync changed
+ OmciCapabilitiesEvent = 2 # OMCI ME and message type capabilities
+ AlarmDatabaseSyncEvent = 3 # Alarm database sync changed
+
+ # TODO: Add other events here as needed
+
+
+class OnuDeviceEntry(object):
+ """
+ An ONU Device entry in the MIB
+ """
+ def __init__(self, omci_agent, device_id, adapter_agent, custom_me_map,
+ mib_db, alarm_db, support_classes, clock=None):
+ """
+ Class initializer
+
+ :param omci_agent: (OpenOMCIAgent) Reference to OpenOMCI Agent
+ :param device_id: (str) ONU Device ID
+ :param adapter_agent: (AdapterAgent) Adapter agent for ONU
+ :param custom_me_map: (dict) Additional/updated ME to add to class map
+ :param mib_db: (MibDbApi) MIB Database reference
+ :param alarm_db: (MibDbApi) Alarm Table/Database reference
+ :param support_classes: (dict) State machines and tasks for this ONU
+ """
+ self.log = structlog.get_logger(device_id=device_id)
+
+ self._started = False
+ self._omci_agent = omci_agent # OMCI AdapterAgent
+ self._device_id = device_id # ONU Device ID
+ self._adapter_agent = adapter_agent
+ self._runner = TaskRunner(device_id, clock=clock) # OMCI_CC Task runner
+ self._deferred = None
+ # self._img_download_deferred = None # deferred of image file download from server
+ self._omci_upgrade_deferred = None # deferred of ONU OMCI upgrading procedure
+ self._omci_activate_deferred = None # deferred of ONU OMCI Softwre Image Activate
+ self._img_deferred = None # deferred returned to caller of do_onu_software_download
+ self._first_in_sync = False
+ self._first_capabilities = False
+ self._timestamp = None
+ # self._image_download = None # (voltha_pb2.ImageDownload)
+ self.reactor = clock if clock is not None else reactor
+
+ # OMCI related databases are on a per-agent basis. State machines and tasks
+ # are per ONU Vendor
+ #
+ self._support_classes = support_classes
+ self._configuration = None
+
+ try:
+ # MIB Synchronization state machine
+ self._mib_db_in_sync = False
+ mib_synchronizer_info = support_classes.get('mib-synchronizer')
+ advertise = mib_synchronizer_info['advertise-events']
+ self._mib_sync_sm = mib_synchronizer_info['state-machine'](self._omci_agent,
+ device_id,
+ mib_synchronizer_info['tasks'],
+ mib_db,
+ advertise_events=advertise)
+ # ONU OMCI Capabilities state machine
+ capabilities_info = support_classes.get('omci-capabilities')
+ advertise = capabilities_info['advertise-events']
+ self._capabilities_sm = capabilities_info['state-machine'](self._omci_agent,
+ device_id,
+ capabilities_info['tasks'],
+ advertise_events=advertise)
+ # ONU Performance Monitoring Intervals state machine
+ interval_info = support_classes.get('performance-intervals')
+ advertise = interval_info['advertise-events']
+ self._pm_intervals_sm = interval_info['state-machine'](self._omci_agent, device_id,
+ interval_info['tasks'],
+ advertise_events=advertise)
+
+ # ONU ALARM Synchronization state machine
+ self._alarm_db_in_sync = False
+ alarm_synchronizer_info = support_classes.get('alarm-synchronizer')
+ advertise = alarm_synchronizer_info['advertise-events']
+ self._alarm_sync_sm = alarm_synchronizer_info['state-machine'](self._omci_agent,
+ device_id,
+ alarm_synchronizer_info['tasks'],
+ alarm_db,
+ advertise_events=advertise)
+ # State machine of downloading image file from server
+ downloader_info = support_classes.get('image_downloader')
+ image_upgrader_info = support_classes.get('image_upgrader')
+ # image_activate_info = support_classes.get('image_activator')
+ advertise = downloader_info['advertise-event']
+ # self._img_download_sm = downloader_info['state-machine'](self._omci_agent, device_id,
+ # downloader_info['tasks'],
+ # advertise_events=advertise)
+ self._image_agent = ImageAgent(self._omci_agent, device_id,
+ downloader_info['state-machine'], downloader_info['tasks'],
+ image_upgrader_info['state-machine'], image_upgrader_info['tasks'],
+ # image_activate_info['state-machine'],
+ advertise_events=advertise, clock=clock)
+
+ # self._omci_upgrade_sm = image_upgrader_info['state-machine'](device_id, advertise_events=advertise)
+
+ except Exception as e:
+ self.log.exception('state-machine-create-failed', e=e)
+ raise
+
+ # Put state machines in the order you wish to start them
+
+ self._state_machines = []
+ self._on_start_state_machines = [ # Run when 'start()' called
+ self._mib_sync_sm,
+ self._capabilities_sm,
+ ]
+ self._on_sync_state_machines = [ # Run after first in_sync event
+ self._alarm_sync_sm,
+ ]
+ self._on_capabilities_state_machines = [ # Run after first capabilities events
+ self._pm_intervals_sm
+ ]
+ self._custom_me_map = custom_me_map
+ self._me_map = omci_entities.entity_id_to_class_map.copy()
+
+ if custom_me_map is not None:
+ self._me_map.update(custom_me_map)
+
+ self.event_bus = EventBusClient()
+
+ # Create OMCI communications channel
+ self._omci_cc = OMCI_CC(adapter_agent, self.device_id, self._me_map, clock=clock)
+
+ @staticmethod
+ def event_bus_topic(device_id, event):
+ """
+ Get the topic name for a given event for this ONU Device
+ :param device_id: (str) ONU Device ID
+ :param event: (OnuDeviceEvents) Type of event
+ :return: (str) Topic string
+ """
+ assert event in OnuDeviceEvents, \
+ 'Event {} is not an ONU Device Event'.format(event.name)
+ return 'omci-device:{}:{}'.format(device_id, event.name)
+
+ @property
+ def device_id(self):
+ return self._device_id
+
+ @property
+ def omci_cc(self):
+ return self._omci_cc
+
+ @property
+ def adapter_agent(self):
+ return self._adapter_agent
+
+ @property
+ def task_runner(self):
+ return self._runner
+
+ @property
+ def mib_synchronizer(self):
+ """
+ Reference to the OpenOMCI MIB Synchronization state machine for this ONU
+ """
+ return self._mib_sync_sm
+
+ @property
+ def omci_capabilities(self):
+ """
+ Reference to the OpenOMCI OMCI Capabilities state machine for this ONU
+ """
+ return self._capabilities_sm
+
+ @property
+ def pm_intervals_state_machine(self):
+ """
+ Reference to the OpenOMCI PM Intervals state machine for this ONU
+ """
+ return self._pm_intervals_sm
+
+ def set_pm_config(self, pm_config):
+ """
+ Set PM interval configuration
+
+ :param pm_config: (OnuPmIntervalMetrics) PM Interval configuration
+ """
+ self._pm_intervals_sm.set_pm_config(pm_config)
+
+ @property
+ def timestamp(self):
+ """Pollable Metrics last collected timestamp"""
+ return self._timestamp
+
+ @timestamp.setter
+ def timestamp(self, value):
+ self._timestamp = value
+
+ @property
+ def alarm_synchronizer(self):
+ """
+ Reference to the OpenOMCI Alarm Synchronization state machine for this ONU
+ """
+ return self._alarm_sync_sm
+
+ @property
+ def active(self):
+ """
+ Is the ONU device currently active/running
+ """
+ return self._started
+
+ @property
+ def custom_me_map(self):
+ """ Vendor-specific Managed Entity Map for this vendor's device"""
+ return self._custom_me_map
+
+ @property
+ def me_map(self):
+ """ Combined ME and Vendor-specific Managed Entity Map for this device"""
+ return self._me_map
+
+ def _cancel_deferred(self):
+ d, self._deferred = self._deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ @property
+ def mib_db_in_sync(self):
+ return self._mib_db_in_sync
+
+ @mib_db_in_sync.setter
+ def mib_db_in_sync(self, value):
+ if self._mib_db_in_sync != value:
+ # Save value
+ self._mib_db_in_sync = value
+
+ # Start up other state machines if needed
+ if self._first_in_sync:
+ self.first_in_sync_event()
+
+ # Notify any event listeners
+ topic = OnuDeviceEntry.event_bus_topic(self.device_id,
+ OnuDeviceEvents.MibDatabaseSyncEvent)
+ msg = {
+ IN_SYNC_KEY: self._mib_db_in_sync,
+ LAST_IN_SYNC_KEY: self.mib_synchronizer.last_mib_db_sync
+ }
+ self.event_bus.publish(topic=topic, msg=msg)
+
+ @property
+ def alarm_db_in_sync(self):
+ return self._alarm_db_in_sync
+
+ @alarm_db_in_sync.setter
+ def alarm_db_in_sync(self, value):
+ if self._alarm_db_in_sync != value:
+ # Save value
+ self._alarm_db_in_sync = value
+
+ # Start up other state machines if needed
+ if self._first_in_sync:
+ self.first_in_sync_event()
+
+ # Notify any event listeners
+ topic = OnuDeviceEntry.event_bus_topic(self.device_id,
+ OnuDeviceEvents.AlarmDatabaseSyncEvent)
+ msg = {
+ IN_SYNC_KEY: self._alarm_db_in_sync
+ }
+ self.event_bus.publish(topic=topic, msg=msg)
+
+ @property
+ def configuration(self):
+ """
+ Get the OMCI Configuration object for this ONU. This is a class that provides some
+ common database access functions for ONU capabilities and read-only configuration values.
+
+ :return: (OnuConfiguration)
+ """
+ return self._configuration
+
+ @property
+ def image_agent(self):
+ return self._image_agent
+
+ # @property
+ # def image_download(self):
+ # return self._image_download
+
+ def start(self):
+ """
+ Start the ONU Device Entry state machines
+ """
+ self.log.debug('OnuDeviceEntry.start', previous=self._started)
+ if self._started:
+ return
+
+ self._started = True
+ self._omci_cc.enabled = True
+ self._first_in_sync = True
+ self._first_capabilities = True
+ self._runner.start()
+ self._configuration = OnuConfiguration(self._omci_agent, self._device_id)
+
+ # Start MIB Sync and other state machines that can run before the first
+ # MIB Synchronization event occurs. Start 'later' so that any
+ # ONU Device, OMCI DB, OMCI Agent, and others are fully started before
+ # performing the start.
+
+ self._state_machines = []
+
+ def start_state_machines(machines):
+ for sm in machines:
+ self._state_machines.append(sm)
+ sm.start()
+
+ self._deferred = reactor.callLater(0, start_state_machines,
+ self._on_start_state_machines)
+ # Notify any event listeners
+ self._publish_device_status_event()
+
+ def stop(self):
+ """
+ Stop the ONU Device Entry state machines
+ """
+ if not self._started:
+ return
+
+ self._started = False
+ self._cancel_deferred()
+ self._omci_cc.enabled = False
+
+ # Halt MIB Sync and other state machines
+ for sm in self._state_machines:
+ sm.stop()
+
+ self._state_machines = []
+
+ # Stop task runner
+ self._runner.stop()
+
+ # Notify any event listeners
+ self._publish_device_status_event()
+
+ def first_in_sync_event(self):
+ """
+ This event is called on the first MIB synchronization event after
+ OpenOMCI has been started. It is responsible for starting any
+ other state machine and to initiate an ONU Capabilities report
+ """
+ if self._first_in_sync:
+ self._first_in_sync = False
+
+ # Start up the ONU Capabilities task
+ self._configuration.reset()
+
+ # Insure that the ONU-G Administrative lock is disabled
+ def failure(reason):
+ self.log.error('disable-admin-state-lock', reason=reason)
+
+ frame = OntGFrame(attributes={'administrative_state': 0}).set()
+ task = OmciModifyRequest(self._omci_agent, self.device_id, frame)
+ self.task_runner.queue_task(task).addErrback(failure)
+
+ # Start up any other remaining OpenOMCI state machines
+ def start_state_machines(machines):
+ for sm in machines:
+ self._state_machines.append(sm)
+ reactor.callLater(0, sm.start)
+
+ self._deferred = reactor.callLater(0, start_state_machines,
+ self._on_sync_state_machines)
+
+ # if an ongoing upgrading is not accomplished, restart it
+ if self._img_deferred is not None:
+ self._image_agent.onu_bootup()
+
+ def first_in_capabilities_event(self):
+ """
+ This event is called on the first capabilities event after
+ OpenOMCI has been started. It is responsible for starting any
+ other state machine. These are often state machines that have tasks
+ that are dependent upon knowing if various MEs are supported
+ """
+ if self._first_capabilities:
+ self._first_capabilities = False
+
+ # Start up any other remaining OpenOMCI state machines
+ def start_state_machines(machines):
+ for sm in machines:
+ self._state_machines.append(sm)
+ reactor.callLater(0, sm.start)
+
+ self._deferred = reactor.callLater(0, start_state_machines,
+ self._on_capabilities_state_machines)
+
+ # def __on_omci_download_success(self, image_download):
+ # self.log.debug("__on_omci_download_success", image=image_download)
+ # self._omci_upgrade_deferred = None
+ # # self._ret_deferred = None
+ # self._omci_activate_deferred = self._image_agent.activate_onu_image(image_download.name)
+ # self._omci_activate_deferred.addCallbacks(self.__on_omci_image_activate_success,
+ # self.__on_omci_image_activate_fail, errbackArgs=(image_name,))
+ # return image_name
+
+ # def __on_omci_download_fail(self, fail, image_name):
+ # self.log.debug("__on_omci_download_fail", failure=fail, image_name=image_name)
+ # self.reactor.callLater(0, self._img_deferred.errback, fail)
+ # self._omci_upgrade_deferred = None
+ # self._img_deferred = None
+
+ def __on_omci_image_activate_success(self, image_name):
+ self.log.debug("__on_omci_image_activate_success", image_name=image_name)
+ self._omci_activate_deferred = None
+ self._img_deferred.callback(image_name)
+ self._img_deferred = None
+ return image_name
+
+ def __on_omci_image_activate_fail(self, fail, image_name):
+ self.log.debug("__on_omci_image_activate_fail", faile=fail, image_name=image_name)
+ self._omci_activate_deferred = None
+ self._img_deferred.errback(fail)
+ self._img_deferred = None
+
+ def _publish_device_status_event(self):
+ """
+ Publish the ONU Device start/start status.
+ """
+ topic = OnuDeviceEntry.event_bus_topic(self.device_id,
+ OnuDeviceEvents.DeviceStatusEvent)
+ msg = {ACTIVE_KEY: self._started}
+ self.event_bus.publish(topic=topic, msg=msg)
+
+ def publish_omci_capabilities_event(self):
+ """
+ Publish the ONU Device start/start status.
+ """
+ if self.first_in_capabilities_event:
+ self.first_in_capabilities_event()
+
+ topic = OnuDeviceEntry.event_bus_topic(self.device_id,
+ OnuDeviceEvents.OmciCapabilitiesEvent)
+ msg = {
+ SUPPORTED_MESSAGE_ENTITY_KEY: self.omci_capabilities.supported_managed_entities,
+ SUPPORTED_MESSAGE_TYPES_KEY: self.omci_capabilities.supported_message_types
+ }
+ self.event_bus.publish(topic=topic, msg=msg)
+
+ def delete(self):
+ """
+ Stop the ONU Device's state machine and remove the ONU, and any related
+ OMCI state information from the OpenOMCI Framework
+ """
+ self.stop()
+ self.mib_synchronizer.delete()
+
+ # OpenOMCI cleanup
+ if self._omci_agent is not None:
+ self._omci_agent.remove_device(self._device_id, cleanup=True)
+
+ def query_mib(self, class_id=None, instance_id=None, attributes=None):
+ """
+ Get MIB database information.
+
+ This method can be used to request information from the database to the detailed
+ level requested
+
+ :param class_id: (int) Managed Entity class ID
+ :param instance_id: (int) Managed Entity instance
+ :param attributes: (list or str) Managed Entity instance's attributes
+
+ :return: (dict) The value(s) requested. If class/inst/attribute is
+ not found, an empty dictionary is returned
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ self.log.debug('query', class_id=class_id, instance_id=instance_id,
+ attributes=attributes)
+
+ return self.mib_synchronizer.query_mib(class_id=class_id, instance_id=instance_id,
+ attributes=attributes)
+
+ def query_mib_single_attribute(self, class_id, instance_id, attribute):
+ """
+ Get MIB database information for a single specific attribute
+
+ This method can be used to request information from the database to the detailed
+ level requested
+
+ :param class_id: (int) Managed Entity class ID
+ :param instance_id: (int) Managed Entity instance
+ :param attribute: (str) Managed Entity instance's attribute
+
+ :return: (varies) The value requested. If class/inst/attribute is
+ not found, None is returned
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ self.log.debug('query-single', class_id=class_id,
+ instance_id=instance_id, attributes=attribute)
+ assert isinstance(attribute, basestring), \
+ 'Only a single attribute value can be retrieved'
+
+ entry = self.mib_synchronizer.query_mib(class_id=class_id,
+ instance_id=instance_id,
+ attributes=attribute)
+
+ return entry[attribute] if attribute in entry else None
+
+ def query_alarm_table(self, class_id=None, instance_id=None):
+ """
+ Get Alarm information
+
+ This method can be used to request information from the alarm database to
+ the detailed level requested
+
+ :param class_id: (int) Managed Entity class ID
+ :param instance_id: (int) Managed Entity instance
+
+ :return: (dict) The value(s) requested. If class/inst/attribute is
+ not found, an empty dictionary is returned
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ self.log.debug('query', class_id=class_id, instance_id=instance_id)
+
+ return self.alarm_synchronizer.query_mib(class_id=class_id, instance_id=instance_id)
+
+ def reboot(self,
+ flags=RebootFlags.Reboot_Unconditionally,
+ timeout=OmciRebootRequest.DEFAULT_REBOOT_TIMEOUT):
+ """
+ Request a reboot of the ONU
+
+ :param flags: (RebootFlags) Reboot condition
+ :param timeout: (int) Reboot task priority
+ :return: (deferred) Fires upon completion or error
+ """
+ assert self.active, 'This device is not active'
+
+ return self.task_runner.queue_task(OmciRebootRequest(self._omci_agent,
+ self.device_id,
+ flags=flags,
+ timeout=timeout))
+
+ # def get_imagefile(self, local_name, local_dir, remote_url=None):
+ # """
+ # Return a Deferred that will be triggered if the file is locally available
+ # or downloaded successfully
+ # """
+ # self.log.info('start download from {}'.format(remote_url))
+
+ # # for debug purpose, start runner here to queue downloading task
+ # # self._runner.start()
+
+ # return self._image_agent.get_image(self._image_download)
+
+ def do_onu_software_download(self, image_dnld):
+ """
+ image_dnld: (ImageDownload)
+ : Return a Deferred that will be triggered when upgrading results in success or failure
+ """
+ self.log.debug('do_onu_software_download')
+ image_download = deepcopy(image_dnld)
+ # self._img_download_deferred = self._image_agent.get_image(self._image_download)
+ # self._img_download_deferred.addCallbacks(self.__on_download_success, self.__on_download_fail, errbackArgs=(self._image_download,))
+ # self._ret_deferred = defer.Deferred()
+ # return self._ret_deferred
+ return self._image_agent.get_image(image_download)
+
+ # def do_onu_software_switch(self):
+ def do_onu_image_activate(self, image_dnld_name):
+ """
+ Return a Deferred that will be triggered when switching software image results in success or failure
+ """
+ if self._img_deferred is None:
+ self.log.debug('do_onu_image_activate')
+ self._img_deferred = defer.Deferred()
+ self._omci_upgrade_deferred = self._image_agent.onu_omci_download(image_dnld_name)
+ self._omci_upgrade_deferred.addCallbacks(self.__on_omci_image_activate_success,
+ self.__on_omci_image_activate_fail, errbackArgs=(image_dnld_name,))
+ return self._img_deferred
+
+ def cancel_onu_software_download(self, image_name):
+ self.log.debug('cancel_onu_software_download')
+ self._image_agent.cancel_download_image(image_name)
+ self._image_agent.cancel_upgrade_onu()
+ if self._img_deferred and not self._img_deferred.called:
+ self._img_deferred.cancel()
+ self._img_deferred = None
+ # self._image_download = None
+
+ def get_image_download_status(self, image_name):
+ return self._image_agent.get_image_status(image_name)
+
diff --git a/python/adapters/extensions/omci/openomci_agent.py b/python/adapters/extensions/omci/openomci_agent.py
new file mode 100644
index 0000000..98ba684
--- /dev/null
+++ b/python/adapters/extensions/omci/openomci_agent.py
@@ -0,0 +1,283 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import structlog
+from twisted.internet import reactor
+from voltha.extensions.omci.database.mib_db_dict import MibDbVolatileDict
+from voltha.extensions.omci.database.mib_db_ext import MibDbExternal
+from voltha.extensions.omci.state_machines.mib_sync import MibSynchronizer
+from voltha.extensions.omci.tasks.mib_upload import MibUploadTask
+from voltha.extensions.omci.tasks.get_mds_task import GetMdsTask
+from voltha.extensions.omci.tasks.mib_resync_task import MibResyncTask
+from voltha.extensions.omci.tasks.mib_reconcile_task import MibReconcileTask
+from voltha.extensions.omci.tasks.sync_time_task import SyncTimeTask
+from voltha.extensions.omci.state_machines.alarm_sync import AlarmSynchronizer
+from voltha.extensions.omci.tasks.alarm_resync_task import AlarmResyncTask
+from voltha.extensions.omci.database.alarm_db_ext import AlarmDbExternal
+from voltha.extensions.omci.tasks.interval_data_task import IntervalDataTask
+from voltha.extensions.omci.onu_device_entry import OnuDeviceEntry
+from voltha.extensions.omci.state_machines.omci_onu_capabilities import OnuOmciCapabilities
+from voltha.extensions.omci.tasks.onu_capabilities_task import OnuCapabilitiesTask
+from voltha.extensions.omci.state_machines.performance_intervals import PerformanceIntervals
+from voltha.extensions.omci.tasks.omci_create_pm_task import OmciCreatePMRequest
+from voltha.extensions.omci.tasks.omci_delete_pm_task import OmciDeletePMRequest
+from voltha.extensions.omci.state_machines.image_agent import ImageDownloadeSTM, OmciSoftwareImageDownloadSTM
+from voltha.extensions.omci.tasks.file_download_task import FileDownloadTask
+from voltha.extensions.omci.tasks.omci_sw_image_upgrade_task import OmciSwImageUpgradeTask
+
+OpenOmciAgentDefaults = {
+ 'mib-synchronizer': {
+ 'state-machine': MibSynchronizer, # Implements the MIB synchronization state machine
+ 'database': MibDbVolatileDict, # Implements volatile ME MIB database
+ #'database': MibDbExternal, # Implements persistent ME MIB database
+ 'advertise-events': True, # Advertise events on OpenOMCI event bus
+ 'tasks': {
+ 'mib-upload': MibUploadTask,
+ 'get-mds': GetMdsTask,
+ 'mib-audit': GetMdsTask,
+ 'mib-resync': MibResyncTask,
+ 'mib-reconcile': MibReconcileTask
+ }
+ },
+ 'omci-capabilities': {
+ 'state-machine': OnuOmciCapabilities, # Implements OMCI capabilities state machine
+ 'advertise-events': False, # Advertise events on OpenOMCI event bus
+ 'tasks': {
+ 'get-capabilities': OnuCapabilitiesTask # Get supported ME and Commands
+ }
+ },
+ 'performance-intervals': {
+ 'state-machine': PerformanceIntervals, # Implements PM Intervals State machine
+ 'advertise-events': False, # Advertise events on OpenOMCI event bus
+ 'tasks': {
+ 'sync-time': SyncTimeTask,
+ 'collect-data': IntervalDataTask,
+ 'create-pm': OmciCreatePMRequest,
+ 'delete-pm': OmciDeletePMRequest,
+ },
+ },
+ 'alarm-synchronizer': {
+ 'state-machine': AlarmSynchronizer, # Implements the Alarm sync state machine
+ 'database': AlarmDbExternal, # For any State storage needs
+ 'advertise-events': True, # Advertise events on OpenOMCI event bus
+ 'tasks': {
+ 'alarm-resync': AlarmResyncTask
+ }
+ },
+ 'image_downloader': {
+ 'state-machine': ImageDownloadeSTM,
+ 'advertise-event': True,
+ 'tasks': {
+ 'download-file': FileDownloadTask
+ }
+ },
+ 'image_upgrader': {
+ 'state-machine': OmciSoftwareImageDownloadSTM,
+ 'advertise-event': True,
+ 'tasks': {
+ 'omci_upgrade_task': OmciSwImageUpgradeTask
+ }
+ }
+ # 'image_activator': {
+ # 'state-machine': OmciSoftwareImageActivateSTM,
+ # 'advertise-event': True,
+ # }
+}
+
+
+class OpenOMCIAgent(object):
+ """
+ OpenOMCI for VOLTHA
+
+ This will become the primary interface into OpenOMCI for ONU Device Adapters
+ in VOLTHA v1.3 sprint 3 time frame.
+ """
+ def __init__(self, core, support_classes=OpenOmciAgentDefaults, clock=None):
+ """
+ Class initializer
+
+ :param core: (VolthaCore) VOLTHA Core
+ :param support_classes: (Dict) Classes to support OMCI
+ """
+ self.log = structlog.get_logger()
+ self._core = core
+ self.reactor = clock if clock is not None else reactor
+ self._started = False
+ self._devices = dict() # device-id -> DeviceEntry
+ self._event_bus = None
+
+ # OMCI related databases are on a per-agent basis. State machines and tasks
+ # are per ONU Vendore
+ #
+ # MIB Synchronization Database
+ self._mib_db = None
+ self._mib_database_cls = support_classes['mib-synchronizer']['database']
+
+ # Alarm Synchronization Database
+ self._alarm_db = None
+ self._alarm_database_cls = support_classes['alarm-synchronizer']['database']
+
+ @property
+ def core(self):
+ """ Return a reference to the VOLTHA Core component"""
+ return self._core
+
+ @property
+ def database_class(self):
+ return self._mib_database_cls
+
+ # TODO: Need to deprecate this. ImageAgent is using it and should not
+ @property
+ def database(self):
+ return self._mib_db
+
+ def start(self):
+ """
+ Start OpenOMCI
+ """
+ if self._started:
+ return
+
+ self.log.debug('OpenOMCIAgent.start')
+ self._started = True
+
+ try:
+ # Create all databases as needed. This should be done before
+ # State machines are started for the first time
+
+ if self._mib_db is None:
+ self._mib_db = self._mib_database_cls(self)
+
+ if self._alarm_db is None:
+ self._alarm_db = self._alarm_database_cls(self)
+
+ # Start/restore databases
+
+ self._mib_db.start()
+ self._alarm_db.start()
+
+ for device in self._devices.itervalues():
+ device.start()
+
+ except Exception as e:
+ self.log.exception('startup', e=e)
+
+ def stop(self):
+ """
+ Shutdown OpenOMCI
+ """
+ if not self._started:
+ return
+
+ self.log.debug('stop')
+ self._started = False
+ self._event_bus = None
+
+ # ONUs OMCI shutdown
+ for device in self._devices.itervalues():
+ device.stop()
+
+ # DB shutdown
+ self._mib_db.stop()
+ self._alarm_db.stop()
+
+ def mk_event_bus(self):
+ """ Get the event bus for OpenOMCI"""
+ if self._event_bus is None:
+ from voltha.extensions.omci.openomci_event_bus import OpenOmciEventBus
+ self._event_bus = OpenOmciEventBus()
+
+ return self._event_bus
+
+ def advertise(self, event_type, data):
+ """
+ Advertise an OpenOMCU event on the kafka bus
+ :param event_type: (int) Event Type (enumberation from OpenOMCI protobuf definitions)
+ :param data: (Message, dict, ...) Associated data (will be convert to a string)
+ """
+ if self._started:
+ try:
+ self.mk_event_bus().advertise(event_type, data)
+
+ except Exception as e:
+ self.log.exception('advertise-failure', e=e)
+
+ def add_device(self, device_id, adapter_agent, custom_me_map=None,
+ support_classes=OpenOmciAgentDefaults):
+ """
+ Add a new ONU to be managed.
+
+ To provide vendor-specific or custom Managed Entities, create your own Entity
+ ID to class mapping dictionary.
+
+ Since ONU devices can be added at any time (even during Device Handler
+ startup), the ONU device handler is responsible for calling start()/stop()
+ for this object.
+
+ :param device_id: (str) Device ID of ONU to add
+ :param adapter_agent: (AdapterAgent) Adapter agent for ONU
+ :param custom_me_map: (dict) Additional/updated ME to add to class map
+ :param support_classes: (dict) State machines and tasks for this ONU
+
+ :return: (OnuDeviceEntry) The ONU device
+ """
+ self.log.debug('OpenOMCIAgent.add-device', device_id=device_id)
+
+ device = self._devices.get(device_id)
+
+ if device is None:
+ device = OnuDeviceEntry(self, device_id, adapter_agent, custom_me_map,
+ self._mib_db, self._alarm_db, support_classes, clock=self.reactor)
+
+ self._devices[device_id] = device
+
+ return device
+
+ def remove_device(self, device_id, cleanup=False):
+ """
+ Remove a managed ONU
+
+ :param device_id: (str) Device ID of ONU to remove
+ :param cleanup: (bool) If true, scrub any state related information
+ """
+ self.log.debug('remove-device', device_id=device_id, cleanup=cleanup)
+
+ device = self._devices.get(device_id)
+
+ if device is not None:
+ device.stop()
+
+ if cleanup:
+ del self._devices[device_id]
+
+ def device_ids(self):
+ """
+ Get an immutable set of device IDs managed by this OpenOMCI instance
+
+ :return: (frozenset) Set of device IDs (str)
+ """
+ return frozenset(self._devices.keys())
+
+ def get_device(self, device_id):
+ """
+ Get ONU device entry. For external (non-OpenOMCI users) the ONU Device
+ returned should be used for read-only activity.
+
+ :param device_id: (str) ONU Device ID
+
+ :return: (OnuDeviceEntry) ONU Device entry
+ :raises KeyError: If device does not exist
+ """
+ return self._devices[device_id]
diff --git a/python/adapters/extensions/omci/openomci_event_bus.py b/python/adapters/extensions/omci/openomci_event_bus.py
new file mode 100644
index 0000000..5c67865
--- /dev/null
+++ b/python/adapters/extensions/omci/openomci_event_bus.py
@@ -0,0 +1,54 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from google.protobuf.json_format import MessageToDict
+from google.protobuf.message import Message
+from simplejson import dumps
+from common.event_bus import EventBusClient
+from voltha.protos.omci_mib_db_pb2 import OpenOmciEvent
+from voltha.protos.omci_alarm_db_pb2 import AlarmOpenOmciEvent
+from common.utils.json_format import MessageToDict
+
+
+class OpenOmciEventBus(object):
+ """ Event bus for publishing OpenOMCI related events. """
+ __slots__ = (
+ '_event_bus_client', # The event bus client used to publish events.
+ '_topic' # the topic to publish to
+ )
+
+ def __init__(self):
+ self._event_bus_client = EventBusClient()
+ self._topic = 'openomci-events'
+
+ def message_to_dict(m):
+ return MessageToDict(m, True, True, False)
+
+ def advertise(self, event_type, data):
+ if isinstance(data, Message):
+ msg = dumps(MessageToDict(data, True, True))
+ elif isinstance(data, dict):
+ msg = dumps(data)
+ else:
+ msg = str(data)
+
+ event_func = AlarmOpenOmciEvent if 'AlarmSynchronizer' in msg \
+ else OpenOmciEvent
+ event = event_func(
+ type=event_type,
+ data=msg
+ )
+
+ self._event_bus_client.publish(self._topic, event)
diff --git a/python/adapters/extensions/omci/state_machines/__init__.py b/python/adapters/extensions/omci/state_machines/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/omci/state_machines/alarm_sync.py b/python/adapters/extensions/omci/state_machines/alarm_sync.py
new file mode 100644
index 0000000..c7b7d64
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/alarm_sync.py
@@ -0,0 +1,670 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import structlog
+from datetime import datetime
+from transitions import Machine
+from twisted.internet import reactor
+from voltha.extensions.omci.omci_defs import ReasonCodes, EntityOperations
+from voltha.extensions.omci.omci_cc import OmciCCRxEvents, OMCI_CC, RX_RESPONSE_KEY
+from voltha.extensions.omci.omci_messages import OmciGetAllAlarmsResponse
+from voltha.extensions.omci.omci_frame import OmciFrame
+from voltha.extensions.omci.database.alarm_db_ext import AlarmDbExternal
+from voltha.extensions.omci.database.mib_db_api import ATTRIBUTES_KEY
+from voltha.extensions.omci.omci_entities import CircuitPack, PptpEthernetUni, OntG, AniG
+
+from common.event_bus import EventBusClient
+from voltha.protos.omci_alarm_db_pb2 import AlarmOpenOmciEventType
+
+RxEvent = OmciCCRxEvents
+RC = ReasonCodes
+OP = EntityOperations
+
+
+class AlarmSynchronizer(object):
+ """
+ OpenOMCI Alarm Synchronizer state machine
+ """
+ DEFAULT_STATES = ['disabled', 'starting', 'auditing', 'in_sync']
+
+ DEFAULT_TRANSITIONS = [
+ {'trigger': 'start', 'source': 'disabled', 'dest': 'starting'},
+
+ {'trigger': 'audit_alarm', 'source': 'starting', 'dest': 'auditing'},
+ {'trigger': 'sync_alarm', 'source': 'starting', 'dest': 'in_sync'},
+
+ {'trigger': 'success', 'source': 'auditing', 'dest': 'in_sync'},
+ {'trigger': 'audit_alarm', 'source': 'auditing', 'dest': 'auditing'},
+ {'trigger': 'failure', 'source': 'auditing', 'dest': 'auditing'},
+
+ {'trigger': 'audit_alarm', 'source': 'in_sync', 'dest': 'auditing'},
+
+ # Do wildcard 'stop' trigger last so it covers all previous states
+ {'trigger': 'stop', 'source': '*', 'dest': 'disabled'},
+ ]
+ DEFAULT_TIMEOUT_RETRY = 15 # Seconds to delay after task failure/timeout
+ DEFAULT_AUDIT_DELAY = 180 # Periodic tick to audit the ONU's alarm table
+
+ def __init__(self, agent, device_id, alarm_sync_tasks, db,
+ advertise_events=False,
+ states=DEFAULT_STATES,
+ transitions=DEFAULT_TRANSITIONS,
+ initial_state='disabled',
+ timeout_delay=DEFAULT_TIMEOUT_RETRY,
+ audit_delay=DEFAULT_AUDIT_DELAY):
+ """
+ Class initialization
+
+ :param agent: (OpenOmciAgent) Agent
+ :param device_id: (str) ONU Device ID
+ :param db: (MibDbApi) MIB/Alarm Database
+ :param advertise_events: (bool) Advertise events on OpenOMCI Event Bus
+ :param alarm_sync_tasks: (dict) Tasks to run
+ :param states: (list) List of valid states
+ :param transitions: (dict) Dictionary of triggers and state changes
+ :param initial_state: (str) Initial state machine state
+ :param timeout_delay: (int/float) Number of seconds after a timeout to attempt
+ a retry (goes back to starting state)
+ :param audit_delay: (int) Seconds between Alarm audits while in sync. Set to
+ zero to disable audit. An operator can request
+ an audit manually by calling 'self.audit_alarm'
+ """
+
+ self.log = structlog.get_logger(device_id=device_id)
+
+ self._agent = agent
+ self._device_id = device_id
+ self._device = None
+ self._database = db
+ self._timeout_delay = timeout_delay
+ self._audit_delay = audit_delay
+ self._resync_task = alarm_sync_tasks['alarm-resync']
+ self._advertise_events = advertise_events
+ self._alarm_manager = None
+ self._onu_id = None
+ self._uni_ports = list()
+ self._ani_ports = list()
+
+ self._deferred = None
+ self._current_task = None
+ self._task_deferred = None
+ self._last_alarm_sequence_value = 0
+ self._device_in_db = False
+
+ self._event_bus = EventBusClient()
+ self._omci_cc_subscriptions = { # RxEvent.enum -> Subscription Object
+ RxEvent.Get_ALARM_Get: None,
+ RxEvent.Alarm_Notification: None
+ }
+ self._omci_cc_sub_mapping = {
+ RxEvent.Get_ALARM_Get: self.on_alarm_update_response,
+ RxEvent.Alarm_Notification: self.on_alarm_notification
+ }
+
+ # Statistics and attributes
+ # TODO: add any others if it will support problem diagnosis
+
+ # Set up state machine to manage states
+ self.machine = Machine(model=self, states=states,
+ transitions=transitions,
+ initial=initial_state,
+ queued=True,
+ name='{}-{}'.format(self.__class__.__name__,
+ device_id))
+
+ def _cancel_deferred(self):
+ d1, self._deferred = self._deferred, None
+ d2, self._task_deferred = self._task_deferred, None
+
+ for d in [d1, d1]:
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def __str__(self):
+ return 'Alarm Synchronizer: Device ID: {}, State:{}'.format(self._device_id, self.state)
+
+ def delete(self):
+ """
+ Cleanup any state information
+ """
+ self.stop()
+ db, self._database = self._database, None
+
+ if db is not None:
+ db.remove(self._device_id)
+
+ @property
+ def device_id(self):
+ return self._device_id
+
+ @property
+ def last_alarm_sequence(self):
+ return self._last_alarm_sequence_value
+
+ def reset_alarm_sequence(self):
+ if self._last_alarm_sequence_value != 0:
+ self._last_alarm_sequence_value = 0
+
+ def increment_alarm_sequence(self):
+ self._last_alarm_sequence_value += 1
+ if self._last_alarm_sequence_value > 255:
+ self._last_alarm_sequence_value = 1
+
+ @property
+ def advertise_events(self):
+ return self._advertise_events
+
+ @advertise_events.setter
+ def advertise_events(self, value):
+ if not isinstance(value, bool):
+ raise TypeError('Advertise event is a boolean')
+ self._advertise_events = value
+
+ def advertise(self, event, info):
+ """Advertise an event on the OpenOMCI event bus"""
+ if self._advertise_events:
+ self._agent.advertise(event,
+ {
+ 'state-machine': self.machine.name,
+ 'info': info,
+ 'time': str(datetime.utcnow())
+ })
+
+ def set_alarm_params(self, mgr=None, onu_id=None, uni_ports=None, ani_ports=None):
+ if mgr is not None:
+ self._alarm_manager = mgr
+
+ if onu_id is not None:
+ self._onu_id = onu_id
+
+ if uni_ports is not None:
+ assert isinstance(uni_ports, list)
+ self._uni_ports = uni_ports
+
+ if ani_ports is not None:
+ assert isinstance(ani_ports, list)
+ self._ani_ports = ani_ports
+
+ def on_enter_disabled(self):
+ """
+ State machine is being stopped
+ """
+ self.advertise(AlarmOpenOmciEventType.state_change, self.state)
+
+ self._cancel_deferred()
+
+ task, self._current_task = self._current_task, None
+ if task is not None:
+ task.stop()
+
+ # Drop Response and Autonomous notification subscriptions
+ for event, sub in self._omci_cc_subscriptions.iteritems():
+ if sub is not None:
+ self._omci_cc_subscriptions[event] = None
+ self._device.omci_cc.event_bus.unsubscribe(sub)
+
+ def _seed_database(self):
+ if not self._device_in_db:
+ try:
+ try:
+ self._database.start()
+ self._database.add(self._device_id)
+ self.log.debug('seed-db-does-not-exist', device_id=self._device_id)
+
+ except KeyError:
+ # Device already is in database
+ self.log.debug('seed-db-exist', device_id=self._device_id)
+
+ self._device_in_db = True
+
+ except Exception as e:
+ self.log.exception('seed-database-failure', e=e)
+
+ def on_enter_starting(self):
+ """
+ Determine ONU status and start Alarm Synchronization tasks
+ """
+ self._device = self._agent.get_device(self._device_id)
+ self.advertise(AlarmOpenOmciEventType.state_change, self.state)
+
+ # Make sure root of external Alarm Database exists
+ self._seed_database()
+
+ # Set up Response and Autonomous notification subscriptions
+ try:
+ for event, sub in self._omci_cc_sub_mapping.iteritems():
+ if self._omci_cc_subscriptions[event] is None:
+ self._omci_cc_subscriptions[event] = \
+ self._device.omci_cc.event_bus.subscribe(
+ topic=OMCI_CC.event_bus_topic(self._device_id, event),
+ callback=sub)
+
+ except Exception as e:
+ self.log.exception('omci-cc-subscription-setup', e=e)
+
+ # Schedule first audit if enabled
+ if self._audit_delay > 0:
+ # Note using the shorter timeout delay here since this is the first
+ # audit after startup
+ self._deferred = reactor.callLater(self._timeout_delay, self.audit_alarm)
+ else:
+ self._deferred = reactor.callLater(0, self.sync_alarm)
+
+ def on_enter_in_sync(self):
+ """
+ Schedule a tick to occur to in the future to request an audit
+ """
+ self.advertise(AlarmOpenOmciEventType.state_change, self.state)
+
+ if self._audit_delay > 0:
+ # Note using the shorter timeout delay here since this is the first
+ # audit after startup
+ self._deferred = reactor.callLater(self._audit_delay, self.audit_alarm)
+
+ def on_enter_auditing(self):
+ """
+ Begin full Alarm data sync, Comparing the all alarms
+ """
+ self.advertise(AlarmOpenOmciEventType.state_change, self.state)
+
+ def success(results):
+ self.log.debug('alarm-diff-success')
+ self._current_task = None
+
+ # Any differences found between ONU and OpenOMCI Alarm tables?
+ if results is None:
+ self._device.alarm_db_in_sync = True
+ self._deferred = reactor.callLater(0, self.success)
+ else:
+ # Reconcile the alarm table and re-run audit
+ self.reconcile_alarm_table(results)
+ self._deferred = reactor.callLater(5, self.audit_alarm)
+
+ def failure(reason):
+ self.log.info('alarm-update-failure', reason=reason)
+ self._current_task = None
+ self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+
+ self._current_task = self._resync_task(self._agent, self._device_id)
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+
+ def reconcile_alarm_table(self, results):
+ self.log.debug('alarm-reconcile', state=self.state, results=results)
+
+ onu_only = results['onu-only']
+ olt_only = results['olt-only']
+ attr_diffs = results['attr-diffs']
+ onu_db = results['onu-db']
+ olt_db = results['olt-db']
+
+ if any(item is not None for item in (onu_only, olt_only, attr_diffs)):
+ self._device.alarm_db_in_sync = False
+
+ # Compare the differences. During upload, if there are no alarms at all,
+ # then the ONU alarm table retrieved may be empty (instead of MEs with all
+ # bits cleared) depending upon the ONU's OMCI Stack.
+
+ if onu_only is not None:
+ self.process_onu_only_diffs(onu_only, onu_db)
+
+ if olt_only is not None:
+ self.process_olt_only_diffs(olt_only)
+
+ if attr_diffs is not None:
+ self.process_attr_diffs(attr_diffs, olt_db, onu_db)
+
+ def process_onu_only_diffs(self, onu_only, onu_db):
+ """
+ ONU only alarms will typically occur when doing the first audit as our
+ database is clear and we are seeding the alarm table. Save the entries
+ and if any are set, we need to raise that alarm.
+
+ :param onu_only: (list) Tuples with [0]=class ID, [1]=entity ID
+ :param onu_db: (dict) ONU Alarm database from the alarm audit upload
+ """
+ for cid_eid in onu_only:
+ class_id = cid_eid[0]
+ entity_id = cid_eid[1]
+ try:
+ bitmap = onu_db[class_id][entity_id][ATTRIBUTES_KEY][AlarmDbExternal.ALARM_BITMAP_KEY]
+ self.process_alarm_data(class_id, entity_id, bitmap, -1)
+
+ except KeyError as e:
+ self.log.error('alarm-not-found', class_id=class_id, entity_id=entity_id, e=e)
+
+ def process_olt_only_diffs(self, olt_only):
+ """
+ OLT only alarms may occur if the alarm(s) are no longer active on the ONU
+ and the notification was missed. Process this by sending a cleared bitmap
+ for any alarm in the OLT database only
+
+ :param olt_only: (list) Tuples with [0]=class ID, [1]=entity ID
+ """
+ for cid_eid in olt_only:
+ # First process the alarm clearing
+ self.process_alarm_data(cid_eid[0], cid_eid[1], 0, -1)
+ # Now remove from alarm DB so we match the ONU alarm table
+ self._database.delete(self._device_id, cid_eid[0], cid_eid[1])
+
+ def process_attr_diffs(self, attr_diffs, onu_db):
+ """
+ Mismatch in alarm settings. Note that the attribute should always be the
+ alarm bitmap attribute (long). For differences, the ONU is always right
+
+ :param attr_diffs: (list(int,int,str)) [0]=class ID, [1]=entity ID, [1]=attr
+ :param olt_db: (dict) OLT Alarm database snapshot from the alarm audit
+ :param onu_db: (dict) ONU Alarm database from the alarm audit upload
+ """
+ for cid_eid_attr in attr_diffs:
+ class_id = cid_eid_attr[0]
+ entity_id = cid_eid_attr[1]
+
+ try:
+ assert AlarmDbExternal.ALARM_BITMAP_KEY == cid_eid_attr[2]
+ bitmap = onu_db[class_id][entity_id][ATTRIBUTES_KEY][AlarmDbExternal.ALARM_BITMAP_KEY]
+ self.process_alarm_data(class_id, entity_id, bitmap, -1)
+
+ except KeyError as e:
+ self.log.error('alarm-not-found', class_id=class_id, entity_id=entity_id, e=e)
+
+ def on_alarm_update_response(self, _topic, msg):
+ """
+ Process a Get All Alarms response
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-alarm-update-response', state=self.state, msg=msg)
+
+ if self._omci_cc_subscriptions[RxEvent.Get_ALARM_Get]:
+ if self.state == 'disabled':
+ self.log.error('rx-in-invalid-state', state=self.state)
+ return
+
+ try:
+ response = msg.get(RX_RESPONSE_KEY)
+
+ if isinstance(response, OmciFrame) and \
+ isinstance(response.fields.get('omci_message'), OmciGetAllAlarmsResponse):
+ # ONU will reset its last alarm sequence number to 0 on receipt of the
+ # Get All Alarms request
+ self.log.debug('received-alarm-response')
+ self.reset_alarm_sequence()
+
+ except Exception as e:
+ self.log.exception('upload-alarm-failure', e=e)
+
+ def on_alarm_notification(self, _topic, msg):
+ """
+ Process an alarm Notification
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with keys:
+ TX_REQUEST_KEY -> None (this is an autonomous msg)
+ RX_RESPONSE_KEY -> OmciMessage (Alarm notification frame)
+ """
+ self.log.debug('on-alarm-update-response', state=self.state, msg=msg)
+
+ alarm_msg = msg.get(RX_RESPONSE_KEY)
+ if alarm_msg is not None:
+ omci_msg = alarm_msg.fields['omci_message'].fields
+ class_id = omci_msg['entity_class']
+ seq_no = omci_msg['alarm_sequence_number']
+
+ # Validate that this ME supports alarm notifications
+ if class_id not in self._device.me_map or \
+ OP.AlarmNotification not in self._device.me_map[class_id].notifications or \
+ len(self._device.me_map[class_id].alarms) == 0:
+ self.log.warn('invalid-alarm-notification', class_id=class_id)
+ return
+
+ self.process_alarm_data(class_id,
+ omci_msg['entity_id'],
+ omci_msg['alarm_bit_map'],
+ seq_no)
+
+ def process_alarm_data(self, class_id, entity_id, bitmap, msg_seq_no):
+ """
+ Process new alarm data
+
+ :param class_id: (int) Class ID of alarm
+ :param entity_id: (int) Entity ID of alarm
+ :param bitmap: (long) Alarm bitmap value
+ :param msg_seq_no: (int) Alarm sequence number. -1 if generated during an audit
+ """
+ if msg_seq_no > 0:
+ # increment alarm number & compare to alarm # in message
+ # Signal early audit if no match and audits are enabled
+ self.increment_alarm_sequence()
+
+ if self.last_alarm_sequence != msg_seq_no and self._audit_delay > 0:
+ self._deferred = reactor.callLater(0, self.audit_alarm)
+
+ key = AlarmDbExternal.ALARM_BITMAP_KEY
+ prev_entry = self._database.query(self._device_id, class_id, entity_id)
+ try:
+ # Need to access the bit map structure which is nested in dict attributes
+ prev_bitmap = 0 if len(prev_entry) == 0 else long(prev_entry['attributes'][key])
+ except Exception as e:
+ self.log.exception('alarm-prev-entry-collection-failure', class_id=class_id,
+ device_id=self._device_id, entity_id=entity_id, value=bitmap, e=e)
+ # Save current entry before going on
+ try:
+ self._database.set(self._device_id, class_id, entity_id, {key: bitmap})
+
+ except Exception as e:
+ self.log.exception('alarm-save-failure', class_id=class_id,
+ device_id=self._device_id, entity_id=entity_id, value=bitmap, e=e)
+
+ if self._alarm_manager is not None:
+ # Generate a set of alarm number that are raised in current and previous
+ previously_raised = {alarm_no for alarm_no in xrange(224)
+ if prev_bitmap & (1L << (223-alarm_no)) != 0L}
+
+ currently_raised = {alarm_no for alarm_no in xrange(224)
+ if bitmap & (1L << (223-alarm_no)) != 0L}
+
+ newly_cleared = previously_raised - currently_raised
+ newly_raised = currently_raised - previously_raised
+
+ # Generate the set/clear alarms now
+ for alarm_number in newly_cleared:
+ reactor.callLater(0, self.clear_alarm, class_id, entity_id, alarm_number)
+
+ for alarm_number in newly_raised:
+ reactor.callLater(0, self.raise_alarm, class_id, entity_id, alarm_number)
+
+ def get_alarm_description(self, class_id, alarm_number):
+ """
+ Get the alarm description, both as a printable-string and also a CamelCase value
+ """
+ if alarm_number in self._device.me_map[class_id].alarms:
+ description = self._device.me_map[class_id].alarms[alarm_number]
+ elif alarm_number <= 207:
+ description = 'Reserved alarm {}'.format(alarm_number)
+ else:
+ description = 'Vendor specific alarm {}'.format(alarm_number)
+
+ # For CamelCase, replace hyphens with spaces before camel casing the string
+ return description, description.replace('-', ' ').title().replace(' ', '')
+
+ def raise_alarm(self, class_id, entity_id, alarm_number):
+ """
+ Raise an alarm on the ONU
+
+ :param class_id: (int) Class ID of the Alarm ME
+ :param entity_id: (int) Entity ID of the Alarm
+ :param alarm_number: (int) Alarm number (bit) that is alarmed
+ """
+ description, name = self.get_alarm_description(class_id, alarm_number)
+
+ self.log.warn('alarm-set', class_id=class_id, entity_id=entity_id,
+ alarm_number=alarm_number, name=name, description=description)
+
+ if self._alarm_manager is not None:
+ alarm = self.omci_alarm_to_onu_alarm(class_id, entity_id, alarm_number)
+ if alarm is not None:
+ alarm.raise_alarm()
+
+ def clear_alarm(self, class_id, entity_id, alarm_number):
+ """
+ Lower/clear an alarm on the ONU
+
+ :param class_id: (int) Class ID of the Alarm ME
+ :param entity_id: (int) Entity ID of the Alarm
+ :param alarm_number: (int) Alarm number (bit) that is alarmed
+ """
+ description, name = self.get_alarm_description(class_id, alarm_number)
+
+ self.log.info('alarm-cleared', class_id=class_id, entity_id=entity_id,
+ alarm_number=alarm_number, name=name, description=description)
+
+ if self._alarm_manager is not None:
+ alarm = self.omci_alarm_to_onu_alarm(class_id, entity_id, alarm_number)
+ if alarm is not None:
+ alarm.clear_alarm()
+
+ def query_mib(self, class_id=None, instance_id=None):
+ """
+ Get Alarm database information.
+
+ This method can be used to request information from the database to the detailed
+ level requested
+
+ :param class_id: (int) Managed Entity class ID
+ :param instance_id: (int) Managed Entity instance
+
+ :return: (dict) The value(s) requested. If class/inst/attribute is
+ not found, an empty dictionary is returned
+ :raises DatabaseStateError: If the database is not enabled or does not exist
+ """
+ from voltha.extensions.omci.database.mib_db_api import DatabaseStateError
+
+ self.log.debug('query', class_id=class_id, instance_id=instance_id)
+ if self._database is None:
+ raise DatabaseStateError('Database does not yet exist')
+
+ return self._database.query(self._device_id, class_id=class_id, instance_id=instance_id)
+
+ def omci_alarm_to_onu_alarm(self, class_id, entity_id, alarm_number):
+ """
+ Map an OMCI Alarm Notification alarm to the proper ONU Alarm Library alarm
+
+ :param class_id: (int) ME Class ID
+ :param entity_id: (int) ME Class instance ID
+ :param alarm_number: (int) Alarm Number
+ :return: (AlarmBase) Alarm library alarm or None if not supported/found
+ """
+ from voltha.extensions.alarms.onu.onu_dying_gasp_alarm import OnuDyingGaspAlarm
+ from voltha.extensions.alarms.onu.onu_los_alarm import OnuLosAlarm
+ from voltha.extensions.alarms.onu.onu_equipment_alarm import OnuEquipmentAlarm
+ from voltha.extensions.alarms.onu.onu_selftest_failure_alarm import OnuSelfTestFailureAlarm
+ from voltha.extensions.alarms.onu.onu_laser_eol_alarm import OnuLaserEolAlarm
+ from voltha.extensions.alarms.onu.onu_laser_bias_current_alarm import OnuLaserBiasAlarm
+ from voltha.extensions.alarms.onu.onu_temp_yellow_alarm import OnuTempYellowAlarm
+ from voltha.extensions.alarms.onu.onu_temp_red_alarm import OnuTempRedAlarm
+ from voltha.extensions.alarms.onu.onu_voltage_yellow_alarm import OnuVoltageYellowAlarm
+ from voltha.extensions.alarms.onu.onu_voltage_red_alarm import OnuVoltageRedAlarm
+ from voltha.extensions.alarms.onu.onu_low_rx_optical_power_alarm import OnuLowRxOpticalAlarm
+ from voltha.extensions.alarms.onu.onu_high_rx_optical_power_alarm import OnuHighRxOpticalAlarm
+ from voltha.extensions.alarms.onu.onu_low_tx_optical_power_alarm import OnuLowTxOpticalAlarm
+ from voltha.extensions.alarms.onu.onu_high_tx_optical_power_alarm import OnuHighTxOpticalAlarm
+
+ mgr = self._alarm_manager
+ if class_id in (CircuitPack.class_id, PptpEthernetUni.class_id):
+ intf_id = self.select_uni_port(class_id, entity_id)
+
+ elif class_id in (AniG.class_id, OntG.class_id):
+ intf_id = self.select_ani_port(class_id, entity_id)
+
+ else:
+ self.log.error('unsupported-class-id', class_id=class_id, alarm_number=alarm_number)
+ return
+
+ alarm_map = {
+ (CircuitPack.class_id, 0): OnuEquipmentAlarm,
+ (CircuitPack.class_id, 2): OnuSelfTestFailureAlarm,
+ (CircuitPack.class_id, 3): OnuLaserEolAlarm,
+ (CircuitPack.class_id, 4): OnuTempYellowAlarm,
+ (CircuitPack.class_id, 5): OnuTempRedAlarm,
+
+ (PptpEthernetUni.class_id, 0): OnuLosAlarm,
+
+ (OntG.class_id, 0): OnuEquipmentAlarm,
+ (OntG.class_id, 6): OnuSelfTestFailureAlarm,
+ (OntG.class_id, 7): OnuDyingGaspAlarm,
+ (OntG.class_id, 8): OnuTempYellowAlarm,
+ (OntG.class_id, 9): OnuTempRedAlarm,
+ (OntG.class_id, 10): OnuVoltageYellowAlarm,
+ (OntG.class_id, 11): OnuVoltageRedAlarm,
+
+ (AniG.class_id, 0): OnuLowRxOpticalAlarm,
+ (AniG.class_id, 1): OnuHighRxOpticalAlarm,
+ (AniG.class_id, 4): OnuLowTxOpticalAlarm,
+ (AniG.class_id, 5): OnuHighTxOpticalAlarm,
+ (AniG.class_id, 6): OnuLaserBiasAlarm,
+ }
+ alarm_cls = alarm_map.get((class_id, alarm_number))
+
+ return alarm_cls(mgr, self._onu_id, intf_id) if alarm_cls is not None else None
+
+ def select_uni_port(self, class_id, entity_id):
+ """
+ Select the best possible UNI Port (logical) interface number for this ME class and
+ entity ID.
+
+ This base implementation will assume that a UNI Port object has been registered
+ on startup and supports both an 'entity_id' and also 'logical_port_number'
+ property. See both the Adtran and BroadCom OpenOMCI ONU DA for an example
+ of this UNI port object.
+
+ :param class_id: (int) ME Class ID for which the alarms belongs to
+ :param entity_id: (int) Instance ID
+
+ :return: (int) Logical Port number for the UNI port
+ """
+ # NOTE: Of the three class ID's supported in this version of code, only the CircuitPack,
+ # and PptpEthernetUni MEs will map to the UNI port
+ assert class_id in (CircuitPack.class_id, PptpEthernetUni.class_id)
+
+ return next((uni.logical_port_number for uni in self._uni_ports if
+ uni.entity_id == entity_id), None)
+
+ def select_ani_port(self, class_id, _entity_id):
+ """
+ Select the best possible ANI Port (physical) interface number for this ME class and
+ entity ID.
+
+ Currently the base implementation assumes only a single PON port and it will be
+ chosen. A future implementation may want to have a PON Port object (similar to
+ the BroadCom Open OMCI and Adtran ONU's UNI Port object) that provides a match
+ for entity ID. This does assume that the PON port object supports a property
+ of 'port_number' to return the physical port number.
+
+ :param class_id: (int) ME Class ID for which the alarms belongs to
+ :param _entity_id: (int) Instance ID
+
+ :return: (int) Logical Port number for the UNI port
+ """
+ # NOTE: Of the three class ID's supported in this version of code, only the AniG
+ # MEs will map to the ANI port. For some the OntG alarms (Dying Gasp) the
+ # PON interface will also be selected.
+ assert class_id in (AniG.class_id, OntG.class_id)
+
+ return self._ani_ports[0].port_number if len(self._ani_ports) else None
diff --git a/python/adapters/extensions/omci/state_machines/image_agent.py b/python/adapters/extensions/omci/state_machines/image_agent.py
new file mode 100755
index 0000000..e6d5884
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/image_agent.py
@@ -0,0 +1,1024 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import os
+import structlog
+from datetime import datetime, timedelta
+from binascii import crc32, hexlify
+from transitions import Machine
+from transitions.extensions.nesting import HierarchicalMachine as HMachine
+from twisted.python import failure
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred, CancelledError
+from common.event_bus import EventBusClient
+from voltha.protos.voltha_pb2 import ImageDownload
+from voltha.protos.omci_mib_db_pb2 import OpenOmciEventType
+from voltha.extensions.omci.omci_defs import EntityOperations, ReasonCodes, AttributeAccess, OmciSectionDataSize
+from voltha.extensions.omci.omci_entities import SoftwareImage
+from voltha.extensions.omci.omci_cc import DEFAULT_OMCI_TIMEOUT
+from voltha.extensions.omci.omci_messages import OmciEndSoftwareDownloadResponse, OmciActivateImageResponse
+
+###################################################################################
+## OLT out-of-band download image procedure
+###################################################################################
+
+class ImageDownloadeSTM(object):
+ DEFAULT_STATES = ['disabled', 'downloading', 'validating', 'done']
+ DEFAULT_TRANSITIONS = [
+ {'trigger': 'start', 'source': 'disabled', 'dest': 'downloading'},
+ {'trigger': 'stop', 'source': ['downloading', 'validating', 'done'], 'dest': 'disabled'},
+ {'trigger': 'dw_success', 'source': 'downloading', 'dest': 'validating'},
+ {'trigger': 'dw_fail', 'source': 'downloading', 'dest': 'done'},
+ {'trigger': 'validate_success', 'source': 'validating', 'dest': 'done'},
+ ]
+ DEFAULT_TIMEOUT_RETRY = 1000 # Seconds to delay after task failure/timeout
+
+ # def __init__(self, omci_agent, dev_id, local_name, local_dir, remote_url, download_task,
+ def __init__(self, omci_agent, image_download,
+ download_task_cls,
+ states=DEFAULT_STATES,
+ transitions=DEFAULT_TRANSITIONS,
+ initial_state='disabled',
+ timeout_delay=DEFAULT_TIMEOUT_RETRY,
+ advertise_events=True, clock=None):
+ """
+ :Param: omci_agent: (OpenOMCIAgent)
+ :Param: image_dnld: (ImageDownload)
+ ImageDownload.id : device id
+ ImageDownload.name: file name of the image
+ ImageDownload.url : URL to download the image from server
+ ImageDownload.local_dir: local directory of the image file
+ """
+ self.log = structlog.get_logger(device_id=image_download.id)
+ self._agent = omci_agent
+ # self._imgdw = ImageDownload()
+ # self._imgdw.name = local_name
+ # self._imgdw.id = dev_id
+ # self._imgdw.url = remote_url
+ # self._imgdw.local_dir = local_dir
+ self._imgdw = image_download
+ # self._imgdw.state = ImageDownload.DOWNLOAD_UNKNOWN # voltha_pb2
+
+ self._download_task_cls = download_task_cls
+ self._timeout_delay = timeout_delay
+
+ self._current_task = None
+ self._task_deferred = None
+ self._ret_deferred = None
+ self._timeout_dc = None # DelayedCall
+ self._advertise_events = advertise_events
+ self.reactor = clock if clock is not None else reactor
+
+ self.log.debug("ImageDownloadeSTM", image_download=self._imgdw)
+ self.machine = Machine(model=self, states=states,
+ transitions=transitions,
+ initial=initial_state,
+ queued=True,
+ name='{}-{}'.format(self.__class__.__name__, self._imgdw.id))
+ # @property
+ # def name(self):
+ # return self._imgdw.name
+
+ def _cancel_timeout(self):
+ d, self._timeout_dc = self._timeout_dc, None
+ if d is not None and not d.called:
+ d.cancel()
+
+ @property
+ def status(self):
+ return self._imgdw
+
+ @property
+ def deferred(self):
+ return self._ret_deferred
+
+ def advertise(self, event, info):
+ """Advertise an event on the OpenOMCI event bus"""
+ if self._advertise_events:
+ self._agent.advertise(event,
+ {
+ 'state-machine': self.machine.name,
+ 'info': info,
+ 'time': str(datetime.utcnow())
+ })
+
+ # def reset(self):
+ # """
+ # Reset all the state machine to intial state
+ # It is used to clear failed result in last downloading
+ # """
+ # self.log.debug('reset download', image_download=self._imgdw)
+ # if self._current_task is not None:
+ # self._current_task.stop()
+
+ # self._cancel_deferred()
+
+ # if self._ret_deferred is not None:
+ # self._ret_deferred.cancel()
+ # self._ret_deferred = None
+
+ # self.stop()
+ # self._imgdw.state = ImageDownload.DOWNLOAD_UNKNOWN
+
+ def get_file(self):
+ """
+ return a Deferred object
+ Caller will register a callback to the Deferred to get notified once the image is available
+ """
+ # self.log.debug('get_file', image_download=self._imgdw)
+ if self._ret_deferred is None or self._ret_deferred.called:
+ self._ret_deferred = Deferred()
+
+ if self._imgdw.state == ImageDownload.DOWNLOAD_SUCCEEDED:
+ self.log.debug('Image Available')
+ self.reactor.callLater(0, self._ret_deferred.callback, self._imgdw)
+ elif self._imgdw.state == ImageDownload.DOWNLOAD_FAILED or self._imgdw.state == ImageDownload.DOWNLOAD_UNSUPPORTED:
+ self.log.debug('Image not exist')
+ self.reactor.callLater(0, self._ret_deferred.errback, failure.Failure(Exception('Image Download Failed ' + self._imgdw.name)))
+ elif self._imgdw.state == ImageDownload.DOWNLOAD_UNKNOWN or self._imgdw.state == ImageDownload.DOWNLOAD_REQUESTED:
+ self.log.debug('Start Image STM')
+ self._imgdw.state = ImageDownload.DOWNLOAD_STARTED
+ self.reactor.callLater(0, self.start)
+ else:
+ self.log.debug('NO action', state=self._imgdw.state)
+
+ return self._ret_deferred
+
+ def timeout(self):
+ self.log.debug('Image Download Timeout', download_task=self._current_task);
+ if self._current_task:
+ self.reactor.callLater(0, self._current_task.stop)
+ # if self._task_deferred is not None and not self._task_deferred.called:
+ # self._task_deferred.cancel()
+ self._current_task = None
+ # else:
+ # self.dw_fail()
+
+ def on_enter_downloading(self):
+ self.log.debug("on_enter_downloading")
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ def success(results):
+ self.log.debug('image-download-success', results=results)
+ self._imgdw.state = ImageDownload.DOWNLOAD_SUCCEEDED
+ self._imgdw.reason = ImageDownload.NO_ERROR
+ self._current_task = None
+ self._task_deferred = None
+ self.dw_success()
+
+ def failure(reason):
+ self.log.info('image-download-failure', reason=reason)
+ if self._imgdw.state == ImageDownload.DOWNLOAD_STARTED:
+ self._imgdw.state = ImageDownload.DOWNLOAD_FAILED
+ if isinstance(reason, CancelledError):
+ self._imgdw.reason = ImageDownload.CANCELLED
+ self._current_task = None
+ self._task_deferred = None
+ self.dw_fail()
+
+ self._device = self._agent.get_device(self._imgdw.id)
+ self._current_task = self._download_task_cls(self._agent, self._imgdw, self.reactor)
+
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+ self._imgdw.state = ImageDownload.DOWNLOAD_STARTED
+
+ if self._timeout_delay > 0:
+ self._timeout_dc = self.reactor.callLater(self._timeout_delay, self.timeout)
+
+ def on_enter_validating(self):
+ self.log.debug("on_enter_validating")
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self.validate_success()
+
+ def on_enter_done(self):
+ self.log.debug("on_enter_done")
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self._cancel_timeout()
+
+ d, self._ret_deferred = self._ret_deferred, None
+ if d is not None:
+ if self._imgdw.state == ImageDownload.DOWNLOAD_SUCCEEDED:
+ self.reactor.callLater(0, d.callback, self._imgdw)
+ else: # failed
+ if self._imgdw.reason == ImageDownload.CANCELLED:
+ self.reactor.callLater(0, d.cancel)
+ else:
+ self.reactor.callLater(0, d.errback, failure.Failure(Exception('Image Download Failed ' + self._imgdw.name)))
+
+ def on_enter_disabled(self):
+ self.log.debug("on_enter_disabled")
+ self.advertise(OpenOmciEventType.state_change, self.state)
+
+ self._cancel_timeout()
+ if self._current_task is not None:
+ self.reactor.callLater(0, self._current_task.stop)
+ self._current_task = None
+
+ if self._ret_deferred:
+ self.reactor.callLater(0, self._ret_deferred.cancel)
+ self._ret_deferred = None
+
+ # remove local file fragments if download failed
+ file_path = self._imgdw.local_dir + '/' + self._imgdw.name
+ if self._imgdw.state != ImageDownload.DOWNLOAD_SUCCEEDED and os.path.exists(file_path):
+ os.remove(file_path)
+ self._imgdw.state = ImageDownload.DOWNLOAD_UNKNOWN
+
+###################################################################################
+## OMCI Software Image Download Procedure
+###################################################################################
+
+class OmciSoftwareImageDownloadSTM(object):
+
+ OMCI_SWIMG_DOWNLOAD_TIMEOUT = 5400 # TODO: Seconds for the full downloading procedure to avoid errors that cause infinte downloading
+ OMCI_SWIMG_DOWNLOAD_WINDOW_SIZE = 32
+ OMCI_SWIMG_WINDOW_RETRY_MAX = 2
+ OMCI_SWIMG_ACTIVATE_RETRY_MAX = 2
+ OMCI_SWIMG_ACTIVATE_TRANSITIONS_TIMEOUT = 10 # Seconds to delay after task failure/timeout
+
+ # def __init__(self, omci_agent, dev_id, img_path,
+ def __init__(self, image_id, omci_agent, image_dnld,
+ window_size=OMCI_SWIMG_DOWNLOAD_WINDOW_SIZE,
+ timeout_delay=OMCI_SWIMG_DOWNLOAD_TIMEOUT,
+ advertise_events=True,
+ clock=None):
+ """
+ omci_agent: (OpenOMCIAgent)
+ image_dnld: (ImageDownload)
+ ImageDownload.id : device id
+ ImageDownload.name: file name of the image
+ ImageDownload.url : URL to download the image from server
+ ImageDownload.local_dir: local directory of the image file
+ window_size: window size of OMCI download procedure
+ """
+ self.log = structlog.get_logger(device_id=image_dnld.id)
+ self._omci_agent = omci_agent
+ self._image_download = image_dnld
+ self._timeout = timeout_delay
+ self._timeout_dc = None
+ self._window_size = window_size
+ self.reactor = clock if clock is not None else reactor
+ self._offset = 0
+ # self._win_section = 0
+ self._win_retry = 0
+ self._device_id = image_dnld.id
+ self._device = omci_agent.get_device(image_dnld.id)
+ self.__init_state_machine()
+ self._ret_deferred = None
+ self._image_id = image_id # Target software image entity ID
+ self._image_file = image_dnld.local_dir + '/' + image_dnld.name
+ self._image_obj = open(self._image_file, mode='rb')
+ self._image_size = os.path.getsize(self._image_file)
+ self._crc32 = 0
+ self._win_crc32 = 0
+ self._win_data = None
+ self._current_deferred = None
+ self._result = None # ReasonCodes
+ self.crctable = []
+ self._crctable_init = False
+ self._actimg_retry_max = OmciSoftwareImageDownloadSTM.OMCI_SWIMG_ACTIVATE_RETRY_MAX
+ self._actimg_retry = 0
+ self.log.debug("DownloadSTM", image=self._image_file, image_size=self._image_size)
+
+ def __init_state_machine(self):
+
+ #### Download Window Sub State Machine ####
+ OMCI_DOWNLOAD_WINDOW_STATE = ['init_window', 'sending_sections', 'window_success', 'window_failed']
+ OMCI_DOWNLOAD_WINDOW_TRANSITIONS = [
+ {'trigger': 'send_sections', 'source': 'init_window', 'dest': 'sending_sections'},
+ # {'trigger': 'send_section_last', 'source': 'start_section', 'dest': 'last_section' },
+ {'trigger': 'rx_ack_success', 'source': 'sending_sections', 'dest': 'window_success' },
+ {'trigger': 'rx_ack_failed', 'source': 'sending_sections', 'dest': 'window_failed' },
+ # {'trigger': 'retry_window', 'source': 'window_failed', 'dest': 'start_section' },
+ {'trigger': 'reset_window', 'source': '*', 'dest': 'init_window' }
+ ]
+ self.win_machine = HMachine(model=self,
+ states=OMCI_DOWNLOAD_WINDOW_STATE,
+ transitions=OMCI_DOWNLOAD_WINDOW_TRANSITIONS,
+ initial='init_window',
+ queued=True,
+ name='{}-window_section_machine'.format(self.__class__.__name__))
+
+ #### Software Activation Sub State Machine ####
+ OMCI_SWIMG_ACTIVATE_STATES = ['init_act', 'activating', 'busy', 'rebooting', 'committing', 'done', 'failed']
+ OMCI_SWIMG_ACTIVATE_TRANSITIONS = [
+ {'trigger': 'activate', 'source': ['init_act', 'busy'], 'dest': 'activating'},
+ {'trigger': 'onu_busy', 'source': 'activating', 'dest': 'busy'},
+ {'trigger': 'reboot', 'source': 'activating', 'dest': 'rebooting'},
+ {'trigger': 'do_commit', 'source': ['activating', 'rebooting'], 'dest': 'committing'},
+ # {'trigger': 'commit_ok', 'source': 'committing', 'dest': 'done'},
+ {'trigger': 'reset_actimg', 'source': ['activating', 'rebooting', 'committing', 'failed'], 'dest': 'init_act'},
+ # {'trigger': 'actimg_fail', 'source': ['init_act', 'activating', 'rebooting', 'committing'], 'dest': 'failed'}
+ ]
+
+ self.activate_machine = HMachine(model=self,
+ states=OMCI_SWIMG_ACTIVATE_STATES,
+ transitions=OMCI_SWIMG_ACTIVATE_TRANSITIONS,
+ initial='init_act',
+ queued=True,
+ name='{}-activate_machine'.format(self.__class__.__name__))
+
+ #### Main State Machine ####
+ OMCI_SWIMG_DOWNLOAD_STATES = [ 'init_image', 'starting_image', 'ending_image', 'endimg_busy', 'done_image',
+ {'name': 'dwin', 'children': self.win_machine},
+ {'name': 'actimg', 'children': self.activate_machine}
+ ]
+ OMCI_SWIMG_DOWNLOAD_TRANSITIONS = [
+ {'trigger': 'start_image', 'source': 'init_image', 'dest': 'starting_image' },
+ {'trigger': 'download_window', 'source': 'starting_image', 'dest': 'dwin_init_window' },
+ {'trigger': 'download_success', 'source': 'dwin', 'dest': 'ending_image' },
+ {'trigger': 'onu_busy', 'source': 'ending_image', 'dest': 'endimg_busy' },
+ {'trigger': 'retry_endimg', 'source': 'endimg_busy', 'dest': 'ending_image' },
+ {'trigger': 'end_img_success', 'source': 'ending_image', 'dest': 'actimg_init_act' },
+ {'trigger': 'activate_done', 'source': 'actimg', 'dest': 'done_image' },
+ {'trigger': 'download_fail', 'source': '*', 'dest': 'done_image' },
+ {'trigger': 'reset_image', 'source': '*', 'dest': 'init_image' },
+ ]
+
+ self.img_machine = HMachine(model=self,
+ states=OMCI_SWIMG_DOWNLOAD_STATES,
+ transitions=OMCI_SWIMG_DOWNLOAD_TRANSITIONS,
+ initial='init_image',
+ queued=True,
+ name='{}-image_download_machine'.format(self.__class__.__name__))
+
+ # @property
+ # def image_filename(self):
+ # return self._image_file
+
+ # @image_filename.setter
+ # def image_filename(self, value):
+ # if self._image_fd is not None:
+ # self._image_fd.close()
+ # self._image_filename = value
+ # self._image_fd = open(self._image_filename, mode='rb')
+ # self._image_size = os.path.getsize(self._image_filename)
+ # print("Set image file: " + self._image_filename + " size: " + str(self._image_size))
+
+ def __omci_start_download_resp_success(self, rx_frame):
+ self.log.debug("__omci_download_resp_success")
+ self.download_window()
+ return rx_frame
+
+ def __omci_start_download_resp_fail(self, fail):
+ self.log.debug("__omci_download_resp_fail", failure=fail)
+ self._result = ReasonCodes.ProcessingError
+ self.download_fail()
+
+ def __omci_end_download_resp_success(self, rx_frame):
+ self.log.debug("__omci_end_download_resp_success")
+ if rx_frame.fields['message_type'] == OmciEndSoftwareDownloadResponse.message_id: # 0x35
+ omci_data = rx_frame.fields['omci_message']
+ if omci_data.fields['result'] == 0:
+ self.log.debug('OMCI End Image OK')
+ self._result = ReasonCodes.Success
+ self.end_img_success()
+ elif omci_data.fields['result'] == 6: # Device Busy
+ self.log.debug('OMCI End Image Busy')
+ self.onu_busy()
+ else:
+ self.log.debug('OMCI End Image Failed', reason=omci_data.fields['result'])
+ else:
+ self.log.debug('Receive Unexpected OMCI', message_type=rx_frame.fields['message_type'])
+
+ def __omci_end_download_resp_fail(self, fail):
+ self.log.debug("__omci_end_download_resp_fail", failure=fail)
+ self._result = ReasonCodes.ProcessingError
+ self.download_fail()
+
+ def __omci_send_window_resp_success(self, rx_frame, cur_state, datasize):
+ # self.log.debug("__omci_send_window_resp_success", current_state=cur_state)
+ self._offset += datasize
+ self._image_download.downloaded_bytes += datasize
+ self.rx_ack_success()
+
+ def __omci_send_window_resp_fail(self, fail, cur_state):
+ self.log.debug("__omci_send_window_resp_fail", current_state=cur_state)
+ self.rx_ack_failed()
+
+ def __activate_resp_success(self, rx_frame):
+ self._current_deferred = None
+ if rx_frame.fields['message_type'] == OmciActivateImageResponse.message_id: # 0x36
+ omci_data = rx_frame.fields['omci_message']
+ if omci_data.fields['result'] == 0:
+ self.log.debug("Activate software image success, rebooting ONU ...", device_id=self._device.device_id,
+ state=self._image_download.image_state)
+ standby_image_id = 0 if self._image_id else 1
+ self._omci_agent.database.set(self._device.device_id, SoftwareImage.class_id, self._image_id, {"is_active": 1})
+ self._omci_agent.database.set(self._device.device_id, SoftwareImage.class_id, standby_image_id, {"is_active": 0})
+ self.reboot()
+ elif omci_data.fields['result'] == 6: # Device Busy
+ self.log.debug('OMCI Activate Image Busy')
+ self.onu_busy()
+ else:
+ self.log.debug('OMCI Activate Image Failed', reason=omci_data['result'])
+ else:
+ self.log.debug('Receive Unexpected OMCI', message_type=rx_frame['message_type'])
+
+ def __activate_fail(self, fail):
+ self.log.debug("Activate software image failed", faile=fail)
+ self._current_deferred = None
+ self._result = ReasonCodes.ProcessingError
+ self.activate_done()
+
+ def __commit_success(self, rx_frame):
+ self.log.debug("Commit software success", device_id=self._device_id)
+ self._current_deferred = None
+ standby_image_id = 0 if self._image_id else 1
+ self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, self._image_id, {"is_committed": 1})
+ self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, standby_image_id, {"is_committed": 0})
+ self._image_download.image_state = ImageDownload.IMAGE_ACTIVE
+ self._result = ReasonCodes.Success
+ self.activate_done()
+
+ def __commit_fail(self, fail):
+ self.log.debug("Commit software image failed", faile=fail)
+ self._current_deferred = None
+ self._result = ReasonCodes.ProcessingError
+ self._image_download.image_state = ImageDownload.IMAGE_REVERT
+ self.activate_done()
+
+# @property
+# def image_id(self):
+# return self._image_id
+
+# @image_id.setter
+# def image_id(self, value):
+# self._image_id = value
+
+ @property
+ def status(self):
+ return self._image_download
+
+ def start(self):
+ self.log.debug("OmciSoftwareImageDownloadSTM.start", current_state=self.state)
+ if self._ret_deferred is None:
+ self._ret_deferred = Deferred()
+ if self.state == 'init_image':
+ self.reactor.callLater(0, self.start_image)
+ return self._ret_deferred
+
+ def stop(self):
+ self.log.debug("OmciSoftwareImageDownloadSTM.stop", current_state=self.state)
+ self._result = ReasonCodes.OperationCancelled
+ self.download_fail()
+
+ def on_enter_init_image(self):
+ self.log.debug("on_enter_init_image")
+ self._image_obj.seek(0)
+ self._offset = 0
+ # self._win_section = 0
+ self._win_retry = 0
+
+ def on_enter_starting_image(self):
+ self.log.debug("on_enter_starting_image")
+ self._image_download.downloaded_bytes = 0
+ self._current_deferred = self._device.omci_cc.send_start_software_download(self._image_id, self._image_size, self._window_size)
+ self._current_deferred.addCallbacks(self.__omci_start_download_resp_success, self.__omci_start_download_resp_fail)
+ # callbackArgs=(self.state,), errbackArgs=(self.state,))
+
+ def on_enter_dwin_init_window(self):
+ # self.log.debug("on_enter_dwin_init_window", offset=self._offset, image_size=self._image_size)
+ if self._offset < self._image_size:
+ self.send_sections()
+
+ def on_enter_dwin_sending_sections(self):
+ # self.log.debug("on_enter_dwin_sending_sections", offset=self._offset)
+
+ if (self._offset + self._window_size * OmciSectionDataSize) <= self._image_size:
+ sections = self._window_size
+ mod = 0
+ datasize = self._window_size * OmciSectionDataSize
+ else:
+ datasize = self._image_size - self._offset
+ sections = datasize / OmciSectionDataSize
+ mod = datasize % OmciSectionDataSize
+ sections = sections + 1 if mod > 0 else sections
+
+ # self.log.debug("on_enter_dwin_sending_sections", offset=self._offset, datasize=datasize, sections=sections)
+ if self._win_retry == 0:
+ self._win_data = self._image_obj.read(datasize)
+ self._win_crc32 = self.crc32(self._crc32, self._win_data)
+ # self.log.debug("CRC32", crc32=self._win_crc32, offset=self._offset)
+ else:
+ self.log.debug("Retry download window with crc32", offset=self._offset)
+
+ sent = 0
+ for i in range(0, sections):
+ if i < sections - 1:
+ # self.log.debug("section data", data=hexlify(data[(self._offset+sent):(self._offset+sent+OmciSectionDataSize)]))
+ self._device.omci_cc.send_download_section(self._image_id, i,
+ self._win_data[sent:sent+OmciSectionDataSize])
+ sent += OmciSectionDataSize
+ else:
+ last_size = OmciSectionDataSize if mod == 0 else mod
+ self._current_deferred = self._device.omci_cc.send_download_section(self._image_id, i,
+ self._win_data[sent:sent+last_size],
+ timeout=DEFAULT_OMCI_TIMEOUT)
+ self._current_deferred.addCallbacks(self.__omci_send_window_resp_success, self.__omci_send_window_resp_fail,
+ callbackArgs=(self.state, datasize), errbackArgs=(self.state,))
+ sent += last_size
+ assert sent==datasize
+
+ # def on_enter_dwin_last_section(self):
+ # self._current_deferred = self._device.omci_cc.send_download_section, self._instance_id, self._win_section, data)
+ # self._current_deferred.addCallbacks(self.__omci_resp_success, self.__omci_resp_fail,
+ # callbackArgs=(self.state,), errbackArgs=(self.state,))
+
+ def on_enter_dwin_window_success(self):
+ # self.log.debug("on_enter_dwin_window_success")
+ self._crc32 = self._win_crc32 if self._win_crc32 != 0 else self._crc32
+ self._win_crc32 = 0
+ self._win_retry = 0
+ if self._offset < self._image_size:
+ self.reset_window()
+ else:
+ self.download_success()
+
+ def on_enter_dwin_window_failed(self):
+ self.log.debug("on_enter_dwin_window_fail: ", retry=self._win_retry)
+ if self._win_retry < self.OMCI_SWIMG_WINDOW_RETRY_MAX:
+ self._win_retry += 1
+ self.reset_window()
+ else:
+ self._result = ReasonCodes.ProcessingError
+ self.download_fail()
+
+ def on_enter_ending_image(self):
+ self.log.debug("on_enter_ending_image", crc32=self._crc32)
+ self._current_deferred = self._device.omci_cc.send_end_software_download(self._image_id, self._crc32,
+ self._image_size, timeout=18)
+ self._current_deferred.addCallbacks(self.__omci_end_download_resp_success, self.__omci_end_download_resp_fail)
+ # callbackArgs=(self.state,), errbackArgs=(self.state,))
+
+ def on_enter_endimg_busy(self):
+ self.log.debug("on_enter_endimg_busy")
+ self.reactor.callLater(3, self.retry_endimg)
+
+ def on_enter_actimg_init_act(self):
+ self.log.debug("on_enter_actimg_init_act", retry=self._actimg_retry, max_retry=self._actimg_retry_max)
+ # self._images[0] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 0, ["is_active", "is_committed", "is_valid"])
+ # self._images[1] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 1, ["is_active", "is_committed", "is_valid"])
+ # if (self._images[self._to_image]["is_active"] != 1 and self._images[self._to_image]["is_valid"] == 1):
+ if self._actimg_retry > self._actimg_retry_max:
+ self.log.debug("activate image failed: retry max", retries=self._actimg_retry)
+ self._result = ReasonCodes.ProcessingError
+ self.activate_done()
+ else:
+ self._image_download.image_state = ImageDownload.IMAGE_ACTIVATE
+ self.activate()
+
+ def on_enter_actimg_activating(self):
+ self.log.debug("on_enter_actimg_activating")
+ img = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id,
+ self._image_id, ["is_active", "is_committed", "is_valid"])
+
+ self.log.debug("on_enter_actimg_activating", instance=self._image_id, state=img)
+ if img["is_active"] == 0:
+ #if img["is_valid"] == 1:
+ self._current_deferred = self._device.omci_cc.send_active_image(self._image_id)
+ self._current_deferred.addCallbacks(self.__activate_resp_success, self.__activate_fail)
+ #else:
+ # self.fail()
+ else:
+ self.do_commit()
+
+ def on_enter_actimg_busy(self):
+ self.log.debug("on_enter_actimg_busy")
+ self.reactor.callLater(3, self.activate)
+
+ def __on_reboot_timeout(self):
+ self.log.debug("on_reboot_timeout")
+ self._timeout_dc = None
+ self._result = ReasonCodes.ProcessingError
+ self.activate_done()
+
+ def on_enter_actimg_rebooting(self):
+ self.log.debug("on_enter_actimg_rebooting")
+ if self._timeout_dc == None:
+ self._timeout_dc = self.reactor.callLater(self._timeout, self.__on_reboot_timeout)
+
+ def on_exit_actimg_rebooting(self):
+ self.log.debug("on_exit_actimg_rebooting", timeout=self._timeout_dc)
+ if self._timeout_dc and self._timeout_dc.active:
+ self._timeout_dc.cancel()
+ self._timeout_dc = None
+
+ def on_enter_actimg_committing(self):
+ # self.log.debug("on_enter_committing")
+ img = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id,
+ self._image_id, ["is_active", "is_committed", "is_valid"])
+ self.log.debug("on_enter_actimg_committing", instance=self._image_id, state=img)
+ if (img['is_active'] == 0):
+ self._actimg_retry += 1
+ self.log.debug("do retry", retry=self._actimg_retry)
+ self.reset_actimg()
+ else:
+ self._actimg_retry = 0
+ self._current_deferred = self._device.omci_cc.send_commit_image(self._image_id)
+ self._current_deferred.addCallbacks(self.__commit_success, self.__commit_fail)
+
+ def on_enter_done_image(self):
+ self.log.debug("on_enter_done_image", result=self._result)
+ if self._result == ReasonCodes.Success:
+ self.reactor.callLater(0, self._ret_deferred.callback, self._image_download) # (str(self._instance_id))
+ else:
+ self._ret_deferred.errback(failure.Failure(Exception('ONU Software Download Failed, instance ' + str(self._image_id))))
+
+ def __crc_GenTable32(self):
+ if self._crctable_init:
+ return
+
+ # x32 + x26 + x23 + x22 + x16 + x12 + x11 + x10 + x8 + x7 + x5 + x4 + x2 + x + 1
+ pn32 = [0, 1, 2, 4, 5, 7, 8, 10, 11, 12, 16, 22, 23, 26]
+ poly = 0
+ for i in pn32:
+ poly |= (1 << i)
+
+ for i in range(0, 256):
+ _accum = (i << 24) & 0xFFFFFFFF
+ for j in range(0, 8):
+ if _accum & (1 << 31):
+ _accum = (_accum << 1) ^ poly
+ else:
+ _accum = (_accum << 1) & 0xFFFFFFFF
+ # self.crctable[i] = accum
+ self.crctable.append(_accum)
+ self._crctable_init = True
+
+ def crc32(self, accum, data):
+ self.__crc_GenTable32()
+ _accum = ~accum & 0xFFFFFFFF
+ num = len(data)
+ for i in range(0, num):
+ _accum = self.crctable[((_accum >> 24) ^ ord(data[i])) & 0xFF] ^ ((_accum << 8) & 0xFFFFFFFF)
+
+ return ~_accum & 0xFFFFFFFF
+
+###################################################################################
+## OMCI Software Image Activation/Committing Procedure
+###################################################################################
+'''
+class OmciSoftwareImageActivateSTM(object):
+ OMCI_SWIMG_ACTIVATE_STATES = ['starting', 'activating', 'busy', 'rebooting', 'committing', 'done', 'failed']
+ OMCI_SWIMG_ACTIVATE_TRANSITIONS = [
+ {'trigger': 'activate', 'source': ['starting', 'busy'], 'dest': 'activating'},
+ {'trigger': 'onu_busy', 'source': 'activating', 'dest': 'busy'},
+ {'trigger': 'reboot', 'source': 'activating', 'dest': 'rebooting'},
+ {'trigger': 'do_commit', 'source': ['activating', 'rebooting'], 'dest': 'committing'},
+ {'trigger': 'commit_ok', 'source': 'committing', 'dest': 'done'},
+ {'trigger': 'reset', 'source': ['activating', 'rebooting', 'committing', 'failed'], 'dest': 'starting'},
+ {'trigger': 'fail', 'source': ['starting', 'activating', 'rebooting', 'committing'], 'dest': 'failed'}
+ ]
+ OMCI_SWIMG_ACTIVATE_TRANSITIONS_TIMEOUT = 10 # Seconds to delay after task failure/timeout
+ OMCI_SWIMG_ACTIVATE_RETRY_MAX = 2
+ def __init__(self, omci_agent, dev_id, target_img_entity_id, image_download,
+ states=OMCI_SWIMG_ACTIVATE_STATES,
+ transitions=OMCI_SWIMG_ACTIVATE_TRANSITIONS,
+ initial_state='disabled',
+ timeout_delay=OMCI_SWIMG_ACTIVATE_TRANSITIONS_TIMEOUT,
+ advertise_events=True,
+ clock=None):
+ self.log = structlog.get_logger(device_id=dev_id)
+ self._omci_agent = omci_agent
+ self._device_id = dev_id
+ self._device = omci_agent.get_device(dev_id)
+ self._to_image = target_img_entity_id
+ self._from_image = 0 if self._to_image == 1 else 1
+ self._image_download = image_download
+ # self._images = dict()
+ self._timeout = timeout_delay
+ self._timeout_dc = None
+ self.reactor = clock if clock is not None else reactor
+ self._retry_max = OmciSoftwareImageActivateSTM.OMCI_SWIMG_ACTIVATE_RETRY_MAX
+ self._retry = 0
+ self._deferred = None
+ self.ret_deferred = None
+ self.machine = Machine(model=self,
+ states=states,
+ transitions=transitions,
+ initial='starting',
+ queued=True,
+ name='{}-image_activate_machine'.format(self.__class__.__name__))
+ self.log.debug("OmciSoftwareImageActivateSTM", target=self._to_image)
+
+ def __activate_resp_success(self, rx_frame):
+ if rx_frame.fields['message_type'] == 0x36: # (OmciActivateImageResponse)
+ omci_data = rx_frame.fields['omci_message']
+ if omci_data.fields['result'] == 0:
+ self.log.debug("Activate software image success, rebooting ONU ...", device_id=self._device_id)
+ self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, self._to_image, {"is_active": 1})
+ self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, self._from_image, {"is_active": 0})
+ self.reboot()
+ elif omci_data.fields['result'] == 6: # Device Busy
+ self.log.debug('OMCI Activate Image Busy')
+ self.onu_busy()
+ else:
+ self.log.debug('OMCI Activate Image Failed', reason=omci_data['result'])
+ else:
+ self.log.debug('Receive Unexpected OMCI', message_type=rx_frame['message_type'])
+
+ def __activate_fail(self, fail):
+ self.log.debug("Activate software image failed", faile=fail)
+
+ def __commit_success(self, rx_frame):
+ self.log.debug("Commit software success", device_id=self._device_id)
+ self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, self._to_image, {"is_committed": 1})
+ self._omci_agent.database.set(self._device_id, SoftwareImage.class_id, self._from_image, {"is_committed": 0})
+ self.commit_ok()
+
+ def __commit_fail(self, fail):
+ self.log.debug("Commit software image failed", faile=fail)
+
+ @property
+ def status(self):
+ return self._image_download
+
+ def start(self):
+ self.log.debug("Start switch software image", target=self._to_image)
+ # self._images[0] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 0, ["is_active", "is_committed", "is_valid"])
+ # self._images[1] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 1, ["is_active", "is_committed", "is_valid"])
+ # if (self._images[self._to_image]["is_active"] == 0 and self._images[self._to_image]["is_valid"] == 1):
+ self.ret_deferred = Deferred()
+ self._image_download.image_state = ImageDownload.IMAGE_ACTIVATE
+ self.reactor.callLater(0, self.activate)
+ return self.ret_deferred
+
+ def on_enter_starting(self):
+ # self.log.debug("on_enter_starting")
+ # self._images[0] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 0, ["is_active", "is_committed", "is_valid"])
+ # self._images[1] = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 1, ["is_active", "is_committed", "is_valid"])
+ # if (self._images[self._to_image]["is_active"] != 1 and self._images[self._to_image]["is_valid"] == 1):
+ if self._retry > self._retry_max:
+ self.log.debug("failed: retry max", retries=self._retry)
+ self.fail()
+ else:
+ self.activate()
+
+ def on_enter_activating(self):
+ img = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id,
+ self._to_image, ["is_active", "is_committed", "is_valid"])
+
+ self.log.debug("on_enter_activating", instance=self._to_image, state=img)
+ if img["is_active"] == 0:
+ #if img["is_valid"] == 1:
+ self._deferred = self._device.omci_cc.send_active_image(self._to_image)
+ self._deferred.addCallbacks(self.__activate_resp_success, self.__activate_fail)
+ #else:
+ # self.fail()
+ else:
+ self.do_commit()
+
+ def on_enter_busy(self):
+ self.log.debug("on_enter_busy")
+ self.reactor.callLater(3, self.activate)
+
+ def on_enter_rebooting(self):
+ self.log.debug("on_enter_rebooting")
+ if self._timeout_dc == None:
+ self._timeout_dc = self.reactor.callLater(self._timeout, self.fail)
+
+ def on_exit_rebooting(self):
+ self.log.debug("on_exit_rebooting")
+ if self._timeout_dc and self._timeout_dc.active:
+ self._timeout_dc.cancel()
+ self._timeout_dc = None
+
+ def on_enter_committing(self):
+ # self.log.debug("on_enter_committing")
+ img = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id,
+ self._to_image, ["is_active", "is_committed", "is_valid"])
+ self.log.debug("on_enter_committing", instance=self._to_image, state=img)
+ if (img['is_active'] == 0):
+ self._retry += 1
+ self.log.debug("do retry", retry=self._retry)
+ self.reset()
+ else:
+ self._retry = 0
+ self._deferred = self._device.omci_cc.send_commit_image(self._to_image)
+ self._deferred.addCallbacks(self.__commit_success, self.__commit_fail)
+
+ def on_enter_done(self):
+ self.log.debug("on_enter_done")
+ self._image_download.image_state = ImageDownload.IMAGE_ACTIVE
+ self.ret_deferred.callback(self._to_image)
+
+ def on_enter_failed(self):
+ self.log.debug("on_enter_failed")
+ self._image_download.image_state = ImageDownload.IMAGE_REVERT
+ self.ret_deferred.errback(failure.Failure(Exception('ONU Software Activating Failed, instance ' + str(self._to_image))))
+'''
+
+###################################################################################
+## Image Agent for OLT/ONT software image handling
+###################################################################################
+class ImageAgent(object):
+ """
+ Image Agent supports multiple state machines running at the same time:
+ """
+
+ DEFAULT_LOCAL_ROOT = "/"
+
+ # def __init__(self, omci_agent, dev_id, stm_cls, img_tasks, advertise_events=True):
+ def __init__(self, omci_agent, dev_id,
+ dwld_stm_cls, dwld_img_tasks,
+ upgrade_onu_stm_cls, upgrade_onu_tasks,
+ # image_activate_stm_cls,
+ advertise_events=True, local_dir=None, clock=None):
+ """
+ Class initialization
+
+ :param omci_agent: (OpenOmciAgent) Agent
+ :param dev_id : (str) ONU Device ID
+ :param dwld_stm_cls : (ImageDownloadeSTM) Image download state machine class
+ :param dwld_img_tasks : (FileDownloadTask) file download task
+ :param upgrade_onu_stm_cls : (OmciSoftwareImageDownloadSTM) ONU Image upgrade state machine class
+ :param upgrade_onu_tasks : ({OmciSwImageUpgradeTask})
+ # :param image_activate_stm_cls: (OmciSoftwareImageActivateSTM)
+ """
+
+ self.log = structlog.get_logger(device_id=dev_id)
+
+ self._omci_agent = omci_agent
+ self._device_id = dev_id
+ self._dwld_stm_cls = dwld_stm_cls
+ # self._image_download_sm = None
+ self._images = dict()
+ self._download_task_cls = dwld_img_tasks['download-file']
+
+ self._omci_upgrade_sm_cls = upgrade_onu_stm_cls
+ self._omci_upgrade_task_cls = upgrade_onu_tasks['omci_upgrade_task']
+ self._omci_upgrade_task = None
+ self._omci_upgrade_deferred = None
+
+ # self._omci_activate_img_sm_cls = image_activate_stm_cls
+ # self._omci_activate_img_sm = None
+ self.reactor = clock if clock is not None else reactor
+
+ self._advertise_events = advertise_events
+ # self._local_dir = None
+
+ self._device = None
+ # onu_dev = self._omci_agent.get_device(self._device_id)
+ # assert device
+
+ # self._local_dir = DEFAULT_LOCAL_ROOT + onu_dev.adapter_agent.name
+ # self.log.debug("ImageAgent", local_dir=self._local_dir)
+
+
+ def __get_standby_image_instance(self):
+ instance_id = None
+ instance_0 = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 0, ["is_active", "is_committed"])
+ if instance_0['is_active'] == 1:
+ instance_id = 1
+ else:
+ instance_1 = self._omci_agent.database.query(self._device_id, SoftwareImage.class_id, 1, ["is_active", "is_committed"])
+ if instance_1['is_active'] == 1:
+ instance_id = 0
+ return instance_id
+
+ def __clear_task(self, arg):
+ self.__omci_upgrade_task = None
+
+ # def get_image(self, name, local_dir, remote_url, timeout_delay=ImageDownloadeSTM.DEFAULT_TIMEOUT_RETRY):
+ def get_image(self, image_download, timeout_delay=ImageDownloadeSTM.DEFAULT_TIMEOUT_RETRY):
+
+ """
+ Get named image from servers
+ :param image_download: (voltha_pb2.ImageDownload)
+ :param timeout_delay : (number) timeout for download task
+ :
+ :Return a Deferred that will be triggered if the file is locally availabe or downloaded sucessfully
+ : Caller will register callback and errback to the returned defer to get notified
+ """
+ self.log.debug("get_image", download=image_download)
+
+ # if self._local_dir is None:
+ # onu_dev = self._omci_agent.get_device(self._device_id)
+ # assert onu_dev
+ # if image_download.local_dir is None:
+ # self._local_dir = ImageAgent.DEFAULT_LOCAL_ROOT + onu_dev.adapter_agent.name
+ # else:
+ # self._local_dir = image_download.local_dir + '/' + onu_dev.adapter_agent.name
+
+ # self.log.debug("ImageAgent", local_dir=self._local_dir)
+ # image_download.local_dir = self._local_dir
+
+ # if os.path.isfile(self._local_dir + '/' + image_download.name): # image file exists
+ # d = Deferred()
+ # self.reactor.callLater(0, d.callback, image_download)
+ # self.log.debug("Image file exists")
+ # return d
+
+ img_dnld_sm = self._images.get(image_download.name)
+ if img_dnld_sm is None:
+ img_dnld_sm = self._dwld_stm_cls(self._omci_agent, # self._device_id, name, local_dir, remote_url,
+ image_download,
+ self._download_task_cls,
+ timeout_delay=timeout_delay,
+ clock=self.reactor
+ )
+ self._images[image_download.name] = img_dnld_sm
+
+ # if self._image_download_sm is None:
+ # self._image_download_sm = self._dwld_stm_cls(self._omci_agent, # self._device_id, name, local_dir, remote_url,
+ # image_download,
+ # self._download_task_cls,
+ # timeout_delay=timeout_delay,
+ # clock=self.reactor
+ # )
+ # else:
+ # if self._image_download_sm.download_status.state != ImageDownload.DOWNLOAD_SUCCEEDED:
+ # self._image_download_sm.reset()
+
+ d = img_dnld_sm.get_file()
+ return d
+
+ def cancel_download_image(self, name):
+ img_dnld_sm = self._images.pop(name, None)
+ if img_dnld_sm is not None:
+ img_dnld_sm.stop()
+
+
+ def onu_omci_download(self, image_dnld_name):
+ """
+ Start upgrading ONU.
+ image_dnld: (ImageDownload)
+ : Return Defer instance to get called after upgrading success or failed.
+ : Or return None if image does not exist
+ """
+ self.log.debug("onu_omci_download", image=image_dnld_name)
+
+ image_dnld_sm = self._images.get(image_dnld_name)
+ if image_dnld_sm is None:
+ return None
+
+ self._device = self._omci_agent.get_device(image_dnld_sm.status.id) if self._device is None else self._device
+
+ # if restart:
+ # self.cancel_upgrade_onu()
+
+ if self._omci_upgrade_task is None:
+ img_id = self.__get_standby_image_instance()
+ self.log.debug("start task", image_Id=img_id, task=self._omci_upgrade_sm_cls)
+ self._omci_upgrade_task = self._omci_upgrade_task_cls(img_id,
+ self._omci_upgrade_sm_cls,
+ self._omci_agent,
+ image_dnld_sm.status, clock=self.reactor)
+ self.log.debug("task created but not started")
+ # self._device.task_runner.start()
+ self._omci_upgrade_deferred = self._device.task_runner.queue_task(self._omci_upgrade_task)
+ self._omci_upgrade_deferred.addBoth(self.__clear_task)
+ return self._omci_upgrade_deferred
+
+
+ def cancel_upgrade_onu(self):
+ self.log.debug("cancel_upgrade_onu")
+ if self._omci_upgrade_task is not None:
+ self.log.debug("cancel_upgrade_onu", running=self._omci_upgrade_task.running)
+ # if self._omci_upgrade_task.running:
+ self._omci_upgrade_task.stop()
+ self._omci_upgrade_task = None
+ if self._omci_upgrade_deferred is not None:
+ self.reactor.callLater(0, self._omci_upgrade_deferred.cancel)
+ self._omci_upgrade_deferred = None
+
+
+ # def activate_onu_image(self, image_name):
+ # self.log.debug("activate_onu_image", image=image_name)
+ # img_dnld = self.get_image_status(image_name)
+ # if img_dnld is None:
+ # return None
+
+ # img_dnld.image_state = ImageDownload.IMAGE_INACTIVE
+ # if self._omci_activate_img_sm is None:
+ # self._omci_activate_img_sm = self._omci_activate_img_sm_cls(self._omci_agent, self._device_id,
+ # self.__get_standby_image_instance(),
+ # img_dnld, clock=self.reactor)
+ # return self._omci_activate_img_sm.start()
+ # else:
+ # return None
+
+ def onu_bootup(self):
+ if self._omci_upgrade_task is not None:
+ self._omci_upgrade_task.onu_bootup()
+
+ def get_image_status(self, image_name):
+ """
+ Return (ImageDownload)
+ """
+ sm = self._images.get(image_name)
+ return sm.status if sm is not None else None
+
diff --git a/python/adapters/extensions/omci/state_machines/mib_sync.py b/python/adapters/extensions/omci/state_machines/mib_sync.py
new file mode 100644
index 0000000..d257257
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/mib_sync.py
@@ -0,0 +1,942 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import structlog
+from datetime import datetime, timedelta
+from transitions import Machine
+from twisted.internet import reactor
+from voltha.extensions.omci.omci_frame import OmciFrame
+from voltha.extensions.omci.database.mib_db_api import MDS_KEY
+from voltha.extensions.omci.omci_defs import EntityOperations, ReasonCodes, \
+ AttributeAccess
+from voltha.extensions.omci.omci_cc import OmciCCRxEvents, OMCI_CC, TX_REQUEST_KEY, \
+ RX_RESPONSE_KEY
+from voltha.extensions.omci.onu_device_entry import OnuDeviceEvents, OnuDeviceEntry, \
+ SUPPORTED_MESSAGE_ENTITY_KEY, SUPPORTED_MESSAGE_TYPES_KEY
+from voltha.extensions.omci.omci_entities import OntData
+from common.event_bus import EventBusClient
+from voltha.protos.omci_mib_db_pb2 import OpenOmciEventType
+
+RxEvent = OmciCCRxEvents
+DevEvent = OnuDeviceEvents
+OP = EntityOperations
+RC = ReasonCodes
+AA = AttributeAccess
+
+
+class MibSynchronizer(object):
+ """
+ OpenOMCI MIB Synchronizer state machine
+ """
+ DEFAULT_STATES = ['disabled', 'starting', 'uploading', 'examining_mds',
+ 'in_sync', 'out_of_sync', 'auditing', 'resynchronizing']
+
+ DEFAULT_TRANSITIONS = [
+ {'trigger': 'start', 'source': 'disabled', 'dest': 'starting'},
+
+ {'trigger': 'upload_mib', 'source': 'starting', 'dest': 'uploading'},
+ {'trigger': 'examine_mds', 'source': 'starting', 'dest': 'examining_mds'},
+
+ {'trigger': 'success', 'source': 'uploading', 'dest': 'in_sync'},
+
+ {'trigger': 'success', 'source': 'examining_mds', 'dest': 'in_sync'},
+ {'trigger': 'mismatch', 'source': 'examining_mds', 'dest': 'resynchronizing'},
+
+ {'trigger': 'audit_mib', 'source': 'in_sync', 'dest': 'auditing'},
+
+ {'trigger': 'success', 'source': 'out_of_sync', 'dest': 'in_sync'},
+ {'trigger': 'audit_mib', 'source': 'out_of_sync', 'dest': 'auditing'},
+
+ {'trigger': 'success', 'source': 'auditing', 'dest': 'in_sync'},
+ {'trigger': 'mismatch', 'source': 'auditing', 'dest': 'resynchronizing'},
+ {'trigger': 'force_resync', 'source': 'auditing', 'dest': 'resynchronizing'},
+
+ {'trigger': 'success', 'source': 'resynchronizing', 'dest': 'in_sync'},
+ {'trigger': 'diffs_found', 'source': 'resynchronizing', 'dest': 'out_of_sync'},
+
+ # Do wildcard 'timeout' trigger that sends us back to start
+ {'trigger': 'timeout', 'source': '*', 'dest': 'starting'},
+
+ # Do wildcard 'stop' trigger last so it covers all previous states
+ {'trigger': 'stop', 'source': '*', 'dest': 'disabled'},
+ ]
+ DEFAULT_TIMEOUT_RETRY = 5 # Seconds to delay after task failure/timeout
+ DEFAULT_AUDIT_DELAY = 60 # Periodic tick to audit the MIB Data Sync
+ DEFAULT_RESYNC_DELAY = 300 # Periodically force a resync
+
+ def __init__(self, agent, device_id, mib_sync_tasks, db,
+ advertise_events=False,
+ states=DEFAULT_STATES,
+ transitions=DEFAULT_TRANSITIONS,
+ initial_state='disabled',
+ timeout_delay=DEFAULT_TIMEOUT_RETRY,
+ audit_delay=DEFAULT_AUDIT_DELAY,
+ resync_delay=DEFAULT_RESYNC_DELAY):
+ """
+ Class initialization
+
+ :param agent: (OpenOmciAgent) Agent
+ :param device_id: (str) ONU Device ID
+ :param db: (MibDbVolatileDict) MIB Database
+ :param advertise_events: (bool) Advertise events on OpenOMCI Event Bus
+ :param mib_sync_tasks: (dict) Tasks to run
+ :param states: (list) List of valid states
+ :param transitions: (dict) Dictionary of triggers and state changes
+ :param initial_state: (str) Initial state machine state
+ :param timeout_delay: (int/float) Number of seconds after a timeout to attempt
+ a retry (goes back to starting state)
+ :param audit_delay: (int) Seconds between MIB audits while in sync. Set to
+ zero to disable audit. An operator can request
+ an audit manually by calling 'self.audit_mib'
+ :param resync_delay: (int) Seconds in sync before performing a forced MIB
+ resynchronization
+ """
+ self.log = structlog.get_logger(device_id=device_id)
+
+ self._agent = agent
+ self._device_id = device_id
+ self._device = None
+ self._database = db
+ self._timeout_delay = timeout_delay
+ self._audit_delay = audit_delay
+ self._resync_delay = resync_delay
+
+ self._upload_task = mib_sync_tasks['mib-upload']
+ self._get_mds_task = mib_sync_tasks['get-mds']
+ self._audit_task = mib_sync_tasks['mib-audit']
+ self._resync_task = mib_sync_tasks['mib-resync']
+ self._reconcile_task = mib_sync_tasks['mib-reconcile']
+ self._advertise_events = advertise_events
+
+ self._deferred = None
+ self._current_task = None # TODO: Support multiple running tasks after v.2.0 release
+ self._task_deferred = None
+ self._mib_data_sync = 0
+ self._last_mib_db_sync_value = None
+ self._device_in_db = False
+ self._next_resync = None
+
+ self._on_olt_only_diffs = None
+ self._on_onu_only_diffs = None
+ self._attr_diffs = None
+ self._audited_olt_db = None
+ self._audited_onu_db = None
+
+ self._event_bus = EventBusClient()
+ self._omci_cc_subscriptions = { # RxEvent.enum -> Subscription Object
+ RxEvent.MIB_Reset: None,
+ RxEvent.AVC_Notification: None,
+ RxEvent.MIB_Upload: None,
+ RxEvent.MIB_Upload_Next: None,
+ RxEvent.Create: None,
+ RxEvent.Delete: None,
+ RxEvent.Set: None,
+ }
+ self._omci_cc_sub_mapping = {
+ RxEvent.MIB_Reset: self.on_mib_reset_response,
+ RxEvent.AVC_Notification: self.on_avc_notification,
+ RxEvent.MIB_Upload: self.on_mib_upload_response,
+ RxEvent.MIB_Upload_Next: self.on_mib_upload_next_response,
+ RxEvent.Create: self.on_create_response,
+ RxEvent.Delete: self.on_delete_response,
+ RxEvent.Set: self.on_set_response,
+ }
+ self._onu_dev_subscriptions = { # DevEvent.enum -> Subscription Object
+ DevEvent.OmciCapabilitiesEvent: None
+ }
+ self._onu_dev_sub_mapping = {
+ DevEvent.OmciCapabilitiesEvent: self.on_capabilities_event
+ }
+
+ # Statistics and attributes
+ # TODO: add any others if it will support problem diagnosis
+
+ # Set up state machine to manage states
+ self.machine = Machine(model=self, states=states,
+ transitions=transitions,
+ initial=initial_state,
+ queued=True,
+ name='{}-{}'.format(self.__class__.__name__,
+ device_id))
+ try:
+ import logging
+ logging.getLogger('transitions').setLevel(logging.WARNING)
+ except Exception as e:
+ self.log.exception('log-level-failed', e=e)
+
+ def _cancel_deferred(self):
+ d1, self._deferred = self._deferred, None
+ d2, self._task_deferred = self._task_deferred, None
+
+ for d in [d1, d1]:
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def __str__(self):
+ return 'MIBSynchronizer: Device ID: {}, State:{}'.format(self._device_id, self.state)
+
+ def delete(self):
+ """
+ Cleanup any state information
+ """
+ self.stop()
+ db, self._database = self._database, None
+
+ if db is not None:
+ db.remove(self._device_id)
+
+ @property
+ def device_id(self):
+ return self._device_id
+
+ @property
+ def mib_data_sync(self):
+ return self._mib_data_sync
+
+ def increment_mib_data_sync(self):
+ self._mib_data_sync += 1
+ if self._mib_data_sync > 255:
+ self._mib_data_sync = 0
+
+ if self._database is not None:
+ self._database.save_mib_data_sync(self._device_id,
+ self._mib_data_sync)
+
+ @property
+ def last_mib_db_sync(self):
+ return self._last_mib_db_sync_value
+
+ @last_mib_db_sync.setter
+ def last_mib_db_sync(self, value):
+ self._last_mib_db_sync_value = value
+ if self._database is not None:
+ self._database.save_last_sync(self.device_id, value)
+
+ @property
+ def is_new_onu(self):
+ """
+ Is this a new ONU (has never completed MIB synchronization)
+ :return: (bool) True if this ONU should be considered new
+ """
+ return self.last_mib_db_sync is None
+
+ @property
+ def advertise_events(self):
+ return self._advertise_events
+
+ @advertise_events.setter
+ def advertise_events(self, value):
+ if not isinstance(value, bool):
+ raise TypeError('Advertise event is a boolean')
+ self._advertise_events = value
+
+ def advertise(self, event, info):
+ """Advertise an event on the OpenOMCI event bus"""
+ if self._advertise_events:
+ self._agent.advertise(event,
+ {
+ 'state-machine': self.machine.name,
+ 'info': info,
+ 'time': str(datetime.utcnow())
+ })
+
+ def on_enter_disabled(self):
+ """
+ State machine is being stopped
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+
+ self._cancel_deferred()
+ if self._device is not None:
+ self._device.mib_db_in_sync = False
+
+ task, self._current_task = self._current_task, None
+ if task is not None:
+ task.stop()
+
+ # Drop Response and Autonomous notification subscriptions
+ for event, sub in self._omci_cc_subscriptions.iteritems():
+ if sub is not None:
+ self._omci_cc_subscriptions[event] = None
+ self._device.omci_cc.event_bus.unsubscribe(sub)
+
+ for event, sub in self._onu_dev_subscriptions.iteritems():
+ if sub is not None:
+ self._onu_dev_subscriptions[event] = None
+ self._device.event_bus.unsubscribe(sub)
+
+ # TODO: Stop and remove any currently running or scheduled tasks
+ # TODO: Anything else?
+
+ def _seed_database(self):
+ if not self._device_in_db:
+ try:
+ try:
+ self._database.start()
+ self._database.add(self._device_id)
+ self.log.debug('seed-db-does-not-exist', device_id=self._device_id)
+
+ except KeyError:
+ # Device already is in database
+ self.log.debug('seed-db-exist', device_id=self._device_id)
+ self._mib_data_sync = self._database.get_mib_data_sync(self._device_id)
+ self._last_mib_db_sync_value = self._database.get_last_sync(self._device_id)
+
+ self._device_in_db = True
+
+ except Exception as e:
+ self.log.exception('seed-database-failure', e=e)
+
+ def on_enter_starting(self):
+ """
+ Determine ONU status and start/re-start MIB Synchronization tasks
+ """
+ self._device = self._agent.get_device(self._device_id)
+ self.advertise(OpenOmciEventType.state_change, self.state)
+
+ # Make sure root of external MIB Database exists
+ self._seed_database()
+
+ # Set up Response and Autonomous notification subscriptions
+ try:
+ for event, sub in self._omci_cc_sub_mapping.iteritems():
+ if self._omci_cc_subscriptions[event] is None:
+ self._omci_cc_subscriptions[event] = \
+ self._device.omci_cc.event_bus.subscribe(
+ topic=OMCI_CC.event_bus_topic(self._device_id, event),
+ callback=sub)
+
+ except Exception as e:
+ self.log.exception('omci-cc-subscription-setup', e=e)
+
+ # Set up ONU device subscriptions
+ try:
+ for event, sub in self._onu_dev_sub_mapping.iteritems():
+ if self._onu_dev_subscriptions[event] is None:
+ self._onu_dev_subscriptions[event] = \
+ self._device.event_bus.subscribe(
+ topic=OnuDeviceEntry.event_bus_topic(self._device_id, event),
+ callback=sub)
+
+ except Exception as e:
+ self.log.exception('dev-subscription-setup', e=e)
+
+ # Clear any previous audit results
+ self._on_olt_only_diffs = None
+ self._on_onu_only_diffs = None
+ self._attr_diffs = None
+ self._audited_olt_db = None
+ self._audited_onu_db = None
+
+ # Determine if this ONU has ever synchronized
+ if self.is_new_onu:
+ # Start full MIB upload
+ self._deferred = reactor.callLater(0, self.upload_mib)
+
+ else:
+ # Examine the MIB Data Sync
+ self._deferred = reactor.callLater(0, self.examine_mds)
+
+ def on_enter_uploading(self):
+ """
+ Begin full MIB data upload, starting with a MIB RESET
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+
+ def success(results):
+ self.log.debug('mib-upload-success', results=results)
+ self._current_task = None
+ self._next_resync = datetime.utcnow() + timedelta(seconds=self._resync_delay)
+ self._deferred = reactor.callLater(0, self.success)
+
+ def failure(reason):
+ self.log.info('mib-upload-failure', reason=reason)
+ self._current_task = None
+ self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+ self._device.mib_db_in_sync = False
+ self._current_task = self._upload_task(self._agent, self._device_id)
+
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+
+ def on_enter_examining_mds(self):
+ """
+ Create a simple task to fetch the MIB Data Sync value and
+ determine if the ONU value matches what is in the MIB database
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+
+ self._mib_data_sync = self._database.get_mib_data_sync(self._device_id) or 0
+
+ def success(onu_mds_value):
+ self.log.debug('examine-mds-success', onu_mds_value=onu_mds_value, olt_mds_value=self.mib_data_sync)
+ self._current_task = None
+
+ # Examine MDS value
+ if self.mib_data_sync == onu_mds_value:
+ self._next_resync = datetime.utcnow() + timedelta(seconds=self._resync_delay)
+ self._deferred = reactor.callLater(0, self.success)
+ else:
+ self._deferred = reactor.callLater(0, self.mismatch)
+
+ def failure(reason):
+ self.log.info('examine-mds-failure', reason=reason)
+ self._current_task = None
+ self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+ self._device.mib_db_in_sync = False
+ self._current_task = self._get_mds_task(self._agent, self._device_id)
+
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+
+ def on_enter_in_sync(self):
+ """
+ The OLT/OpenOMCI MIB Database is in sync with the ONU MIB Database.
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self.last_mib_db_sync = datetime.utcnow()
+ self._device.mib_db_in_sync = True
+
+ if self._audit_delay > 0:
+ self._deferred = reactor.callLater(self._audit_delay, self.audit_mib)
+
+ def on_enter_out_of_sync(self):
+ """
+ The MIB in OpenOMCI and the ONU are out of sync. This can happen if:
+
+ o the MIB_Data_Sync values are not equal, or
+ o the MIBs were compared and differences were found.
+
+ Schedule a task to reconcile the differences
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+
+ # We are only out-of-sync if there were differences. If here due to MDS
+ # value differences, still run the reconcile so we up date the ONU's MDS
+ # value to match ours.
+
+ self._device.mib_db_in_sync = self._attr_diffs is None and \
+ self._on_onu_only_diffs is None and \
+ self._on_olt_only_diffs is None
+
+ def success(onu_mds_value):
+ self.log.debug('reconcile-success', mds_value=onu_mds_value)
+ self._current_task = None
+ self._next_resync = datetime.utcnow() + timedelta(seconds=self._resync_delay)
+ self._deferred = reactor.callLater(0, self.success)
+
+ def failure(reason):
+ self.log.info('reconcile-failure', reason=reason)
+ self._current_task = None
+ self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+ diff_collection = {
+ 'onu-only': self._on_onu_only_diffs,
+ 'olt-only': self._on_olt_only_diffs,
+ 'attributes': self._attr_diffs,
+ 'olt-db': self._audited_olt_db,
+ 'onu-db': self._audited_onu_db
+ }
+ # Clear out results since reconciliation task will be handling them
+ self._on_olt_only_diffs = None
+ self._on_onu_only_diffs = None
+ self._attr_diffs = None
+ self._audited_olt_db = None
+ self._audited_onu_db = None
+
+ self._current_task = self._reconcile_task(self._agent, self._device_id, diff_collection)
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+
+ def on_enter_auditing(self):
+ """
+ Perform a MIB Audit. If our last MIB resync was too long in the
+ past, perform a resynchronization anyway
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+
+ if self._next_resync is None:
+ self.log.error('next-forced-resync-error', msg='Next Resync should always be valid at this point')
+ self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+ if datetime.utcnow() >= self._next_resync:
+ self._deferred = reactor.callLater(0, self.force_resync)
+ else:
+ def success(onu_mds_value):
+ self.log.debug('audit-success', onu_mds_value=onu_mds_value, olt_mds_value=self.mib_data_sync)
+ self._current_task = None
+
+ # Examine MDS value
+ if self.mib_data_sync == onu_mds_value:
+ self._deferred = reactor.callLater(0, self.success)
+ else:
+ self._device.mib_db_in_sync = False
+ self._deferred = reactor.callLater(0, self.mismatch)
+
+ def failure(reason):
+ self.log.info('audit-failure', reason=reason)
+ self._current_task = None
+ self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+ self._current_task = self._audit_task(self._agent, self._device_id)
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+
+ def on_enter_resynchronizing(self):
+ """
+ Perform a resynchronization of the MIB database
+
+ First calculate any differences
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+
+ def success(results):
+ self.log.debug('resync-success', results=results)
+
+ on_olt_only = results.get('on-olt-only')
+ on_onu_only = results.get('on-onu-only')
+ attr_diffs = results.get('attr-diffs')
+ olt_db = results.get('olt-db')
+ onu_db = results.get('onu-db')
+
+ self._current_task = None
+ self._on_olt_only_diffs = on_olt_only if on_olt_only and len(on_olt_only) else None
+ self._on_onu_only_diffs = on_onu_only if on_onu_only and len(on_onu_only) else None
+ self._attr_diffs = attr_diffs if attr_diffs and len(attr_diffs) else None
+ self._audited_olt_db = olt_db
+ self._audited_onu_db = onu_db
+
+ mds_equal = self.mib_data_sync == self._audited_onu_db[MDS_KEY]
+
+ if mds_equal and all(diff is None for diff in [self._on_olt_only_diffs,
+ self._on_onu_only_diffs,
+ self._attr_diffs]):
+ self._next_resync = datetime.utcnow() + timedelta(seconds=self._resync_delay)
+ self._deferred = reactor.callLater(0, self.success)
+ else:
+ self._deferred = reactor.callLater(0, self.diffs_found)
+
+ def failure(reason):
+ self.log.info('resync-failure', reason=reason)
+ self._current_task = None
+ self._deferred = reactor.callLater(self._timeout_delay, self.timeout)
+
+ self._current_task = self._resync_task(self._agent, self._device_id)
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+
+ def on_mib_reset_response(self, _topic, msg):
+ """
+ Called upon receipt of a MIB Reset Response for this ONU
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-mib-reset-response', state=self.state)
+ try:
+ response = msg[RX_RESPONSE_KEY]
+
+ # Check if expected in current mib_sync state
+ if self.state != 'uploading' or self._omci_cc_subscriptions[RxEvent.MIB_Reset] is None:
+ self.log.error('rx-in-invalid-state', state=self.state)
+
+ else:
+ now = datetime.utcnow()
+
+ if not isinstance(response, OmciFrame):
+ raise TypeError('Response should be an OmciFrame')
+
+ omci_msg = response.fields['omci_message'].fields
+ status = omci_msg['success_code']
+
+ assert status == RC.Success, 'Unexpected MIB reset response status: {}'. \
+ format(status)
+
+ self._device.mib_db_in_sync = False
+ self._mib_data_sync = 0
+ self._device._modified = now
+ self._database.on_mib_reset(self._device_id)
+
+ except KeyError:
+ pass # NOP
+
+ def on_avc_notification(self, _topic, msg):
+ """
+ Process an Attribute Value Change Notification
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-avc-notification', state=self.state)
+
+ if self._omci_cc_subscriptions[RxEvent.AVC_Notification]:
+ try:
+ notification = msg[RX_RESPONSE_KEY]
+
+ if self.state == 'disabled':
+ self.log.error('rx-in-invalid-state', state=self.state)
+
+ # Inspect the notification
+ omci_msg = notification.fields['omci_message'].fields
+ class_id = omci_msg['entity_class']
+ instance_id = omci_msg['entity_id']
+ data = omci_msg['data']
+ attributes = [data.keys()]
+
+ # Look up ME Instance in Database. Not-found can occur if a MIB
+ # reset has occurred
+ info = self._database.query(self.device_id, class_id, instance_id, attributes)
+ # TODO: Add old/new info to log message
+ self.log.debug('avc-change', class_id=class_id, instance_id=instance_id)
+
+ # Save the changed data to the MIB.
+ self._database.set(self.device_id, class_id, instance_id, data)
+
+ # Autonomous creation and deletion of managed entities do not
+ # result in an increment of the MIB data sync value. However,
+ # AVC's in response to a change by the Operator do incur an
+ # increment of the MIB Data Sync. If here during uploading,
+ # we issued a MIB-Reset which may generate AVC. (TODO: Focus testing during hardening)
+ if self.state == 'uploading':
+ self.increment_mib_data_sync()
+
+ except KeyError:
+ pass # NOP
+
+ def on_mib_upload_response(self, _topic, msg):
+ """
+ Process a MIB Upload response
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-mib-upload-next-response', state=self.state)
+
+ if self._omci_cc_subscriptions[RxEvent.MIB_Upload]:
+ # Check if expected in current mib_sync state
+ if self.state == 'resynchronizing':
+ # The resync task handles this
+ # TODO: Remove this subscription if we never do anything with the response
+ return
+
+ if self.state != 'uploading':
+ self.log.error('rx-in-invalid-state', state=self.state)
+
+ def on_mib_upload_next_response(self, _topic, msg):
+ """
+ Process a MIB Upload Next response
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-mib-upload-next-response', state=self.state)
+
+ if self._omci_cc_subscriptions[RxEvent.MIB_Upload_Next]:
+ try:
+ if self.state == 'resynchronizing':
+ # The resync task handles this
+ return
+
+ # Check if expected in current mib_sync state
+ if self.state != 'uploading':
+ self.log.error('rx-in-invalid-state', state=self.state)
+
+ else:
+ response = msg[RX_RESPONSE_KEY]
+
+ # Extract entity instance information
+ omci_msg = response.fields['omci_message'].fields
+
+ class_id = omci_msg['object_entity_class']
+ entity_id = omci_msg['object_entity_id']
+
+ # Filter out the 'mib_data_sync' from the database. We save that at
+ # the device level and do not want it showing up during a re-sync
+ # during data compares
+
+ if class_id == OntData.class_id:
+ return
+
+ attributes = {k: v for k, v in omci_msg['object_data'].items()}
+
+ # Save to the database
+ self._database.set(self._device_id, class_id, entity_id, attributes)
+
+ except KeyError:
+ pass # NOP
+ except Exception as e:
+ self.log.exception('upload-next', e=e)
+
+ def on_create_response(self, _topic, msg):
+ """
+ Process a Set response
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-create-response', state=self.state)
+
+ if self._omci_cc_subscriptions[RxEvent.Create]:
+ if self.state in ['disabled', 'uploading']:
+ self.log.error('rx-in-invalid-state', state=self.state)
+ return
+ try:
+ request = msg[TX_REQUEST_KEY]
+ response = msg[RX_RESPONSE_KEY]
+ status = response.fields['omci_message'].fields['success_code']
+
+ if status != RC.Success and status != RC.InstanceExists:
+ # TODO: Support offline ONTs in post VOLTHA v1.3.0
+ omci_msg = response.fields['omci_message']
+ self.log.warn('set-response-failure',
+ class_id=omci_msg.fields['entity_class'],
+ instance_id=omci_msg.fields['entity_id'],
+ status=omci_msg.fields['success_code'],
+ status_text=self._status_to_text(omci_msg.fields['success_code']),
+ parameter_error_attributes_mask=omci_msg.fields['parameter_error_attributes_mask'])
+ else:
+ omci_msg = request.fields['omci_message'].fields
+ class_id = omci_msg['entity_class']
+ entity_id = omci_msg['entity_id']
+ attributes = {k: v for k, v in omci_msg['data'].items()}
+
+ # Save to the database
+ created = self._database.set(self._device_id, class_id, entity_id, attributes)
+
+ if created:
+ self.increment_mib_data_sync()
+
+ # If the ME contains set-by-create or writeable values that were
+ # not specified in the create command, the ONU will have
+ # initialized those fields
+
+ if class_id in self._device.me_map:
+ sbc_w_set = {attr.field.name for attr in self._device.me_map[class_id].attributes
+ if (AA.SBC in attr.access or AA.W in attr.access)
+ and attr.field.name != 'managed_entity_id'}
+
+ missing = sbc_w_set - {k for k in attributes.iterkeys()}
+
+ if len(missing):
+ # Request the missing attributes
+ self.update_sbc_w_items(class_id, entity_id, missing)
+
+ except KeyError as e:
+ pass # NOP
+
+ except Exception as e:
+ self.log.exception('create', e=e)
+
+ def update_sbc_w_items(self, class_id, entity_id, missing_attributes):
+ """
+ Perform a get-request for Set-By-Create (SBC) or writable (w) attributes
+ that were not specified in the original Create request.
+
+ :param class_id: (int) Class ID
+ :param entity_id: (int) Instance ID
+ :param missing_attributes: (set) Missing SBC or Writable attribute
+ """
+ if len(missing_attributes) and class_id in self._device.me_map:
+ from voltha.extensions.omci.tasks.omci_get_request import OmciGetRequest
+
+ self.log.info('update-sbc-items', class_id=class_id, entity_id=entity_id,
+ attributes=missing_attributes)
+
+ def success(results):
+ self._database.set(self._device_id, class_id, entity_id, results.attributes)
+
+ def failure(reason):
+ self.log.warn('update-sbc-w-failed', reason=reason, class_id=class_id,
+ entity_id=entity_id, attributes=missing_attributes)
+
+ d = self._device.task_runner.queue_task(OmciGetRequest(self._agent, self._device_id,
+ self._device.me_map[class_id],
+ entity_id, missing_attributes,
+ allow_failure=True))
+ d.addCallbacks(success, failure)
+
+ def on_delete_response(self, _topic, msg):
+ """
+ Process a Delete response
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-delete-response', state=self.state)
+
+ if self._omci_cc_subscriptions[RxEvent.Delete]:
+ if self.state in ['disabled', 'uploading']:
+ self.log.error('rx-in-invalid-state', state=self.state)
+ return
+ try:
+ request = msg[TX_REQUEST_KEY]
+ response = msg[RX_RESPONSE_KEY]
+
+ if response.fields['omci_message'].fields['success_code'] != RC.Success:
+ # TODO: Support offline ONTs in post VOLTHA v1.3.0
+ omci_msg = response.fields['omci_message']
+ self.log.warn('set-response-failure',
+ class_id=omci_msg.fields['entity_class'],
+ instance_id=omci_msg.fields['entity_id'],
+ status=omci_msg.fields['success_code'],
+ status_text=self._status_to_text(omci_msg.fields['success_code']))
+ else:
+ omci_msg = request.fields['omci_message'].fields
+ class_id = omci_msg['entity_class']
+ entity_id = omci_msg['entity_id']
+
+ # Remove from the database
+ deleted = self._database.delete(self._device_id, class_id, entity_id)
+
+ if deleted:
+ self.increment_mib_data_sync()
+
+ except KeyError as e:
+ pass # NOP
+ except Exception as e:
+ self.log.exception('delete', e=e)
+
+ def on_set_response(self, _topic, msg):
+ """
+ Process a Set response
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-set-response', state=self.state)
+
+ if self._omci_cc_subscriptions[RxEvent.Set]:
+ if self.state in ['disabled', 'uploading']:
+ self.log.error('rx-in-invalid-state', state=self.state)
+ try:
+ request = msg[TX_REQUEST_KEY]
+ response = msg[RX_RESPONSE_KEY]
+
+ if response.fields['omci_message'].fields['success_code'] != RC.Success:
+ # TODO: Support offline ONTs in post VOLTHA v1.3.0
+ omci_msg = response.fields['omci_message']
+ self.log.warn('set-response-failure',
+ class_id=omci_msg.fields['entity_class'],
+ instance_id=omci_msg.fields['entity_id'],
+ status=omci_msg.fields['success_code'],
+ status_text=self._status_to_text(omci_msg.fields['success_code']),
+ unsupported_attribute_mask=omci_msg.fields['unsupported_attributes_mask'],
+ failed_attribute_mask=omci_msg.fields['failed_attributes_mask'])
+ else:
+ omci_msg = request.fields['omci_message'].fields
+ class_id = omci_msg['entity_class']
+ entity_id = omci_msg['entity_id']
+ attributes = {k: v for k, v in omci_msg['data'].items()}
+
+ # Save to the database (Do not save 'sets' of the mib-data-sync however)
+ if class_id != OntData.class_id:
+ modified = self._database.set(self._device_id, class_id, entity_id, attributes)
+ if modified:
+ self.increment_mib_data_sync()
+
+ except KeyError as _e:
+ pass # NOP
+ except Exception as e:
+ self.log.exception('set', e=e)
+
+ # TODO: Future -> Monitor Software download start, section, activate, and commit responses
+ # and increment MIB Data Sync per Table 11.2.2-1 of ITUT-T G.988 (11/2017)
+ # on page 515. Eventually also monitor set-table responses once the
+ # extended message set is supported.
+ def on_capabilities_event(self, _topic, msg):
+ """
+ Process a OMCI capabilties event
+ :param _topic: (str) OnuDeviceEntry Capabilities event
+ :param msg: (dict) Message Entities & Message Types supported
+ """
+ self._database.update_supported_managed_entities(self.device_id,
+ msg[SUPPORTED_MESSAGE_ENTITY_KEY])
+ self._database.update_supported_message_types(self.device_id,
+ msg[SUPPORTED_MESSAGE_TYPES_KEY])
+
+ def _status_to_text(self, success_code):
+ return {
+ RC.Success: "Success",
+ RC.ProcessingError: "Processing Error",
+ RC.NotSupported: "Not Supported",
+ RC.ParameterError: "Paremeter Error",
+ RC.UnknownEntity: "Unknown Entity",
+ RC.UnknownInstance: "Unknown Instance",
+ RC.DeviceBusy: "Device Busy",
+ RC.InstanceExists: "Instance Exists"
+ }.get(success_code, 'Unknown status code: {}'.format(success_code))
+
+ def query_mib(self, class_id=None, instance_id=None, attributes=None):
+ """
+ Get MIB database information.
+
+ This method can be used to request information from the database to the detailed
+ level requested
+
+ :param class_id: (int) Managed Entity class ID
+ :param instance_id: (int) Managed Entity instance
+ :param attributes: (list or str) Managed Entity instance's attributes
+
+ :return: (dict) The value(s) requested. If class/inst/attribute is
+ not found, an empty dictionary is returned
+ :raises DatabaseStateError: If the database is not enabled or does not exist
+ """
+ from voltha.extensions.omci.database.mib_db_api import DatabaseStateError
+
+ self.log.debug('query', class_id=class_id,
+ instance_id=instance_id, attributes=attributes)
+ if self._database is None:
+ raise DatabaseStateError('Database does not yet exist')
+
+ return self._database.query(self._device_id, class_id=class_id,
+ instance_id=instance_id,
+ attributes=attributes)
+
+ def mib_set(self, class_id, entity_id, attributes):
+ """
+ Set attributes of an existing ME Class instance
+
+ This method is primarily used by other state machines to save ME specific
+ information to the persistent database. Access by objects external to the
+ OpenOMCI library is discouraged.
+
+ :param class_id: (int) ME Class ID
+ :param entity_id: (int) ME Class entity ID
+ :param attributes: (dict) attribute -> value pairs to set
+ """
+ # It must exist first (but attributes can be new)
+ if isinstance(attributes, dict) and len(attributes) and\
+ self.query_mib(class_id, entity_id) is not None:
+ self._database.set(self._device_id, class_id, entity_id, attributes)
+
+ def mib_delete(self, class_id, entity_id):
+ """
+ Delete an existing ME Class instance
+
+ This method is primarily used by other state machines to delete an ME
+ from the MIB database
+
+ :param class_id: (int) ME Class ID
+ :param entity_id: (int) ME Class entity ID
+
+ :raises KeyError: If device does not exist
+ :raises DatabaseStateError: If the database is not enabled
+ """
+ self._database.delete(self._device_id, class_id, entity_id)
diff --git a/python/adapters/extensions/omci/state_machines/omci_onu_capabilities.py b/python/adapters/extensions/omci/state_machines/omci_onu_capabilities.py
new file mode 100644
index 0000000..c13739e
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/omci_onu_capabilities.py
@@ -0,0 +1,262 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import structlog
+from transitions import Machine
+from twisted.internet import reactor
+from voltha.extensions.omci.onu_device_entry import OnuDeviceEntry, OnuDeviceEvents, IN_SYNC_KEY
+from voltha.protos.omci_mib_db_pb2 import OpenOmciEventType
+
+
+class OnuOmciCapabilities(object):
+ """
+ OpenOMCI ONU OMCI Capabilities State machine
+ """
+ DEFAULT_STATES = ['disabled', 'out_of_sync', 'in_sync', 'idle']
+
+ DEFAULT_TRANSITIONS = [
+ {'trigger': 'start', 'source': 'disabled', 'dest': 'out_of_sync'},
+ {'trigger': 'synchronized', 'source': 'out_of_sync', 'dest': 'in_sync'},
+
+ {'trigger': 'success', 'source': 'in_sync', 'dest': 'idle'},
+ {'trigger': 'failure', 'source': 'in_sync', 'dest': 'out_of_sync'},
+
+ {'trigger': 'not_synchronized', 'source': 'idle', 'dest': 'out_of_sync'},
+
+ # Do wildcard 'stop' trigger last so it covers all previous states
+ {'trigger': 'stop', 'source': '*', 'dest': 'disabled'},
+ ]
+ DEFAULT_RETRY = 10 # Seconds to delay after task failure/timeout/poll
+
+ def __init__(self, agent, device_id, tasks,
+ advertise_events=False,
+ states=DEFAULT_STATES,
+ transitions=DEFAULT_TRANSITIONS,
+ initial_state='disabled',
+ timeout_delay=DEFAULT_RETRY):
+ """
+ Class initialization
+
+ :param agent: (OpenOmciAgent) Agent
+ :param device_id: (str) ONU Device ID
+ :param tasks: (dict) Tasks to run
+ :param advertise_events: (bool) Advertise events on OpenOMCI Event Bus
+ :param states: (list) List of valid states
+ :param transitions: (dict) Dictionary of triggers and state changes
+ :param initial_state: (str) Initial state machine state
+ :param timeout_delay: (int/float) Number of seconds after a timeout or poll
+ """
+ self.log = structlog.get_logger(device_id=device_id)
+
+ self._agent = agent
+ self._device_id = device_id
+ self._device = None
+ self._timeout_delay = timeout_delay
+
+ self._get_capabilities_task = tasks['get-capabilities']
+ self._advertise_events = advertise_events
+
+ self._deferred = None
+ self._current_task = None
+ self._task_deferred = None
+ self._supported_entities = frozenset()
+ self._supported_msg_types = frozenset()
+
+ self._subscriptions = { # RxEvent.enum -> Subscription Object
+ OnuDeviceEvents.MibDatabaseSyncEvent: None
+ }
+ self._sub_mapping = {
+ OnuDeviceEvents.MibDatabaseSyncEvent: self.on_mib_sync_event
+ }
+ # Statistics and attributes
+ # TODO: add any others if it will support problem diagnosis
+
+ # Set up state machine to manage states
+ self.machine = Machine(model=self, states=states,
+ transitions=transitions,
+ initial=initial_state,
+ queued=True,
+ name='{}-{}'.format(self.__class__.__name__,
+ device_id))
+
+ def _cancel_deferred(self):
+ d1, self._deferred = self._deferred, None
+ d2, self._task_deferred = self._task_deferred, None
+
+ for d in [d1, d2]:
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def _cancel_tasks(self):
+ task, self._current_task = self._current_task, None
+ if task is not None:
+ task.stop()
+
+ def __str__(self):
+ return 'OnuOmciCapabilities: Device ID: {}, State:{}'.format(self._device_id, self.state)
+
+ def delete(self):
+ """
+ Cleanup any state information
+ """
+ self.stop()
+
+ @property
+ def device_id(self):
+ return self._device_id
+
+ @property
+ def supported_managed_entities(self):
+ """
+ Return a set of the Managed Entity class IDs supported on this ONU
+ None is returned if no MEs have been discovered
+
+ :return: (set of ints)
+ """
+ return self._supported_entities if len(self._supported_entities) else None
+
+ @property
+ def supported_message_types(self):
+ """
+ Return a set of the Message Types supported on this ONU
+ None is returned if no message types have been discovered
+
+ :return: (set of EntityOperations)
+ """
+ return self._supported_msg_types if len(self._supported_msg_types) else None
+
+ @property
+ def advertise_events(self):
+ return self._advertise_events
+
+ @advertise_events.setter
+ def advertise_events(self, value):
+ if not isinstance(value, bool):
+ raise TypeError('Advertise event is a boolean')
+ self._advertise_events = value
+
+ def advertise(self, event, info):
+ """Advertise an event on the OpenOMCI event bus"""
+ from datetime import datetime
+
+ if self._advertise_events:
+ self._agent.advertise(event,
+ {
+ 'state-machine': self.machine.name,
+ 'info': info,
+ 'time': str(datetime.utcnow())
+ })
+
+ def on_enter_disabled(self):
+ """
+ State machine is being stopped
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self._cancel_deferred()
+ self._cancel_tasks()
+
+ self._supported_entities = frozenset()
+ self._supported_msg_types = frozenset()
+
+ # Drop Response and Autonomous notification subscriptions
+ for event, sub in self._subscriptions.iteritems():
+ if sub is not None:
+ self._subscriptions[event] = None
+ self._device.event_bus.unsubscribe(sub)
+
+ def on_enter_out_of_sync(self):
+ """
+ State machine has just started or the MIB database has transitioned
+ to an out-of-synchronization state
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self._cancel_deferred()
+ self._device = self._agent.get_device(self._device_id)
+
+ # Subscribe to events of interest
+ try:
+ for event, sub in self._sub_mapping.iteritems():
+ if self._subscriptions[event] is None:
+ self._subscriptions[event] = \
+ self._device.event_bus.subscribe(
+ topic=OnuDeviceEntry.event_bus_topic(self._device_id,
+ event),
+ callback=sub)
+
+ except Exception as e:
+ self.log.exception('subscription-setup', e=e)
+
+ # Periodically check/poll for in-sync in case subscription was missed or
+ # already in sync
+ self._deferred = reactor.callLater(0, self.check_in_sync)
+
+ def check_in_sync(self):
+ if self._device.mib_db_in_sync:
+ self.synchronized()
+ else:
+ self._deferred = reactor.callLater(self._timeout_delay,
+ self.check_in_sync)
+
+ def on_enter_in_sync(self):
+ """
+ State machine has just transitioned to an in-synchronization state
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self._cancel_deferred()
+
+ def success(results):
+ self.log.debug('capabilities-success', results=results)
+ self._supported_entities = self._current_task.supported_managed_entities
+ self._supported_msg_types = self._current_task.supported_message_types
+ self._current_task = None
+ self._deferred = reactor.callLater(0, self.success)
+
+ def failure(reason):
+ self.log.info('capabilities-failure', reason=reason)
+ self._supported_entities = frozenset()
+ self._supported_msg_types = frozenset()
+ self._current_task = None
+ self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+
+ # Schedule a task to read the ONU's OMCI capabilities
+ self._current_task = self._get_capabilities_task(self._agent, self._device_id)
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+
+ def on_enter_idle(self):
+ """
+ Notify any subscribers for a capabilities event and wait until
+ stopped or ONU MIB database goes out of sync
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self._cancel_deferred()
+ self._device.publish_omci_capabilities_event()
+
+ def on_mib_sync_event(self, _topic, msg):
+ """
+ Handle In-Sync/Out-of-Sync for the MIB database
+ :param _topic: (str) Subscription topic
+ :param msg: (dict) In-Sync event data
+ """
+ if self._subscriptions.get(OnuDeviceEvents.MibDatabaseSyncEvent) is None:
+ return
+
+ if msg[IN_SYNC_KEY]:
+ self.synchronized()
+ else:
+ self.not_synchronized()
diff --git a/python/adapters/extensions/omci/state_machines/performance_intervals.py b/python/adapters/extensions/omci/state_machines/performance_intervals.py
new file mode 100644
index 0000000..78cfa74
--- /dev/null
+++ b/python/adapters/extensions/omci/state_machines/performance_intervals.py
@@ -0,0 +1,904 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import structlog
+import arrow
+from transitions import Machine
+from datetime import datetime, timedelta
+from random import uniform, shuffle
+from twisted.internet import reactor
+from common.utils.indexpool import IndexPool
+from voltha.protos.omci_mib_db_pb2 import OpenOmciEventType
+from voltha.extensions.omci.omci_defs import EntityOperations, ReasonCodes
+from voltha.extensions.omci.omci_cc import OmciCCRxEvents, OMCI_CC, TX_REQUEST_KEY, \
+ RX_RESPONSE_KEY
+from voltha.extensions.omci.database.mib_db_api import ATTRIBUTES_KEY
+from voltha.extensions.omci.tasks.omci_get_request import OmciGetRequest
+from voltha.extensions.omci.omci_entities import MacBridgePortConfigurationData
+from voltha.extensions.omci.omci_entities import EthernetPMMonitoringHistoryData, \
+ FecPerformanceMonitoringHistoryData, \
+ XgPonTcPerformanceMonitoringHistoryData, \
+ XgPonDownstreamPerformanceMonitoringHistoryData, \
+ XgPonUpstreamPerformanceMonitoringHistoryData, \
+ EthernetFrameUpstreamPerformanceMonitoringHistoryData, \
+ EthernetFrameDownstreamPerformanceMonitoringHistoryData, \
+ EthernetFrameExtendedPerformanceMonitoring, \
+ EthernetFrameExtendedPerformanceMonitoring64Bit, AniG
+
+
+RxEvent = OmciCCRxEvents
+OP = EntityOperations
+RC = ReasonCodes
+
+
+class PerformanceIntervals(object):
+ """
+ OpenOMCI ONU Performance Monitoring Intervals State machine
+
+ This state machine focuses on L2 Internet Data Service and Classical
+ PM (for the v2.0 release).
+ """
+ DEFAULT_STATES = ['disabled', 'starting', 'synchronize_time', 'idle', 'create_pm_me',
+ 'collect_data', 'threshold_exceeded']
+
+ DEFAULT_TRANSITIONS = [
+ {'trigger': 'start', 'source': 'disabled', 'dest': 'starting'},
+ {'trigger': 'tick', 'source': 'starting', 'dest': 'synchronize_time'},
+
+ {'trigger': 'success', 'source': 'synchronize_time', 'dest': 'idle'},
+ {'trigger': 'failure', 'source': 'synchronize_time', 'dest': 'synchronize_time'},
+
+ {'trigger': 'tick', 'source': 'idle', 'dest': 'collect_data'},
+ {'trigger': 'add_me', 'source': 'idle', 'dest': 'create_pm_me'},
+ {'trigger': 'delete_me', 'source': 'idle', 'dest': 'delete_pm_me'},
+
+ # TODO: Can these be combined into one?
+ {'trigger': 'success', 'source': 'create_pm_me', 'dest': 'idle'},
+ {'trigger': 'failure', 'source': 'create_pm_me', 'dest': 'idle'},
+
+ # TODO: Can these be combined into one?
+ {'trigger': 'success', 'source': 'delete_pm_me', 'dest': 'idle'},
+ {'trigger': 'failure', 'source': 'delete_pm_me', 'dest': 'idle'},
+
+ # TODO: Can these be combined into one?
+ {'trigger': 'success', 'source': 'collect_data', 'dest': 'idle'},
+ {'trigger': 'failure', 'source': 'collect_data', 'dest': 'idle'},
+
+ # TODO: Add rebooted event transitions to disabled or synchronize_time
+ # TODO: Need to capture Threshold Crossing Alarms appropriately
+
+ # Do wildcard 'stop' trigger last so it covers all previous states
+ {'trigger': 'stop', 'source': '*', 'dest': 'disabled'},
+ {'trigger': 'reboot', 'source': '*', 'dest': 'rebooted'},
+ ]
+ DEFAULT_RETRY = 10 # Seconds to delay after task failure/timeout/poll
+ DEFAULT_TICK_DELAY = 15 # Seconds between checks for collection tick
+ DEFAULT_INTERVAL_SKEW = 10 * 60 # Seconds to skew past interval boundary
+ DEFAULT_COLLECT_ATTEMPTS = 3 # Maximum number of collection fetch attempts
+ DEFAULT_CREATE_ATTEMPTS = 15 # Maximum number of attempts to create a PM Managed Entities
+
+ def __init__(self, agent, device_id, tasks,
+ advertise_events=False,
+ states=DEFAULT_STATES,
+ transitions=DEFAULT_TRANSITIONS,
+ initial_state='disabled',
+ timeout_delay=DEFAULT_RETRY,
+ tick_delay=DEFAULT_TICK_DELAY,
+ interval_skew=DEFAULT_INTERVAL_SKEW,
+ collect_attempts=DEFAULT_COLLECT_ATTEMPTS,
+ create_attempts=DEFAULT_CREATE_ATTEMPTS):
+ """
+ Class initialization
+
+ :param agent: (OpenOmciAgent) Agent
+ :param device_id: (str) ONU Device ID
+ :param tasks: (dict) Tasks to run
+ :param advertise_events: (bool) Advertise events on OpenOMCI Event Bus
+ :param states: (list) List of valid states
+ :param transitions: (dict) Dictionary of triggers and state changes
+ :param initial_state: (str) Initial state machine state
+ :param timeout_delay: (int/float) Number of seconds after a timeout to pause
+ :param tick_delay: (int/float) Collection poll check delay while idle
+ :param interval_skew: (int/float) Seconds to randomly skew the next interval
+ collection to spread out requests for PM intervals
+ :param collect_attempts: (int) Max requests for a single PM interval before fail
+ :param create_attempts: (int) Max attempts to create PM Managed entities before stopping state machine
+ """
+ self.log = structlog.get_logger(device_id=device_id)
+
+ self._agent = agent
+ self._device_id = device_id
+ self._device = None
+ self._pm_config = None
+ self._timeout_delay = timeout_delay
+ self._tick_delay = tick_delay
+ self._interval_skew = interval_skew
+ self._collect_attempts = collect_attempts
+ self._create_attempts = create_attempts
+
+ self._sync_time_task = tasks['sync-time']
+ self._get_interval_task = tasks['collect-data']
+ self._create_pm_task = tasks['create-pm']
+ self._delete_pm_task = tasks['delete-pm']
+ self._advertise_events = advertise_events
+
+ self._omci_cc_subscriptions = { # RxEvent.enum -> Subscription Object
+ RxEvent.MIB_Reset: None,
+ RxEvent.Create: None,
+ RxEvent.Delete: None
+ }
+ self._omci_cc_sub_mapping = {
+ RxEvent.MIB_Reset: self.on_mib_reset_response,
+ RxEvent.Create: self.on_create_response,
+ RxEvent.Delete: self.on_delete_response,
+ }
+ self._me_watch_list = {
+ MacBridgePortConfigurationData.class_id: {
+ 'create-delete': self.add_remove_enet_frame_pm,
+ 'instances': dict() # BP entity_id -> (PM class_id, PM entity_id)
+ }
+ }
+ self._deferred = None
+ self._task_deferred = None
+ self._current_task = None
+ self._add_me_deferred = None
+ self._delete_me_deferred = None
+ self._next_interval = None
+ self._enet_entity_id = IndexPool(1024, 1)
+ self._add_pm_me_retry = 0
+
+ # (Class ID, Instance ID) -> Collect attempts remaining
+ self._pm_me_collect_retries = dict()
+ self._pm_me_extended_info = dict()
+ self._add_pm_me = dict() # (pm cid, pm eid) -> (me cid, me eid, upstream)
+ self._del_pm_me = set()
+
+ # Pollable PM items
+ # Note that some items the KPI extracts are not listed below. These are the
+ # administrative states, operational states, and sensed ethernet type. The values
+ # in the MIB database should be accurate for these items.
+
+ self._ani_g_items = ["optical_signal_level", "transmit_optical_level"]
+ self._next_poll_time = datetime.utcnow()
+ self._poll_interval = 60 # TODO: Fixed at once a minute
+
+ # Statistics and attributes
+ # TODO: add any others if it will support problem diagnosis
+
+ # Set up state machine to manage states
+ self.machine = Machine(model=self, states=states,
+ transitions=transitions,
+ initial=initial_state,
+ queued=True,
+ ignore_invalid_triggers=True,
+ name='{}-{}'.format(self.__class__.__name__,
+ device_id))
+ try:
+ import logging
+ logging.getLogger('transitions').setLevel(logging.WARNING)
+ except Exception as e:
+ self.log.exception('log-level-failed', e=e)
+
+
+ def _cancel_deferred(self):
+ d1, self._deferred = self._deferred, None
+ d2, self._task_deferred = self._task_deferred, None
+ d3, self._add_me_deferred = self._add_me_deferred, None
+ d4, self._delete_me_deferred = self._delete_me_deferred, None
+
+ for d in [d1, d2, d3, d4]:
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def _cancel_tasks(self):
+ task, self._current_task = self._current_task, None
+ if task is not None:
+ task.stop()
+
+ def __str__(self):
+ return 'PerformanceIntervals: Device ID: {}, State:{}'.format(self._device_id,
+ self.state)
+
+ def delete(self):
+ """
+ Cleanup any state information
+ """
+ self.stop()
+
+ @property
+ def device_id(self):
+ return self._device_id
+
+ @property
+ def advertise_events(self):
+ return self._advertise_events
+
+ @advertise_events.setter
+ def advertise_events(self, value):
+ if not isinstance(value, bool):
+ raise TypeError('Advertise event is a boolean')
+ self._advertise_events = value
+
+ def advertise(self, event, info):
+ """Advertise an event on the OpenOMCI event bus"""
+ if self._advertise_events:
+ self._agent.advertise(event,
+ {
+ 'state-machine': self.machine.name,
+ 'info': info,
+ 'time': str(datetime.utcnow()),
+ 'next': str(self._next_interval)
+ })
+
+ def set_pm_config(self, pm_config):
+ """
+ Set PM interval configuration
+
+ :param pm_config: (OnuPmIntervalMetrics) PM Interval configuration
+ :return:
+ """
+ self._pm_config = pm_config
+
+ def _me_is_supported(self, class_id):
+ """
+ Check to see if ONU supports this ME
+ :param class_id: (int) ME Class ID
+ :return: (bool) If ME is supported
+ """
+ #
+ supported = self._device.omci_capabilities.supported_managed_entities
+ return class_id in supported if supported is not None else False
+
+ def add_pm_me(self, pm_class_id, pm_entity_id, cid=0, eid=0, upstream=False):
+ """
+ Add a new Performance Monitoring ME.
+
+ The ME ID will be added to an internal list and will be added the next
+ time the idle state is reached. An 'add_pm_me' trigger will be raised in
+ case already in the Idle state.
+
+ :param pm_class_id: (int) ME Class ID (1..0xFFFE)
+ :param pm_entity_id: (int) Instance ID (1..0xFFFE)
+ :param cid: (int) Class ID of entity monitored, may be None
+ :param eid: (int) Instance ID of entity monitored, may be None
+ :param upstream: (bool): Flag indicating if PM is for upstream traffic
+ """
+ if not isinstance(pm_class_id, int):
+ raise TypeError('PM ME Instance ID is an integer')
+ if not 0 < pm_class_id < 0xFFFF:
+ raise ValueError('PM ME Instance ID must be 1..65534')
+
+ # Check to see if ONU supports this ME
+ if not self._me_is_supported(pm_class_id):
+ self.log.warn('unsupported-PM-me', class_id=pm_class_id)
+ return
+
+ key = (pm_class_id, pm_entity_id)
+ entry = (cid, eid, upstream)
+
+ if key not in self._pm_me_collect_retries and key not in self._add_pm_me:
+ self._add_pm_me[key] = entry
+
+ if self._add_me_deferred is None:
+ self._add_me_deferred = reactor.callLater(0, self.add_me)
+
+ if (pm_class_id, pm_entity_id) in self._del_pm_me:
+ self._del_pm_me.remove((pm_class_id, pm_entity_id))
+
+ def delete_pm_me(self, class_id, entity_id):
+ """
+ Remove a new Performance Monitoring ME.
+
+ The ME ID will be added to an internal list and will be removed the next
+ time the idle state is reached. An 'delete_pm_me' trigger will be raised in
+ case already in the Idle state.
+
+ :param class_id: (int) ME Class ID (1..0xFFFE)
+ :param entity_id: (int) Instance ID (1..0xFFFE)
+ """
+ if not isinstance(class_id, int):
+ raise TypeError('PM ME Class ID is an integer')
+ if not 0 < class_id < 0xFFFF:
+ raise ValueError('PM ME Class ID must be 1..65534')
+
+ # Check to see if ONU supports this ME
+ if not self._me_is_supported(class_id):
+ self.log.warn('unsupported-PM-me', class_id=class_id)
+ return
+
+ key = (class_id, entity_id)
+
+ if key in self._pm_me_collect_retries and key not in self._del_pm_me:
+ self._del_pm_me.add(key)
+
+ if self._delete_me_deferred is None:
+ self._delete_me_deferred = reactor.callLater(0, self.delete_me)
+
+ if key in self._add_pm_me:
+ self._add_pm_me.pop(key)
+
+ def on_enter_disabled(self):
+ """
+ State machine is being stopped
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self._cancel_deferred()
+ self._cancel_tasks()
+ self._next_interval = None
+
+ # Drop OMCI ME Response subscriptions
+ for event, sub in self._omci_cc_subscriptions.iteritems():
+ if sub is not None:
+ self._omci_cc_subscriptions[event] = None
+ self._device.omci_cc.event_bus.unsubscribe(sub)
+
+ # Manually remove ani ANI/PON and UNI PM interval MEs
+ config = self._device.configuration
+ anis = config.ani_g_entities
+ unis = config.uni_g_entities
+
+ if anis is not None:
+ for entity_id in anis.iterkeys():
+ self.delete_pm_me(FecPerformanceMonitoringHistoryData.class_id, entity_id)
+ self.delete_pm_me(XgPonTcPerformanceMonitoringHistoryData.class_id, entity_id)
+ self.delete_pm_me(XgPonDownstreamPerformanceMonitoringHistoryData.class_id, entity_id)
+ self.delete_pm_me(XgPonUpstreamPerformanceMonitoringHistoryData.class_id, entity_id)
+
+ if unis is not None:
+ for entity_id in config.uni_g_entities.iterkeys():
+ self.delete_pm_me(EthernetPMMonitoringHistoryData.class_id, entity_id)
+
+ def on_enter_starting(self):
+ """ Add the PON/ANI and UNI PM intervals"""
+ self.advertise(OpenOmciEventType.state_change, self.state)
+
+ self._device = self._agent.get_device(self._device_id)
+ self._cancel_deferred()
+
+ # Set up OMCI ME Response subscriptions
+ try:
+ for event, sub in self._omci_cc_sub_mapping.iteritems():
+ if self._omci_cc_subscriptions[event] is None:
+ self._omci_cc_subscriptions[event] = \
+ self._device.omci_cc.event_bus.subscribe(
+ topic=OMCI_CC.event_bus_topic(self._device_id, event),
+ callback=sub)
+
+ except Exception as e:
+ self.log.exception('omci-cc-subscription-setup', e=e)
+
+ try:
+ # Manually start some ANI/PON and UNI PM interval MEs
+ config = self._device.configuration
+ anis = config.ani_g_entities
+ unis = config.uni_g_entities
+
+ if anis is not None:
+ for entity_id in anis.iterkeys():
+ self.add_pm_me(FecPerformanceMonitoringHistoryData.class_id,
+ entity_id)
+ self.add_pm_me(XgPonTcPerformanceMonitoringHistoryData.class_id,
+ entity_id)
+ self.add_pm_me(XgPonDownstreamPerformanceMonitoringHistoryData.class_id,
+ entity_id)
+ self.add_pm_me(XgPonUpstreamPerformanceMonitoringHistoryData.class_id,
+ entity_id)
+
+ if unis is not None:
+ for entity_id in config.uni_g_entities.iterkeys():
+ self.add_pm_me(EthernetPMMonitoringHistoryData.class_id, entity_id)
+
+ # Look for existing instances of dynamically created ME's that have PM
+ # associated with them and add them now
+ for class_id in self._me_watch_list.iterkeys():
+ instances = {k: v for k, v in
+ self._device.query_mib(class_id=class_id).items()
+ if isinstance(k, int)}
+
+ for entity_id, data in instances.items():
+ method = self._me_watch_list[class_id]['create-delete']
+ cid, eid = method(None, class_id, entity_id,
+ add=True, attributes=data[ATTRIBUTES_KEY])
+ if cid > 0:
+ # BP entity_id -> (PM class_id, PM entity_id)
+ instances = self._me_watch_list[class_id]['instances']
+ instances[entity_id] = (cid, eid)
+
+ except Exception as e:
+ self.log.exception('pm-me-setup', class_id=class_id, e=e)
+
+ # Got to synchronize_time state
+ self._deferred = reactor.callLater(0, self.tick)
+
+ def on_enter_synchronize_time(self):
+ """
+ State machine has just transitioned to the synchronize_time state
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self._cancel_deferred()
+
+ def success(_results):
+ self.log.debug('sync-time-success')
+ self._current_task = None
+ self._deferred = reactor.callLater(0, self.success)
+ # Calculate next interval time
+ self._next_interval = self.get_next_interval
+
+ def failure(reason):
+ self.log.info('sync-time-failure', reason=reason)
+ self._current_task = None
+ self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+
+ # Schedule a task to set the ONU time
+ self._current_task = self._sync_time_task(self._agent, self._device_id)
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+
+ def on_enter_idle(self):
+ """
+ State machine has just transitioned to the idle state
+
+ In this state, any added PM MEs that need to be created will be.
+ TODO: some non-interval PM stats (if there are any) are collected here
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self._cancel_deferred()
+
+ if len(self._del_pm_me) and self._delete_me_deferred is None:
+ self._delete_me_deferred = reactor.callLater(0, self.delete_me)
+
+ elif len(self._add_pm_me) and self._add_me_deferred is None:
+ self._add_me_deferred = reactor.callLater(0, self.add_me)
+
+ elif datetime.utcnow() >= self._next_poll_time:
+ def success(results):
+ self._device.timestamp = arrow.utcnow().float_timestamp
+ self._device.mib_synchronizer.mib_set(results.me_class.class_id,
+ results.entity_id,
+ results.attributes)
+ self._next_poll_time = datetime.utcnow() + timedelta(seconds=self._poll_interval)
+
+ def failure(reason):
+ self.log.info('poll-failure', reason=reason)
+ self._device.timestamp = None
+ return None
+
+ # Scan all ANI-G ports
+ ani_g_entities = self._device.configuration.ani_g_entities
+ ani_g_entities_ids = ani_g_entities.keys() if ani_g_entities is not None else None
+
+ if ani_g_entities_ids is not None and len(ani_g_entities_ids):
+ for entity_id in ani_g_entities_ids:
+ task = OmciGetRequest(self._agent, self.device_id,
+ AniG, entity_id,
+ self._ani_g_items, allow_failure=True)
+ self._task_deferred = self._device.task_runner.queue_task(task)
+ self._task_deferred.addCallbacks(success, failure)
+ else:
+ self.log.warn('poll-pm-no-anis')
+ self._next_poll_time = datetime.utcnow() + timedelta(seconds=self._poll_interval)
+
+ # TODO: Compute a better mechanism than just polling here, perhaps based on
+ # the next time to fetch data for 'any' interval
+ self._deferred = reactor.callLater(self._tick_delay, self.tick)
+
+ def on_enter_create_pm_me(self):
+ """
+ State machine has just transitioned to the create_pm_me state
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self._cancel_deferred()
+ self._cancel_tasks()
+ mes, self._add_pm_me = self._add_pm_me, dict()
+
+ def success(results):
+ self.log.debug('create-me-success', results=results)
+
+ # Check if already here. The create request could have received
+ # an already-exists status code which we consider successful
+ for pm, me in mes.items():
+ self._pm_me_collect_retries[pm] = self.pm_collected(pm)
+ self._pm_me_extended_info[pm] = me
+
+ self._current_task = None
+ self._deferred = reactor.callLater(0, self.success)
+
+ def failure(reason):
+ self.log.info('create-me-failure', reason=reason, retries=self._add_pm_me_retry)
+ self._current_task = None
+ if self._add_pm_me_retry <= self._create_attempts:
+ for pm, me in mes.items():
+ self._add_pm_me[pm] = me
+ self._add_pm_me_retry += 1
+ self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+ else:
+ # we cant seem to create any collection me, no point in doing anything
+ self.log.warn('unable-to-create-pm-me-disabling-collection', reason=reason, device_id=self._device_id)
+ self._deferred = reactor.callLater(self._timeout_delay, self.stop)
+
+ self._current_task = self._create_pm_task(self._agent, self._device_id, mes)
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+
+ def on_enter_delete_pm_me(self):
+ """
+ State machine has just transitioned to the delete_pm_me state
+ """
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self._cancel_deferred()
+ self._cancel_tasks()
+
+ mes, self._del_pm_me = self._del_pm_me, set()
+
+ def success(results):
+ self.log.debug('delete-me-success', results=results)
+ self._current_task = None
+ for me in mes:
+ self._pm_me_collect_retries.pop(me)
+
+ self._deferred = reactor.callLater(0, self.success)
+
+ def failure(reason):
+ self.log.info('delete-me-failure', reason=reason)
+ self._current_task = None
+ for me in mes:
+ self._del_pm_me.add(me)
+
+ self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+
+ self._current_task = self._delete_pm_task(self._agent, self._device_id, mes)
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+
+ def on_enter_collect_data(self):
+ """
+ State machine has just transitioned to the collect_data state
+ """
+
+ if self._next_interval is not None and self._next_interval > datetime.utcnow():
+ self.log.debug('wait-next-interval')
+ # Not ready for next interval, transition back to idle and we should get
+ # called again after a short delay
+ reactor.callLater(0, self.success)
+ return
+
+ self.advertise(OpenOmciEventType.state_change, self.state)
+ self._cancel_deferred()
+ self._cancel_tasks()
+ keys = self._pm_me_collect_retries.keys()
+ shuffle(keys)
+
+ for key in keys:
+ class_id = key[0]
+ entity_id = key[1]
+
+ self.log.debug("in-enter-collect-data", data_key=key,
+ retries=self._pm_me_collect_retries[key])
+
+ # Collect the data ?
+ if self._pm_me_collect_retries[key] > 0:
+ def success(results):
+ self.log.debug('collect-success', results=results,
+ class_id=results.get('class_id'),
+ entity_id=results.get('entity_id'))
+ self._current_task = None
+ self._pm_me_collect_retries[key] = 0
+ self._deferred = reactor.callLater(0, self.success)
+ return results
+
+ def failure(reason):
+ self.log.info('collect-failure', reason=reason)
+ self._current_task = None
+ self._pm_me_collect_retries[key] -= 1
+ self._deferred = reactor.callLater(self._timeout_delay, self.failure)
+ return reason # Halt callback processing
+
+ # start the task
+ if key in self._pm_me_extended_info:
+ self.log.debug('collect-extended-info-found', data_key=key,
+ extended_info=self._pm_me_extended_info[key])
+ parent_class_id = self._pm_me_extended_info[key][0]
+ parent_entity_id = self._pm_me_extended_info[key][1]
+ upstream = self._pm_me_extended_info[key][2]
+ else:
+ self.log.debug('collect-extended-info-not-found', data_key=key)
+ parent_class_id = None
+ parent_entity_id = None
+ upstream = None
+
+ self._current_task = self._get_interval_task(self._agent, self._device_id,
+ class_id, entity_id,
+ parent_class_id=parent_class_id,
+ parent_entity_id=parent_entity_id,
+ upstream=upstream)
+ self._task_deferred = self._device.task_runner.queue_task(self._current_task)
+ self._task_deferred.addCallbacks(success, failure)
+ self._task_deferred.addCallback(self.publish_data)
+ return
+
+ # Here if all intervals have been collected (we are up to date)
+ self._next_interval = self.get_next_interval
+ self.log.debug('collect-calculate-next', next=self._next_interval)
+
+ self._pm_me_collect_retries = dict.fromkeys(self._pm_me_collect_retries, self._collect_attempts)
+ reactor.callLater(0, self.success)
+
+ def on_enter_threshold_exceeded(self):
+ """
+ State machine has just transitioned to the threshold_exceeded state
+ """
+ pass # TODO: Not sure if we want this state. Need to get alarm synchronizer working first
+
+ @property
+ def get_next_interval(self):
+ """
+ Determine the time for the next interval collection for all of this
+ ONUs PM Intervals. Earliest fetch time is at least 1 minute into the
+ next interval.
+
+ :return: (datetime) UTC time to get the next interval
+ """
+ now = datetime.utcnow()
+
+ # Get delta seconds to at least 1 minute into next interval
+ next_delta_secs = (16 - (now.minute % 15)) * 60
+ next_interval = now + timedelta(seconds=next_delta_secs)
+
+ # NOTE: For debugging, uncomment next section to perform collection
+ # right after initial code startup/mib-sync
+ if self._next_interval is None:
+ return now # Do it now (just for debugging purposes)
+
+ # Skew the next time up to the maximum specified
+ # TODO: May want to skew in a shorter range and select the minute
+ # based off some device property value to make collection a
+ # little more predictable on a per-ONU basis.
+ return next_interval + timedelta(seconds=uniform(0, self._interval_skew))
+
+ def pm_collected(self, key):
+ """
+ Query database and determine if PM data needs to be collected for this ME
+ """
+ class_id = key[0]
+ entity_id = key[1]
+
+ return self._collect_attempts # TODO: Implement persistent storage
+
+ def publish_data(self, results):
+ """
+ Publish the PM interval results on the appropriate bus. The results are
+ a dictionary with the following format.
+
+ 'class-id': (int) ME Class ID,
+ 'entity-id': (int) ME Entity ID,
+ 'me-name': (str) ME Class name, # Mostly for debugging...
+ 'interval-end-time': None,
+ 'interval-utc-time': (DateTime) UTC time when retrieved from ONU,
+
+ Counters added here as they are retrieved with the format of
+ 'counter-attribute-name': value (int)
+
+ :param results: (dict) PM results
+ """
+ self.log.debug('collect-publish', results=results)
+
+ if self._pm_config is not None:
+ self._pm_config.publish_metrics(results)
+
+ pass # TODO: Save off last time interval fetched to persistent storage?
+
+ def on_mib_reset_response(self, _topic, msg):
+ """
+ Called upon receipt of a MIB Reset Response for this ONU
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-mib-reset-response', state=self.state)
+ try:
+ response = msg[RX_RESPONSE_KEY]
+ omci_msg = response.fields['omci_message'].fields
+ status = omci_msg['success_code']
+
+ if status == RC.Success:
+ for class_id in self._me_watch_list.iterkeys():
+ # BP entity_id -> (PM class_id, PM entity_id)
+ instances = self._me_watch_list[class_id]['instances']
+ for _, me_pair in instances.items():
+ self._me_watch_list[class_id]['create-delete'](None, me_pair[0],
+ me_pair[1], add=False)
+ self._me_watch_list[class_id]['instances'] = dict()
+
+ except KeyError:
+ pass # NOP
+
+ def on_create_response(self, _topic, msg):
+ """
+ Called upon receipt of a Create Response for this ONU.
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-create-response', state=self.state)
+
+ def valid_request(stat, c_id, e_id):
+ return self._omci_cc_subscriptions[RxEvent.Delete] is not None\
+ and stat in (RC.Success, RC.InstanceExists) \
+ and c_id in self._me_watch_list.keys() \
+ and e_id not in self._me_watch_list[c_id]['instances']
+
+ response = msg[RX_RESPONSE_KEY]
+ omci = response.fields['omci_message'].fields
+ class_id = omci['entity_class']
+ entity_id = omci['entity_id']
+ status = omci['success_code']
+
+ if valid_request(status, class_id, entity_id):
+ request = msg[TX_REQUEST_KEY]
+ method = self._me_watch_list[class_id]['create-delete']
+ cid, eid = method(request, class_id, entity_id, add=True)
+
+ if cid > 0:
+ # BP entity_id -> (PM class_id, PM entity_id)
+ instances = self._me_watch_list[class_id]['instances']
+ instances[entity_id] = (cid, eid)
+
+ def on_delete_response(self, _topic, msg):
+ """
+ Called upon receipt of a Delete Response for this ONU
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-delete-response', state=self.state)
+
+ def valid_request(stat, cid, eid):
+ return self._omci_cc_subscriptions[RxEvent.Delete] is not None\
+ and stat in (RC.Success, RC.UnknownInstance) \
+ and cid in self._me_watch_list.keys() \
+ and eid in self._me_watch_list[cid]['instances']
+
+ response = msg[RX_RESPONSE_KEY]
+ omci = response.fields['omci_message'].fields
+ class_id = omci['entity_class']
+ entity_id = omci['entity_id']
+ status = omci['success_code']
+
+ if valid_request(status, class_id, entity_id):
+ request = msg[TX_REQUEST_KEY]
+ method = self._me_watch_list[class_id]['create-delete']
+
+ method(request, class_id, entity_id, add=False)
+ # BP entity_id -> (PM class_id, PM entity_id)
+ instances = self._me_watch_list[class_id]['instances']
+ del instances[entity_id]
+
+ def get_pm_entity_id_for_add(self, pm_cid, eid):
+ """
+ Select the Entity ID to use for a specific PM Class ID. For extended
+ PM ME's, an entity id (>0) is allocated
+
+ :param pm_cid: (int) PM ME Class ID to create/get entry ID for
+ :param eid: (int) Reference class's entity ID. Used as PM entity ID for non-
+ extended PM history PMs
+ :return: (int) Entity ID to use
+ """
+ if pm_cid in (EthernetFrameExtendedPerformanceMonitoring.class_id,
+ EthernetFrameExtendedPerformanceMonitoring64Bit.class_id):
+ return self._enet_entity_id.get_next()
+ return eid
+
+ def release_pm_entity_id(self, pm_cid, eid):
+ if pm_cid in (EthernetFrameExtendedPerformanceMonitoring.class_id,
+ EthernetFrameExtendedPerformanceMonitoring64Bit.class_id):
+ try:
+ self._enet_entity_id.release(eid)
+ except:
+ pass
+
+ def add_remove_enet_frame_pm(self, request, class_id, entity_id,
+ add=True,
+ attributes=None):
+ """
+ Add/remove PM for the dynamic MAC Port configuration data.
+
+ This can be called in a variety of ways:
+
+ o If from an Response event from OMCI_CC, the request will contain
+ the original create/delete request. The class_id and entity_id will
+ be the MAC Data Configuration Data class and instance ID.
+ add = True if create, False if delete
+
+ o If starting up (and the associated ME is already created), the MAC
+ Data Configuration Data class and instance ID, and attributes are
+ provided. request = None and add = True
+
+ o If cleaning up (stopping), the PM ME class_id, entity_id are provided.
+ request = None and add = False
+
+ :return: (int, int) PM ME class_id and entity_id for add/remove was performed.
+ class and entity IDs are non-zero on success
+ """
+ pm_entity_id = 0
+ cid = 0
+ eid = 0
+ upstream = False
+
+ def tp_type_to_pm(tp):
+ # TODO: Support 64-bit extended Monitoring MEs.
+ # This will result in the need to maintain entity IDs of PMs differently
+ upstream_types = [ # EthernetFrameExtendedPerformanceMonitoring64Bit.class_id,
+ EthernetFrameExtendedPerformanceMonitoring.class_id,
+ EthernetFrameUpstreamPerformanceMonitoringHistoryData.class_id], True
+ downstream_types = [ # EthernetFrameExtendedPerformanceMonitoring64Bit.class_id,
+ EthernetFrameExtendedPerformanceMonitoring.class_id,
+ EthernetFrameDownstreamPerformanceMonitoringHistoryData.class_id], False
+ return {
+ 1: downstream_types,
+ 3: upstream_types,
+ 5: downstream_types,
+ 6: downstream_types,
+ }.get(tp, None)
+
+ if request is not None:
+ assert class_id == MacBridgePortConfigurationData.class_id
+
+ # Is this associated with the ANI or the UNI side of the bridge?
+ # For VOLTHA v2.0, only high-speed internet data service is
+ attributes = request.fields['omci_message'].fields['data']
+ pm_class_ids, upstream = tp_type_to_pm(attributes['tp_type'])
+ cid = request.fields['omci_message'].fields['entity_class']
+ eid = request.fields['omci_message'].fields['entity_id']
+ if not add:
+ instances = self._me_watch_list[cid]['instances']
+ _, pm_entity_id = instances.get(eid, (None, None))
+
+ elif add:
+ assert class_id == MacBridgePortConfigurationData.class_id
+ assert isinstance(attributes, dict)
+
+ # Is this associated with the ANI or the UNI side of the bridge?
+ pm_class_ids, upstream = tp_type_to_pm(attributes.get('tp_type'))
+ cid = class_id
+ eid = entity_id
+
+ else:
+ assert class_id in (EthernetFrameUpstreamPerformanceMonitoringHistoryData.class_id,
+ EthernetFrameDownstreamPerformanceMonitoringHistoryData.class_id,
+ EthernetFrameExtendedPerformanceMonitoring.class_id,
+ EthernetFrameExtendedPerformanceMonitoring64Bit.class_id)
+ pm_class_ids = [class_id]
+
+ if pm_class_ids is None:
+ return False # Unable to select a supported ME for this ONU
+
+ if add:
+ for pm_class_id in pm_class_ids:
+ if self._me_is_supported(pm_class_id):
+ pm_entity_id = self.get_pm_entity_id_for_add(pm_class_id, eid)
+ self.add_pm_me(pm_class_id, pm_entity_id, cid=cid, eid=eid,
+ upstream=upstream)
+ return pm_class_id, pm_entity_id
+ else:
+ for pm_class_id in pm_class_ids:
+ if self._me_is_supported(pm_class_id):
+ self.delete_pm_me(pm_class_id, pm_entity_id)
+ self.release_pm_entity_id(pm_class_id, pm_entity_id)
+ return pm_class_id, pm_entity_id
+
+ return 0, 0
diff --git a/python/adapters/extensions/omci/tasks/__init__.py b/python/adapters/extensions/omci/tasks/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/extensions/omci/tasks/alarm_resync_task.py b/python/adapters/extensions/omci/tasks/alarm_resync_task.py
new file mode 100644
index 0000000..a16f3a2
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/alarm_resync_task.py
@@ -0,0 +1,393 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from twisted.internet.defer import inlineCallbacks, TimeoutError, failure, returnValue
+from twisted.internet import reactor
+from common.utils.asleep import asleep
+from voltha.extensions.omci.database.mib_db_dict import *
+from voltha.extensions.omci.omci_defs import AttributeAccess
+from voltha.extensions.omci.database.alarm_db_ext import AlarmDbExternal
+
+AA = AttributeAccess
+
+
+class AlarmCopyException(Exception):
+ pass
+
+
+class AlarmDownloadException(Exception):
+ pass
+
+
+class AlarmResyncException(Exception):
+ pass
+
+
+class AlarmResyncTask(Task):
+ """
+ OpenOMCI ALARM resynchronization Task
+
+ This task should get a copy of the ALARM and compare compare it to a
+ copy of the database. When the ALARM Upload command is sent to the ONU,
+ it should make a copy and source the data requested from this database.
+ The ONU can still source AVC's and the the OLT can still send config
+ commands to the actual.
+ """
+ task_priority = Task.DEFAULT_PRIORITY
+ name = "ALARM Resynchronization Task"
+
+ max_retries = 3
+ retry_delay = 7
+
+ max_alarm_upload_next_retries = 3
+ alarm_upload_next_delay = 10 # Max * delay < 60 seconds
+
+ def __init__(self, omci_agent, device_id):
+ """
+ Class initialization
+
+ :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+ :param device_id: (str) ONU Device ID
+ """
+ super(AlarmResyncTask, self).__init__(AlarmResyncTask.name,
+ omci_agent,
+ device_id,
+ priority=AlarmResyncTask.task_priority,
+ exclusive=False)
+ self._local_deferred = None
+ self._device = omci_agent.get_device(device_id)
+ self._db_active = MibDbVolatileDict(omci_agent)
+ self._db_active.start()
+
+ def cancel_deferred(self):
+ super(AlarmResyncTask, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def start(self):
+ """
+ Start ALARM Re-Synchronization task
+ """
+ super(AlarmResyncTask, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_alarm_resync)
+ self._db_active.start()
+ self._db_active.add(self.device_id)
+
+ def stop(self):
+ """
+ Shutdown ALARM Re-Synchronization task
+ """
+ self.log.debug('stopping')
+
+ self.cancel_deferred()
+ self._device = None
+ self._db_active.stop()
+ self._db_active = None
+ super(AlarmResyncTask, self).stop()
+
+ @inlineCallbacks
+ def perform_alarm_resync(self):
+ """
+ Perform the ALARM Resynchronization sequence
+
+ The sequence to be performed is:
+ - get a copy of the current ALARM database
+
+ - perform ALARM upload commands to get ONU's database and save this
+ to a local DB.
+ During the alarm upload process, the maximum time between alarm upload next
+ requests is 1 minute.
+ """
+ self.log.debug('perform-alarm-resync')
+
+ try:
+ self.strobe_watchdog()
+ results = yield self.snapshot_alarm()
+ olt_db_copy = results[0]
+ number_of_commands = results[1]
+
+ if olt_db_copy is None:
+ e = AlarmCopyException('Failed to get local database copy')
+ self.deferred.errback(failure.Failure(e))
+ else:
+ # Start the ALARM upload sequence, save alarms to the table
+ self.strobe_watchdog()
+
+ if number_of_commands > 0:
+ commands_retrieved = yield self.upload_alarm(number_of_commands)
+ else:
+ commands_retrieved = 0
+
+ if commands_retrieved != number_of_commands:
+ e = AlarmDownloadException('Only retrieved {} of {} instances'.
+ format(commands_retrieved, number_of_commands))
+ self.deferred.errback(failure.Failure(e))
+ else:
+ # Compare the databases
+ onu_db_copy = self._db_active.query(self.device_id)
+
+ on_olt_only, on_onu_only, attr_diffs = \
+ self.compare_mibs(olt_db_copy, onu_db_copy)
+
+ on_olt_only = on_olt_only if len(on_olt_only) else None
+ on_onu_only = on_onu_only if len(on_onu_only) else None
+ attr_diffs = attr_diffs if len(attr_diffs) else None
+
+ on_olt_only_diffs = on_olt_only if on_olt_only and len(on_olt_only) else None
+ on_onu_only_diffs = on_onu_only if on_onu_only and len(on_onu_only) else None
+ attr_diffs = attr_diffs if attr_diffs and len(attr_diffs) else None
+
+ if all(diff is None for diff in [on_olt_only_diffs, on_onu_only_diffs, attr_diffs]):
+ results = None
+ else:
+ results = {
+ 'onu-only': on_onu_only_diffs,
+ 'olt-only': on_olt_only_diffs,
+ 'attr-diffs': attr_diffs,
+ 'onu-db': onu_db_copy,
+ 'olt-db': olt_db_copy
+ }
+ self.deferred.callback(results)
+
+ except Exception as e:
+ self.log.exception('resync', e=e)
+ self.deferred.errback(failure.Failure(e))
+
+ @inlineCallbacks
+ def snapshot_alarm(self):
+ """
+ Snapshot the ALARM on the ONU and create a copy of our local ALARM database
+
+ :return: (pair) (command_sequence_number)
+ """
+ olt_db_copy = None
+ command_sequence_number = None
+
+ try:
+ max_tries = AlarmResyncTask.max_retries - 1
+
+ for retries in xrange(0, max_tries + 1):
+ # Send ALARM Upload so ONU snapshots its ALARM
+ try:
+ command_sequence_number = yield self.send_alarm_upload()
+ self.strobe_watchdog()
+
+ if command_sequence_number is None:
+ if retries >= max_tries:
+ olt_db_copy = None
+ break
+
+ except TimeoutError as e:
+ self.log.warn('timeout', e=e)
+ if retries >= max_tries:
+ raise
+
+ self.strobe_watchdog()
+ yield asleep(AlarmResyncTask.retry_delay)
+ continue
+
+ # Get a snapshot of the local MIB database
+ olt_db_copy = self._device.query_alarm_table()
+ # if we made it this far, no need to keep trying
+ break
+
+ except Exception as e:
+ self.log.exception('alarm-resync', e=e)
+ raise
+
+ # Handle initial failures
+
+ if olt_db_copy is None or command_sequence_number is None:
+ raise AlarmCopyException('Failed to snapshot ALARM copy after {} retries'.
+ format(AlarmResyncTask.max_retries))
+
+ returnValue((olt_db_copy, command_sequence_number))
+
+ @inlineCallbacks
+ def send_alarm_upload(self):
+ """
+ Perform ALARM upload command and get the number of entries to retrieve
+
+ :return: (int) Number of commands to execute or None on error
+ """
+ ########################################
+ # Begin ALARM Upload
+ try:
+ results = yield self._device.omci_cc.send_get_all_alarm()
+ self.strobe_watchdog()
+ command_sequence_number = results.fields['omci_message'].fields['number_of_commands']
+
+ if command_sequence_number < 0:
+ raise ValueError('Number of commands was {}'.format(command_sequence_number))
+
+ returnValue(command_sequence_number)
+
+ except TimeoutError as e:
+ self.log.warn('alarm-resync-get-timeout', e=e)
+ raise
+
+ @inlineCallbacks
+ def upload_alarm(self, command_sequence_number):
+ ########################################
+ # Begin ALARM Upload
+ seq_no = None
+
+ for seq_no in xrange(command_sequence_number):
+ max_tries = AlarmResyncTask.max_alarm_upload_next_retries
+
+ for retries in xrange(0, max_tries):
+ try:
+ response = yield self._device.omci_cc.send_get_all_alarm_next(seq_no)
+ self.strobe_watchdog()
+
+ omci_msg = response.fields['omci_message'].fields
+ alarm_class_id = omci_msg['alarmed_entity_class']
+ alarm_entity_id = omci_msg['alarmed_entity_id']
+
+ alarm_bit_map = omci_msg['alarm_bit_map']
+ attributes = {AlarmDbExternal.ALARM_BITMAP_KEY: alarm_bit_map}
+
+ # Save to the database
+ self._db_active.set(self.device_id, alarm_class_id,
+ alarm_entity_id, attributes)
+ break
+
+ except TimeoutError:
+ self.log.warn('alarm-resync-timeout', seq_no=seq_no,
+ command_sequence_number=command_sequence_number)
+
+ if retries < max_tries - 1:
+ yield asleep(AlarmResyncTask.alarm_upload_next_delay)
+ self.strobe_watchdog()
+ else:
+ raise
+
+ except Exception as e:
+ self.log.exception('resync', e=e, seq_no=seq_no,
+ command_sequence_number=command_sequence_number)
+
+ returnValue(seq_no + 1) # seq_no is zero based and alarm table.
+
+ def compare_mibs(self, db_copy, db_active):
+ """
+ Compare the our db_copy with the ONU's active copy
+
+ :param db_copy: (dict) OpenOMCI's copy of the database
+ :param db_active: (dict) ONU's database snapshot
+ :return: (dict), (dict), dict() Differences
+ """
+ self.strobe_watchdog()
+
+ # Class & Entities only in local copy (OpenOMCI)
+ on_olt_only = self.get_lsh_only_dict(db_copy, db_active)
+
+ # Class & Entities only on remote (ONU)
+ on_onu_only = self.get_lsh_only_dict(db_active, db_copy)
+
+ # Class & Entities on both local & remote, but one or more attributes
+ # are different on the ONU. This is the value that the local (OpenOMCI)
+ # thinks should be on the remote (ONU)
+
+ me_map = self.omci_agent.get_device(self.device_id).me_map
+ attr_diffs = self.get_attribute_diffs(db_copy, db_active, me_map)
+
+ return on_olt_only, on_onu_only, attr_diffs
+
+ def get_lsh_only_dict(self, lhs, rhs):
+ """
+ Compare two MIB database dictionaries and return the ME Class ID and
+ instances that are unique to the lhs dictionary. Both parameters
+ should be in the common MIB Database output dictionary format that
+ is returned by the mib 'query' command.
+
+ :param lhs: (dict) Left-hand-side argument.
+ :param rhs: (dict) Right-hand-side argument
+
+ return: (list(int,int)) List of tuples where (class_id, inst_id)
+ """
+ results = list()
+
+ for cls_id, cls_data in lhs.items():
+ # Get unique classes
+ #
+ # Skip keys that are not class IDs
+ if not isinstance(cls_id, int):
+ continue
+
+ if cls_id not in rhs:
+ results.extend([(cls_id, inst_id) for inst_id in cls_data.keys()
+ if isinstance(inst_id, int)])
+ else:
+ # Get unique instances of a class
+ lhs_cls = cls_data
+ rhs_cls = rhs[cls_id]
+
+ for inst_id, _ in lhs_cls.items():
+ # Skip keys that are not instance IDs
+ if isinstance(cls_id, int) and inst_id not in rhs_cls:
+ results.extend([(cls_id, inst_id)])
+
+ return results
+
+ def get_attribute_diffs(self, omci_copy, onu_copy, me_map):
+ """
+ Compare two OMCI MIBs and return the ME class and instance IDs that exists
+ on both the local copy and the remote ONU that have different attribute
+ values. Both parameters should be in the common MIB Database output
+ dictionary format that is returned by the mib 'query' command.
+
+ :param omci_copy: (dict) OpenOMCI copy (OLT-side) of the MIB Database
+ :param onu_copy: (dict) active ONU latest copy its database
+ :param me_map: (dict) ME Class ID MAP for this ONU
+
+ return: (list(int,int,str)) List of tuples where (class_id, inst_id, attribute)
+ points to the specific ME instance where attributes
+ are different
+ """
+ results = list()
+
+ # Get class ID's that are in both
+ class_ids = {cls_id for cls_id, _ in omci_copy.items()
+ if isinstance(cls_id, int) and cls_id in onu_copy}
+
+ for cls_id in class_ids:
+ # Get unique instances of a class
+ olt_cls = omci_copy[cls_id]
+ onu_cls = onu_copy[cls_id]
+
+ # Get set of common instance IDs
+ inst_ids = {inst_id for inst_id, _ in olt_cls.items()
+ if isinstance(inst_id, int) and inst_id in onu_cls}
+
+ for inst_id in inst_ids:
+ omci_attributes = {k for k in olt_cls[inst_id][ATTRIBUTES_KEY].iterkeys()}
+ onu_attributes = {k for k in onu_cls[inst_id][ATTRIBUTES_KEY].iterkeys()}
+
+ # Get attributes that exist in one database, but not the other
+ sym_diffs = (omci_attributes ^ onu_attributes)
+ results.extend([(cls_id, inst_id, attr) for attr in sym_diffs])
+
+ # Get common attributes with different values
+ common_attributes = (omci_attributes & onu_attributes)
+ results.extend([(cls_id, inst_id, attr) for attr in common_attributes
+ if olt_cls[inst_id][ATTRIBUTES_KEY][attr] !=
+ onu_cls[inst_id][ATTRIBUTES_KEY][attr]])
+ return results
diff --git a/python/adapters/extensions/omci/tasks/file_download_task.py b/python/adapters/extensions/omci/tasks/file_download_task.py
new file mode 100755
index 0000000..63da427
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/file_download_task.py
@@ -0,0 +1,108 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from twisted.internet.defer import inlineCallbacks, TimeoutError, failure, AlreadyCalledError
+from twisted.internet import reactor
+from voltha.extensions.omci.omci_defs import ReasonCodes
+import requests
+import os
+import time
+
+class FileDownloadTask(Task):
+ name = "Image File Download Task"
+ CHUNK_SIZE = 1024
+
+ def __init__(self, omci_agent, img_dnld, clock= None): #device_id, url, local_path)
+ super(FileDownloadTask, self).__init__(FileDownloadTask.name, omci_agent, img_dnld.id,
+ exclusive=False,
+ watchdog_timeout=45)
+ # self.url = url
+ # self.local_path = local_path
+ self._image_download = img_dnld
+ self.reactor = clock if clock is not None else reactor
+ self._local_deferred = None
+ # self._request = None
+ # self._file = None
+ # self.log.debug('{} running'.format(FileDownloadTask.name))
+
+ # def __save_data(self):
+ # chunk = self._request.iter_content(chunk_size=FileDownloadTask.CHUNK_SIZE)
+ # if len(chunk) == 0:
+ # self._file.close()
+ # self.deferred.callback(self._image_download)
+ # else:
+ # self._file.write(chunk)
+ # self._image_download.downloaded_bytes += len(chunk)
+ # self.reactor.callLater(0, self.__save_data)
+
+ @inlineCallbacks
+ def perform_download_data(self):
+ try:
+ r = requests.get(self._image_download.url, stream=True)
+ with open(self._image_download.local_dir + '/' + self._image_download.name, 'wb') as f:
+ for chunk in r.iter_content(chunk_size=FileDownloadTask.CHUNK_SIZE):
+ self.strobe_watchdog()
+ if chunk: # filter out keep-alive new chunks
+ yield f.write(chunk)
+ self._image_download.file_size += len(chunk)
+ # yield time.sleep(1)
+ self.deferred.callback(self._image_download)
+ except Exception as e:
+ self.deferred.errback(failure.Failure(e))
+
+ def start(self):
+ super(FileDownloadTask, self).start()
+ if not os.path.exists(self._image_download.local_dir):
+ os.makedirs(self._image_download.local_dir)
+
+ self.strobe_watchdog()
+ self._image_download.file_size = 0
+ self._local_deferred = self.reactor.callLater(0, self.perform_download_data)
+ # try:
+ # if not os.path.exists(self._image_download.local_dir):
+ # os.makedirs(self._image_download.local_dir)
+
+ # self.strobe_watchdog()
+ # self._image_download.downloaded_bytes = 0
+ # self.reactor.callLater(0, self.perform_download_data)
+
+ # self._request = requests.get(self._image_download.url, stream=True)
+ # with open(self._image_download.local_dir + '/' + self._image_download.name, 'wb') as f:
+ # for chunk in r.iter_content(chunk_size=FileDownloadTask.CHUNK_SIZE):
+ # self.strobe_watchdog()
+ # if chunk: # filter out keep-alive new chunks
+ # f.write(chunk)
+ # self._image_download.downloaded_bytes += len(chunk)
+
+ # self.deferred.callback(self._image_download)
+ # except Exception as e:
+ # self.deferred.errback(failure.Failure(e))
+
+ # def stop(self):
+ # # self.cancel_deferred()
+ # super(FileDownloadTask, self).stop()
+
+ def cancel_deferred(self):
+ self.log.debug('FileDownloadTask cancel_deferred')
+ super(FileDownloadTask, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
diff --git a/python/adapters/extensions/omci/tasks/get_mds_task.py b/python/adapters/extensions/omci/tasks/get_mds_task.py
new file mode 100644
index 0000000..1560c83
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/get_mds_task.py
@@ -0,0 +1,112 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, TimeoutError, failure
+from voltha.extensions.omci.omci_me import OntDataFrame
+from voltha.extensions.omci.omci_defs import ReasonCodes as RC
+
+
+class GetMdsTask(Task):
+ """
+ OpenOMCI Get MIB Data Sync value task
+
+ On successful completion, this task will call the 'callback' method of the
+ deferred returned by the start method and return the value of the MIB
+ Data Sync attribute of the ONT Data ME
+ """
+ task_priority = Task.DEFAULT_PRIORITY
+ name = "Get MDS Task"
+
+ def __init__(self, omci_agent, device_id):
+ """
+ Class initialization
+
+ :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+ :param device_id: (str) ONU Device ID
+ """
+ super(GetMdsTask, self).__init__(GetMdsTask.name,
+ omci_agent,
+ device_id,
+ priority=GetMdsTask.task_priority)
+ self._local_deferred = None
+
+ def cancel_deferred(self):
+ super(GetMdsTask, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def start(self):
+ """
+ Start MIB Synchronization tasks
+ """
+ super(GetMdsTask, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_get_mds)
+
+ def stop(self):
+ """
+ Shutdown MIB Synchronization tasks
+ """
+ self.log.debug('stopping')
+
+ self.cancel_deferred()
+ super(GetMdsTask, self).stop()
+
+ @inlineCallbacks
+ def perform_get_mds(self):
+ """
+ Get the 'mib_data_sync' attribute of the ONU
+ """
+ self.log.debug('perform-get-mds')
+
+ try:
+ device = self.omci_agent.get_device(self.device_id)
+
+ #########################################
+ # Request (MDS supplied value does not matter for a 'get' request)
+
+ self.strobe_watchdog()
+ results = yield device.omci_cc.send(OntDataFrame().get())
+
+ omci_msg = results.fields['omci_message'].fields
+ status = omci_msg['success_code']
+
+ # Note: Currently the data reported by the Scapy decode is 16-bits since we need
+ # the data field that large in order to support MIB and Alarm Upload Next
+ # commands. Select only the first 8-bits since that is the size of the MIB
+ # Data Sync attribute
+ mds = (omci_msg['data']['mib_data_sync'] >> 8) & 0xFF \
+ if 'data' in omci_msg and 'mib_data_sync' in omci_msg['data'] else -1
+
+ self.log.debug('ont-data-mds', status=status, mib_data_sync=mds)
+
+ assert status == RC.Success, 'Unexpected Response Status: {}'.format(status)
+
+ # Successful if here
+ self.deferred.callback(mds)
+
+ except TimeoutError as e:
+ self.log.warn('get-mds-timeout', e=e)
+ self.deferred.errback(failure.Failure(e))
+
+ except Exception as e:
+ self.log.exception('get-mds', e=e)
+ self.deferred.errback(failure.Failure(e))
diff --git a/python/adapters/extensions/omci/tasks/interval_data_task.py b/python/adapters/extensions/omci/tasks/interval_data_task.py
new file mode 100644
index 0000000..d41c1d0
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/interval_data_task.py
@@ -0,0 +1,198 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from datetime import datetime
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, TimeoutError, failure
+from voltha.extensions.omci.omci_defs import ReasonCodes
+from voltha.extensions.omci.omci_frame import OmciFrame, OmciGet
+
+
+class IntervalDataTaskFailure(Exception):
+ pass
+
+
+class IntervalDataTask(Task):
+ """
+ OpenOMCI Performance Interval Get Request
+ """
+ task_priority = Task.DEFAULT_PRIORITY
+ name = "Interval Data Task"
+ max_payload = 29
+
+ def __init__(self, omci_agent, device_id, class_id, entity_id,
+ max_get_response_payload=max_payload,
+ parent_class_id=None,
+ parent_entity_id=None,
+ upstream=None):
+ """
+ Class initialization
+
+ :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+ :param device_id: (str) ONU Device ID
+ :param class_id: (int) ME Class ID
+ :param entity_id: (int) ME entity ID
+ :param max_get_response_payload: (int) Maximum number of octets in a
+ single GET response frame
+ """
+ super(IntervalDataTask, self).__init__(IntervalDataTask.name,
+ omci_agent,
+ device_id,
+ priority=IntervalDataTask.task_priority,
+ exclusive=False)
+ self._local_deferred = None
+ self._class_id = class_id
+ self._entity_id = entity_id
+
+ self._parent_class_id = parent_class_id
+ self._parent_entity_id = parent_entity_id
+ self._upstream = upstream
+
+ me_map = self.omci_agent.get_device(self.device_id).me_map
+ if self._class_id not in me_map:
+ msg = "The requested ME Class () does not exist in the ONU's ME Map".format(self._class_id)
+ self.log.warn('unknown-pm-me', msg=msg)
+ raise IntervalDataTaskFailure(msg)
+
+ self._entity = me_map[self._class_id]
+ self._counter_attributes = self.get_counter_attributes_names_and_size()
+ self._max_payload = max_get_response_payload
+
+ def cancel_deferred(self):
+ super(IntervalDataTask, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def start(self):
+ """
+ Start the tasks
+ """
+ super(IntervalDataTask, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_get_interval)
+
+ def stop(self):
+ """
+ Shutdown the tasks
+ """
+ self.log.debug('stopping')
+
+ self.cancel_deferred()
+ super(IntervalDataTask, self).stop()
+
+ def get_counter_attributes_names_and_size(self):
+ """
+ Get all of the counter attributes names and the amount of storage they take
+
+ :return: (dict) Attribute name -> length
+ """
+ return {name: self._entity.attributes[attr_index].field.sz
+ for name, attr_index in self._entity.attribute_name_to_index_map.items()
+ if self._entity.attributes[attr_index].is_counter}
+
+ @inlineCallbacks
+ def perform_get_interval(self):
+ """
+ Sync the time
+ """
+ self.log.info('perform-get-interval', class_id=self._class_id,
+ entity_id=self._entity_id)
+
+ device = self.omci_agent.get_device(self.device_id)
+ attr_names = self._counter_attributes.keys()
+
+ final_results = {
+ 'class_id': self._class_id,
+ 'entity_id': self._entity_id,
+ 'me_name': self._entity.__name__, # Mostly for debugging...
+ 'interval_utc_time': None,
+ 'parent_class_id': self._parent_class_id,
+ 'parent_entity_id': self._parent_entity_id,
+ 'upstream': self._upstream
+ # Counters added here as they are retrieved
+ }
+ last_end_time = None
+
+ while len(attr_names) > 0:
+ # Get as many attributes that will fit. Always include the 1 octet
+ # Interval End Time Attribute and 2 octets for the Entity ID
+
+ remaining_payload = self._max_payload - 3
+ attributes = list()
+ for name in attr_names:
+ if self._counter_attributes[name] > remaining_payload:
+ break
+
+ attributes.append(name)
+ remaining_payload -= self._counter_attributes[name]
+
+ attr_names = attr_names[len(attributes):]
+ attributes.append('interval_end_time')
+
+ frame = OmciFrame(
+ transaction_id=None,
+ message_type=OmciGet.message_id,
+ omci_message=OmciGet(
+ entity_class=self._class_id,
+ entity_id=self._entity_id,
+ attributes_mask=self._entity.mask_for(*attributes)
+ )
+ )
+ self.log.debug('interval-get-request', class_id=self._class_id,
+ entity_id=self._entity_id)
+ try:
+ self.strobe_watchdog()
+ results = yield device.omci_cc.send(frame)
+
+ omci_msg = results.fields['omci_message'].fields
+ status = omci_msg['success_code']
+ end_time = omci_msg['data'].get('interval_end_time')
+
+ self.log.debug('interval-get-results', class_id=self._class_id,
+ entity_id=self._entity_id, status=status,
+ end_time=end_time)
+
+ if status != ReasonCodes.Success:
+ raise IntervalDataTaskFailure('Unexpected Response Status: {}, Class ID: {}'.
+ format(status, self._class_id))
+ if last_end_time is None:
+ last_end_time = end_time
+
+ elif end_time != last_end_time:
+ msg = 'Interval End Time Changed during retrieval from {} to {}'\
+ .format(last_end_time, end_time)
+ self.log.info('interval-roll-over', msg=msg, class_id=self._class_id)
+ raise IntervalDataTaskFailure(msg)
+
+ final_results['interval_utc_time'] = datetime.utcnow()
+ for attribute in attributes:
+ final_results[attribute] = omci_msg['data'].get(attribute)
+
+ except TimeoutError as e:
+ self.log.warn('interval-get-timeout', e=e, class_id=self._class_id,
+ entity_id=self._entity_id, attributes=attributes)
+ self.deferred.errback(failure.Failure(e))
+
+ except Exception as e:
+ self.log.exception('interval-get-failure', e=e, class_id=self._class_id)
+ self.deferred.errback(failure.Failure(e))
+
+ # Successful if here
+ self.deferred.callback(final_results)
diff --git a/python/adapters/extensions/omci/tasks/mib_reconcile_task.py b/python/adapters/extensions/omci/tasks/mib_reconcile_task.py
new file mode 100644
index 0000000..38e29dc
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/mib_reconcile_task.py
@@ -0,0 +1,693 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from common.utils.asleep import asleep
+from voltha.extensions.omci.tasks.task import Task
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, failure, returnValue, TimeoutError
+from voltha.extensions.omci.omci_defs import *
+from voltha.extensions.omci.omci_me import OntDataFrame
+from voltha.extensions.omci.omci_frame import OmciFrame, OmciDelete, OmciCreate, OmciSet
+from voltha.extensions.omci.database.mib_db_api import ATTRIBUTES_KEY
+
+OP = EntityOperations
+RC = ReasonCodes
+AA = AttributeAccess
+
+
+class MibReconcileException(Exception):
+ pass
+
+
+class MibPartialSuccessException(Exception):
+ pass
+
+
+class MibReconcileTask(Task):
+ """
+ OpenOMCI MIB Reconcile Task
+
+ This task attempts to resynchronize the MIB. Note that it runs in exclusive
+ OMCI-CC mode so that it can query the current database/ONU to verify the
+ differences still exist before correcting them.
+ """
+ task_priority = 240
+ name = "MIB Reconcile Task"
+ max_sequential_db_updates = 5 # Be kind, rewind
+ db_update_pause = 0.05 # 50mS
+
+ def __init__(self, omci_agent, device_id, diffs):
+ """
+ Class initialization
+
+ :param omci_agent: (OpenOMCIAgent) OMCI Adapter agent
+ :param device_id: (str) ONU Device ID
+ :param diffs: (dict) Dictionary of what was found to be invalid
+ """
+ super(MibReconcileTask, self).__init__(MibReconcileTask.name,
+ omci_agent,
+ device_id,
+ priority=MibReconcileTask.task_priority,
+ exclusive=False)
+ self._local_deferred = None
+ self._diffs = diffs
+ self._device = None
+ self._sync_sm = None
+ self._db_updates = 0 # For tracking sequential blocking consul/etcd updates
+
+ def cancel_deferred(self):
+ super(MibReconcileTask, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def start(self):
+ """
+ Start MIB Reconcile task
+ """
+ super(MibReconcileTask, self).start()
+
+ self._device = self.omci_agent.get_device(self.device_id)
+
+ if self._device is None:
+ e = MibReconcileException('Device {} no longer exists'.format(self.device_id))
+ self.deferred.errback(failure.Failure(e))
+ return
+
+ self._sync_sm = self._device.mib_synchronizer
+
+ if self._device is None:
+ e = MibReconcileException('Device {} MIB State machine no longer exists'.format(self.device_id))
+ self.deferred.errback(failure.Failure(e))
+ return
+
+ self._local_deferred = reactor.callLater(0, self.perform_mib_reconcile)
+
+ def stop(self):
+ """
+ Shutdown MIB Reconcile task
+ """
+ self.log.debug('stopping')
+
+ self.cancel_deferred()
+ self._device = None
+ super(MibReconcileTask, self).stop()
+
+ @inlineCallbacks
+ def perform_mib_reconcile(self):
+ """
+ Perform the MIB Reconciliation sequence.
+
+ The sequence to reconcile will be to clean up ONU only MEs, followed by
+ OLT/OpenOMCI-only MEs, and then finally correct common MEs with differing
+ attributes.
+ """
+ self.log.debug('perform-mib-reconcile')
+
+ try:
+ successes = 0
+ failures = 0
+
+ if self._diffs['onu-only'] is not None and len(self._diffs['onu-only']):
+ results = yield self.fix_onu_only(self._diffs['onu-only'],
+ self._diffs['onu-db'])
+ self.log.debug('onu-only-results', good=results[0], bad=results[1])
+ successes += results[0]
+ failures += results[1]
+
+ if self._diffs['olt-only'] is not None and len(self._diffs['olt-only']):
+ results = yield self.fix_olt_only(self._diffs['olt-only'],
+ self._diffs['onu-db'],
+ self._diffs['olt-db'])
+ self.log.debug('olt-only-results', good=results[0], bad=results[1])
+ successes += results[0]
+ failures += results[1]
+
+ if self._diffs['attributes'] is not None and len(self._diffs['attributes']):
+ results = yield self.fix_attributes_only(self._diffs['attributes'],
+ self._diffs['onu-db'],
+ self._diffs['olt-db'])
+ self.log.debug('attributes-results', good=results[0], bad=results[1])
+ successes += results[0]
+ failures += results[1]
+
+ # Success? Update MIB-data-sync
+ if failures == 0:
+ results = yield self.update_mib_data_sync()
+ successes += results[0]
+ failures += results[1]
+
+ # Send back final status
+ if failures > 0:
+ msg = '{} Successful updates, {} failures'.format(successes, failure)
+ error = MibPartialSuccessException(msg) if successes \
+ else MibReconcileException(msg)
+ self.deferred.errback(failure.Failure(error))
+ else:
+ self.deferred.callback('{} Successful updates'.format(successes))
+
+ except Exception as e:
+ if not self.deferred.called:
+ self.log.exception('reconcile', e=e)
+ self.deferred.errback(failure.Failure(e))
+
+ @inlineCallbacks
+ def fix_onu_only(self, onu, onu_db):
+ """
+ Fix ME's that were only found on the ONU. For ONU only MEs there are
+ the following things that will be checked.
+
+ o ME's that do not have an OpenOMCI class decoder. These are stored
+ as binary blobs in the MIB database. Since we do not ever set them
+ (since no encoder as well), just store them in the OLT/OpenOMCI MIB
+ Database.
+
+ o For ME's that are created by the ONU (no create/delete access), the
+ MEs 'may' be due to a firmware upgrade and reboot or in response to
+ an OLT creating another ME entity and then creating this ME. Place
+ these 'new' into the database.
+
+ o For ME's that are created by the OLT/OpenOMCI, delete them from the
+ ONU
+
+ :param onu: (list(int,int)) List of tuples where (class_id, inst_id)
+ :param onu_db: (dict) ONU Database snapshot at time of audit
+
+ :return: (int, int) successes, failures
+ """
+ successes = 0
+ failures = 0
+ me_map = self._device.me_map
+
+ ####################################################################
+ # First the undecodables and onu-created (treated the same)
+ undecodable = self._undecodable(onu, me_map)
+ onu_created = self._onu_created(onu, me_map)
+
+ if len(undecodable) or len(onu_created):
+ results = yield self.fix_onu_only_save_to_db(undecodable, onu_created, onu_db)
+ successes += results[0]
+ failures += results[1]
+
+ ####################################################################
+ # Last the OLT created values, resend these to the ONU
+
+ olt_created = self._olt_created(onu, me_map)
+ if len(olt_created):
+ results = yield self.fix_onu_only_remove_from_onu(olt_created)
+ successes += results[0]
+ failures += results[1]
+
+ returnValue((successes, failures))
+
+ @inlineCallbacks
+ def fix_onu_only_save_to_db(self, undecodable, onu_created, onu_db):
+ """
+ In ONU database and needs to be saved to OLT/OpenOMCI database.
+
+ Note that some, perhaps all, of these instances could be ONU create
+ in response to the OLT creating some other ME instance. So treat
+ the Database operation as a create.
+ """
+ successes = 0
+ failures = 0
+
+ for cid, eid in undecodable + onu_created:
+ if self.deferred.called: # Check if task canceled
+ break
+ try:
+ # If in current MIB, had an audit issue or other MIB operation
+ # put it into the database, declare it a failure so we audit again
+ try:
+ olt_entry = self._sync_sm.query_mib(class_id=cid, instance_id=eid)
+
+ except KeyError: # Common for ONU created MEs during audit
+ olt_entry = None
+
+ if olt_entry is not None and len(olt_entry):
+ self.log.debug('onu-only-in-current', cid=cid, eid=eid)
+ failures += 1 # Mark as failure so we audit again
+
+ elif cid not in onu_db:
+ self.log.warn('onu-only-not-in-audit', cid=cid, eid=eid)
+ failures += 1
+
+ else:
+ entry = onu_db[cid][eid]
+ self.strobe_watchdog()
+ self._sync_sm.mib_set(cid, eid, entry[ATTRIBUTES_KEY])
+ successes += 1
+
+ # If we do nothing but DB updates for ALOT of MEs, we are
+ # blocking other async twisted tasks, be kind and pause
+ self._db_updates += 1
+
+ if self._db_updates >= MibReconcileTask.max_sequential_db_updates:
+ self._db_updates = 0
+ self._local_deferred = yield asleep(MibReconcileTask.db_update_pause)
+
+ except Exception as e:
+ self.log.warn('onu-only-error', e=e)
+ failures += 1
+
+ returnValue((successes, failures))
+
+ @inlineCallbacks
+ def fix_onu_only_remove_from_onu(self, olt_created,):
+ """ On ONU, but no longer on OLT/OpenOMCI, delete it """
+ successes = 0
+ failures = 0
+
+ for cid, eid in olt_created:
+ if self.deferred.called: # Check if task canceled
+ break
+ try:
+ # If in current MIB, had an audit issue, declare it an error
+ # and next audit should clear it up
+ try:
+ current_entry = self._sync_sm.query_mib(class_id=cid, instance_id=eid)
+
+ except KeyError:
+ # Expected if no other entities with same class present in MIB
+ current_entry = None
+
+ if current_entry is not None and len(current_entry):
+ self.log.debug('onu-only-in-current', cid=cid, eid=eid)
+ failures += 1
+
+ else:
+ # Delete it from the ONU. Assume success
+ frame = OmciFrame(transaction_id=None,
+ message_type=OmciDelete.message_id,
+ omci_message=OmciDelete(entity_class=cid, entity_id=eid))
+
+ self._local_deferred = yield self._device.omci_cc.send(frame)
+ self.check_status_and_state(self._local_deferred, 'onu-attribute-update')
+ successes += 1
+ self._db_updates = 0
+
+ except Exception as e:
+ self.log.warn('olt-only-error', e=e)
+ failures += 1
+ self.strobe_watchdog()
+
+ returnValue((successes, failures))
+
+ @inlineCallbacks
+ def fix_olt_only(self, olt, onu_db, olt_db):
+ """
+ Fix ME's that were only found on the OLT. For OLT only MEs there are
+ the following things that will be checked.
+
+ o ME's that do not have an OpenOMCI class decoder. These are stored
+ as binary blobs in the MIB database. Since the OLT will never
+ create these (all are learned from ONU), it is assumed the ONU
+ has removed them for some purpose. So delete them from the OLT
+ database.
+
+ o For ME's that are created by the ONU (no create/delete access), the
+ MEs 'may' not be on the ONU because of a reboot or an OLT created
+ ME was deleted and the ONU gratuitously removes it. So delete them
+ from the OLT database.
+
+ o For ME's that are created by the OLT/OpenOMCI, delete them from the
+ ONU
+
+ :param olt: (list(int,int)) List of tuples where (class_id, inst_id)
+ :param onu_db: (dict) ONU Database snapshot at time of audit
+ :param olt_db: (dict) OLT Database snapshot at time of audit
+
+ :return: (int, int) successes, failures
+ """
+ successes = 0
+ failures = 0
+ me_map = self._device.me_map
+
+ ####################################################################
+ # First the undecodables and onu-created (treated the same) remove
+ # from OpenOMCI database
+ undecodable = self._undecodable(olt, me_map)
+ onu_created = self._onu_created(olt, me_map)
+
+ if len(undecodable) or len(onu_created):
+ good, bad = self.fix_olt_only_remove_from_db(undecodable, onu_created)
+ successes += good
+ failures += bad
+
+ ####################################################################
+ # Last the OLT created
+
+ olt_created = self._olt_created(olt, me_map)
+ if len(olt_created):
+ results = yield self.fix_olt_only_create_on_onu(olt_created, me_map)
+ successes += results[0]
+ failures += results[1]
+
+ returnValue((successes, failures))
+
+ def fix_olt_only_remove_from_db(self, undecodable, onu_created):
+ """ On OLT, but not on ONU and are ONU created, delete from OLT/OpenOMCI DB """
+ successes = 0
+ failures = 0
+
+ for cid, eid in undecodable + onu_created:
+ if self.deferred.called: # Check if task canceled
+ break
+ try:
+ # Delete it. If already deleted (KeyError), then that is okay
+ self._sync_sm.mib_delete(cid, eid)
+ self.strobe_watchdog()
+
+ except KeyError:
+ successes += 1 # Not found in DB anymore, assume success
+
+ except Exception as e:
+ self.log.warn('olt-only-db-error', cid=cid, eid=eid, e=e)
+ failures += 1
+
+ return successes, failures
+
+ @inlineCallbacks
+ def fix_olt_only_create_on_onu(self, olt_created, me_map):
+ """ Found on OLT and created by OLT, so create on ONU"""
+ successes = 0
+ failures = 0
+
+ for cid, eid in olt_created:
+ if self.deferred.called: # Check if task canceled
+ break
+
+ try:
+ # Get current entry, use it if found
+ olt_entry = self._sync_sm.query_mib(class_id=cid, instance_id=eid)
+ me_entry = me_map[cid]
+
+ if olt_entry is None or len(olt_entry) == 0:
+ successes += 1 # Deleted before task got to run
+ else:
+ # Create it in the ONU. Only set-by-create attributes allowed
+ sbc_data = {k: v for k, v in olt_entry[ATTRIBUTES_KEY].items()
+ if AA.SetByCreate in
+ next((attr.access for attr in me_entry.attributes
+ if attr.field.name == k), set())}
+
+ frame = OmciFrame(transaction_id=None,
+ message_type=OmciCreate.message_id,
+ omci_message=OmciCreate(entity_class=cid,
+ entity_id=eid,
+ data=sbc_data))
+
+ self._local_deferred = yield self._device.omci_cc.send(frame)
+ self.check_status_and_state(self._local_deferred, 'olt-create-sbc')
+ successes += 1
+ self._db_updates = 0
+
+ # Try any writeable attributes now (but not set-by-create)
+ writeable_data = {k: v for k, v in olt_entry[ATTRIBUTES_KEY].items()
+ if AA.Writable in
+ next((attr.access for attr in me_entry.attributes
+ if attr.field.name == k), set())
+ and AA.SetByCreate not in
+ next((attr.access for attr in me_entry.attributes
+ if attr.field.name == k), set())}
+
+ if len(writeable_data):
+ attributes_mask = me_entry.mask_for(*writeable_data.keys())
+ frame = OmciFrame(transaction_id=None,
+ message_type=OmciSet.message_id,
+ omci_message=OmciSet(entity_class=cid,
+ entity_id=eid,
+ attributes_mask=attributes_mask,
+ data=writeable_data))
+
+ self._local_deferred = yield self._device.omci_cc.send(frame)
+ self.check_status_and_state(self._local_deferred, 'olt-set-writeable')
+ successes += 1
+
+ except Exception as e:
+ self.log.exception('olt-only-fix', e=e, cid=cid, eid=eid)
+ failures += 1
+ self.strobe_watchdog()
+
+ returnValue((successes, failures))
+
+ @inlineCallbacks
+ def fix_attributes_only(self, attrs, onu_db, olt_db):
+ """
+ Fix ME's that were found on both the ONU and OLT, but had differing
+ attribute values. There are several cases to handle here
+
+ o For ME's created on the ONU that have write attributes that
+ only exist in the ONU's database, copy these to the OLT/OpenOMCI
+ database
+
+ o For all other writeable attributes, the OLT value takes precedence
+
+ :param attrs: (list(int,int,str)) List of tuples where (class_id, inst_id, attribute)
+ points to the specific ME instance where attributes
+ are different
+ :param onu_db: (dict) ONU Database snapshot at time of audit
+ :param olt_db: (dict) OLT Database snapshot at time of audit
+
+ :return: (int, int) successes, failures
+ """
+ successes = 0
+ failures = 0
+ me_map = self._device.me_map
+
+ # Collect up attributes on a per CID/EID basis. This will result in
+ # the minimal number of operations to either the database of over
+ # the OMCI-CC to the ONU
+
+ attr_map = dict()
+ for cid, eid, attribute in attrs:
+ if (cid, eid) not in attr_map:
+ attr_map[(cid, eid)] = {attribute}
+ else:
+ attr_map[(cid, eid)].add(attribute)
+
+ for entity_pair, attributes in attr_map.items():
+ cid = entity_pair[0]
+ eid = entity_pair[1]
+
+ # Skip MEs we cannot encode/decode
+ if cid not in me_map:
+ self.log.warn('no-me-map-decoder', class_id=cid)
+ failures += 1
+ continue
+
+ if self.deferred.called: # Check if task canceled
+ break
+
+ # Build up MIB set commands and ONU Set (via OMCI) commands
+ # based of the attributes
+ me_entry = me_map[cid]
+ mib_data_to_save = dict()
+ onu_data_to_set = dict()
+ olt_attributes = olt_db[cid][eid][ATTRIBUTES_KEY]
+ onu_attributes = onu_db[cid][eid][ATTRIBUTES_KEY]
+
+ for attribute in attributes:
+ map_access = next((attr.access for attr in me_entry.attributes
+ if attr.field.name == attribute), set())
+ writeable = AA.Writable in map_access or AA.SetByCreate in map_access
+
+ # If only in ONU database snapshot, save it to OLT
+ if attribute in onu_attributes and attribute not in olt_attributes:
+ # On onu only
+ mib_data_to_save[attribute] = onu_attributes[attribute]
+
+ elif writeable:
+ # On olt only or in both. Either way OLT wins
+ onu_data_to_set[attribute] = olt_attributes[attribute]
+
+ # Now do the bulk operations For both, check to see if the target
+ # is still the same as when the audit was performed. If it is, do
+ # the commit. If not, mark as a failure so an expedited audit will
+ # occur and check again.
+
+ if len(mib_data_to_save):
+ results = yield self.fix_attributes_only_in_mib(cid, eid, mib_data_to_save)
+ successes += results[0]
+ failures += results[1]
+
+ if len(onu_data_to_set):
+ results = yield self.fix_attributes_only_on_olt(cid, eid, onu_data_to_set, olt_db, me_entry)
+ successes += results[0]
+ failures += results[1]
+
+ returnValue((successes, failures))
+
+ @inlineCallbacks
+ def fix_attributes_only_in_mib(self, cid, eid, mib_data):
+ successes = 0
+ failures = 0
+ try:
+ # Get current and verify same as during audit it is missing from our DB
+ attributes = mib_data.keys()
+ current_entry = self._device.query_mib(cid, eid, attributes)
+
+ if current_entry is not None and len(current_entry):
+ clashes = {k: v for k, v in current_entry.items()
+ if k in attributes and v != mib_data[k]}
+
+ if len(clashes):
+ raise ValueError('Existing DB entry for {}/{} attributes clash with audit data. Clash: {}'.
+ format(cid, eid, clashes))
+
+ self._sync_sm.mib_set(cid, eid, mib_data)
+ successes += len(mib_data)
+ self.strobe_watchdog()
+
+ # If we do nothing but DB updates for ALOT of MEs, we are
+ # blocking other async twisted tasks, be kind and yield
+ self._db_updates += 1
+ if self._db_updates >= MibReconcileTask.max_sequential_db_updates:
+ self._db_updates = 0
+ self._local_deferred = yield asleep(MibReconcileTask.db_update_pause)
+
+ except ValueError as e:
+ self.log.debug('attribute-changed', e)
+ failures += len(mib_data)
+
+ except Exception as e:
+ self.log.exception('attribute-only-fix-mib', e=e, cid=cid, eid=eid)
+ failures += len(mib_data)
+
+ returnValue((successes, failures))
+
+ @inlineCallbacks
+ def fix_attributes_only_on_olt(self, cid, eid, onu_data, olt_db, me_entry):
+ successes = 0
+ failures = 0
+
+ try:
+ # On olt only or in both. Either way OLT wins, first verify that
+ # the OLT version is still the same data that we want to
+ # update on the ONU. Verify the data for the OLT is the same as
+ # at time of audit
+ olt_db_entries = {k: v for k, v in olt_db[cid][eid][ATTRIBUTES_KEY].items()
+ if k in onu_data.keys()}
+ current_entries = self._sync_sm.query_mib(class_id=cid, instance_id=eid,
+ attributes=onu_data.keys())
+
+ still_the_same = all(current_entries.get(k) == v for k, v in olt_db_entries.items())
+ if not still_the_same:
+ returnValue((0, len(onu_data))) # Wait for it to stabilize
+
+ # OLT data still matches, do the set operations now
+ # while len(onu_data):
+ attributes_mask = me_entry.mask_for(*onu_data.keys())
+ frame = OmciFrame(transaction_id=None,
+ message_type=OmciSet.message_id,
+ omci_message=OmciSet(entity_class=cid,
+ entity_id=eid,
+ attributes_mask=attributes_mask,
+ data=onu_data))
+
+ results = yield self._device.omci_cc.send(frame)
+ self.check_status_and_state(results, 'onu-attribute-update')
+ successes += len(onu_data)
+ self._db_updates = 0
+
+ except Exception as e:
+ self.log.exception('attribute-only-fix-onu', e=e, cid=cid, eid=eid)
+ failures += len(onu_data)
+ self.strobe_watchdog()
+
+ returnValue((successes, failures))
+
+ @inlineCallbacks
+ def update_mib_data_sync(self):
+ """
+ As the final step of MIB resynchronization, the OLT sets the MIB data sync
+ attribute of the ONU data ME to some suitable value of its own choice. It
+ then sets its own record of the same attribute to the same value,
+ incremented by 1, as explained in clause
+
+ :return: (int, int) success, failure counts
+ """
+ # Get MDS to set, do not user zero
+
+ new_mds_value = self._sync_sm.mib_data_sync
+ if new_mds_value == 0:
+ self._sync_sm.increment_mib_data_sync()
+ new_mds_value = self._sync_sm.mib_data_sync
+
+ # Update it. The set response will be sent on the OMCI-CC pub/sub bus
+ # and the MIB Synchronizer will update this MDS value in the database
+ # if successful.
+ try:
+ frame = OntDataFrame(mib_data_sync=new_mds_value).set()
+
+ results = yield self._device.omci_cc.send(frame)
+ self.check_status_and_state(results, 'ont-data-mbs-update')
+ returnValue((1, 0))
+
+ except TimeoutError as e:
+ self.log.debug('ont-data-send-timeout', e=e)
+ returnValue((0, 1))
+
+ except Exception as e:
+ self.log.exception('ont-data-send', e=e, mds=new_mds_value)
+ returnValue((0, 1))
+
+ def check_status_and_state(self, results, operation=''):
+ """
+ Check the results of an OMCI response. An exception is thrown
+ if the task was cancelled or an error was detected.
+
+ :param results: (OmciFrame) OMCI Response frame
+ :param operation: (str) what operation was being performed
+ :return: True if successful, False if the entity existed (already created)
+ """
+ omci_msg = results.fields['omci_message'].fields
+ status = omci_msg['success_code']
+ error_mask = omci_msg.get('parameter_error_attributes_mask', 'n/a')
+ failed_mask = omci_msg.get('failed_attributes_mask', 'n/a')
+ unsupported_mask = omci_msg.get('unsupported_attributes_mask', 'n/a')
+ self.strobe_watchdog()
+
+ self.log.debug(operation, status=status, error_mask=error_mask,
+ failed_mask=failed_mask, unsupported_mask=unsupported_mask)
+
+ if status == RC.Success:
+ return True
+
+ elif status == RC.InstanceExists:
+ return False
+
+ msg = '{} failed with a status of {}, error_mask: {}, failed_mask: {}, unsupported_mask: {}'.\
+ format(operation, status, error_mask, failed_mask, unsupported_mask)
+
+ raise MibReconcileException(msg)
+
+ def _undecodable(self, cid_eid_list, me_map):
+ return [(cid, eid) for cid, eid in cid_eid_list if cid not in me_map]
+
+ def _onu_created(self, cid_eid_list, me_map):
+ return [(cid, eid) for cid, eid in cid_eid_list if cid in me_map and
+ (OP.Create not in me_map[cid].mandatory_operations and
+ OP.Create not in me_map[cid].optional_operations)]
+
+ def _olt_created(self, cid_eid_list, me_map):
+ return [(cid, eid) for cid, eid in cid_eid_list if cid in me_map and
+ (OP.Create in me_map[cid].mandatory_operations or
+ OP.Create in me_map[cid].optional_operations)]
diff --git a/python/adapters/extensions/omci/tasks/mib_resync_task.py b/python/adapters/extensions/omci/tasks/mib_resync_task.py
new file mode 100644
index 0000000..ef9c531
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/mib_resync_task.py
@@ -0,0 +1,427 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from twisted.internet.defer import inlineCallbacks, TimeoutError, failure, returnValue
+from twisted.internet import reactor
+from common.utils.asleep import asleep
+from voltha.extensions.omci.database.mib_db_dict import *
+from voltha.extensions.omci.omci_entities import OntData
+from voltha.extensions.omci.omci_defs import AttributeAccess, EntityOperations
+
+AA = AttributeAccess
+OP = EntityOperations
+
+class MibCopyException(Exception):
+ pass
+
+
+class MibDownloadException(Exception):
+ pass
+
+
+class MibResyncException(Exception):
+ pass
+
+
+class MibResyncTask(Task):
+ """
+ OpenOMCI MIB resynchronization Task
+
+ This task should get a copy of the MIB and compare compare it to a
+ copy of the database. When the MIB Upload command is sent to the ONU,
+ it should make a copy and source the data requested from this database.
+ The ONU can still source AVC's and the the OLT can still send config
+ commands to the actual.
+ """
+ task_priority = 240
+ name = "MIB Resynchronization Task"
+
+ max_db_copy_retries = 3
+ db_copy_retry_delay = 7
+
+ max_mib_upload_next_retries = 3
+ mib_upload_next_delay = 10 # Max * delay < 60 seconds
+ watchdog_timeout = 15 # Should be > max delay
+
+ def __init__(self, omci_agent, device_id):
+ """
+ Class initialization
+
+ :param omci_agent: (OpenOMCIAgent) OMCI Adapter agent
+ :param device_id: (str) ONU Device ID
+ """
+ super(MibResyncTask, self).__init__(MibResyncTask.name,
+ omci_agent,
+ device_id,
+ priority=MibResyncTask.task_priority,
+ exclusive=False)
+ self._local_deferred = None
+ self._device = omci_agent.get_device(device_id)
+ self._db_active = MibDbVolatileDict(omci_agent)
+ self._db_active.start()
+
+ def cancel_deferred(self):
+ super(MibResyncTask, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def start(self):
+ """
+ Start MIB Re-Synchronization task
+ """
+ super(MibResyncTask, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_mib_resync)
+ self._db_active.start()
+ self._db_active.add(self.device_id)
+
+ def stop(self):
+ """
+ Shutdown MIB Re-Synchronization task
+ """
+ self.log.debug('stopping')
+
+ self.cancel_deferred()
+ self._device = None
+ self._db_active.stop()
+ self._db_active = None
+ super(MibResyncTask, self).stop()
+
+ @inlineCallbacks
+ def perform_mib_resync(self):
+ """
+ Perform the MIB Resynchronization sequence
+
+ The sequence to be performed is:
+ - get a copy of the current MIB database (db_copy)
+
+ - perform MIB upload commands to get ONU's database and save this
+ to a local DB (db_active). Note that the ONU can still receive
+ create/delete/set/get operations from the operator and source
+ AVC notifications as well during this period.
+
+ - Compare the information in the db_copy to the db_active
+
+ During the mib upload process, the maximum time between mib upload next
+ requests is 1 minute.
+ """
+ self.log.debug('perform-mib-resync')
+
+ try:
+ results = yield self.snapshot_mib()
+ db_copy = results[0]
+
+ if db_copy is None:
+ e = MibCopyException('Failed to get local database copy')
+ self.deferred.errback(failure.Failure(e))
+
+ else:
+ number_of_commands = results[1]
+
+ # Start the MIB upload sequence
+ self.strobe_watchdog()
+ commands_retrieved = yield self.upload_mib(number_of_commands)
+
+ if commands_retrieved < number_of_commands:
+ e = MibDownloadException('Only retrieved {} of {} instances'.
+ format(commands_retrieved, number_of_commands))
+ self.deferred.errback(failure.Failure(e))
+ else:
+ # Compare the databases
+ active_copy = self._db_active.query(self.device_id)
+ on_olt_only, on_onu_only, attr_diffs = \
+ self.compare_mibs(db_copy, active_copy)
+
+ self.deferred.callback(
+ {
+ 'on-olt-only': on_olt_only if len(on_olt_only) else None,
+ 'on-onu-only': on_onu_only if len(on_onu_only) else None,
+ 'attr-diffs': attr_diffs if len(attr_diffs) else None,
+ 'olt-db': db_copy,
+ 'onu-db': active_copy
+ })
+
+ except Exception as e:
+ self.log.exception('resync', e=e)
+ self.deferred.errback(failure.Failure(e))
+
+ @inlineCallbacks
+ def snapshot_mib(self):
+ """
+ Snapshot the MIB on the ONU and create a copy of our local MIB database
+
+ :return: (pair) (db_copy, number_of_commands)
+ """
+ db_copy = None
+ number_of_commands = None
+
+ try:
+ max_tries = MibResyncTask.max_db_copy_retries - 1
+
+ for retries in xrange(0, max_tries + 1):
+ # Send MIB Upload so ONU snapshots its MIB
+ try:
+ self.strobe_watchdog()
+ number_of_commands = yield self.send_mib_upload()
+
+ if number_of_commands is None:
+ if retries >= max_tries:
+ db_copy = None
+ break
+
+ except (TimeoutError, ValueError) as e:
+ self.log.warn('timeout-or-value-error', e=e)
+ if retries >= max_tries:
+ raise
+
+ self.strobe_watchdog()
+ yield asleep(MibResyncTask.db_copy_retry_delay)
+ continue
+
+ # Get a snapshot of the local MIB database
+ db_copy = self._device.query_mib()
+ # if we made it this far, no need to keep trying
+ break
+
+ except Exception as e:
+ self.log.exception('mib-resync', e=e)
+ raise
+
+ # Handle initial failures
+
+ if db_copy is None or number_of_commands is None:
+ raise MibCopyException('Failed to snapshot MIB copy after {} retries'.
+ format(MibResyncTask.max_db_copy_retries))
+
+ returnValue((db_copy, number_of_commands))
+
+ @inlineCallbacks
+ def send_mib_upload(self):
+ """
+ Perform MIB upload command and get the number of entries to retrieve
+
+ :return: (int) Number of commands to execute or None on error
+ """
+ ########################################
+ # Begin MIB Upload
+ try:
+ self.strobe_watchdog()
+ results = yield self._device.omci_cc.send_mib_upload()
+
+ number_of_commands = results.fields['omci_message'].fields['number_of_commands']
+
+ if number_of_commands is None or number_of_commands <= 0:
+ raise ValueError('Number of commands was {}'.format(number_of_commands))
+
+ returnValue(number_of_commands)
+
+ except TimeoutError as e:
+ self.log.warn('mib-resync-get-timeout', e=e)
+ raise
+
+ @inlineCallbacks
+ def upload_mib(self, number_of_commands):
+ ########################################
+ # Begin MIB Upload
+ seq_no = None
+
+ for seq_no in xrange(number_of_commands):
+ max_tries = MibResyncTask.max_mib_upload_next_retries
+
+ for retries in xrange(0, max_tries):
+ try:
+ self.strobe_watchdog()
+ response = yield self._device.omci_cc.send_mib_upload_next(seq_no)
+
+ omci_msg = response.fields['omci_message'].fields
+ class_id = omci_msg['object_entity_class']
+ entity_id = omci_msg['object_entity_id']
+
+ # Filter out the 'mib_data_sync' from the database. We save that at
+ # the device level and do not want it showing up during a re-sync
+ # during data comparison
+ from binascii import hexlify
+ if class_id == OntData.class_id:
+ break
+
+ # The T&W ONU reports an ME with class ID 0 but only on audit. Perhaps others do as well.
+ if class_id == 0 or class_id > 0xFFFF:
+ self.log.warn('invalid-class-id', class_id=class_id)
+ break
+
+ attributes = {k: v for k, v in omci_msg['object_data'].items()}
+
+ # Save to the database
+ self._db_active.set(self.device_id, class_id, entity_id, attributes)
+ break
+
+ except TimeoutError:
+ self.log.warn('mib-resync-timeout', seq_no=seq_no,
+ number_of_commands=number_of_commands)
+
+ if retries < max_tries - 1:
+ self.strobe_watchdog()
+ yield asleep(MibResyncTask.mib_upload_next_delay)
+ else:
+ raise
+
+ except Exception as e:
+ self.log.exception('resync', e=e, seq_no=seq_no,
+ number_of_commands=number_of_commands)
+
+ returnValue(seq_no + 1) # seq_no is zero based.
+
+ def compare_mibs(self, db_copy, db_active):
+ """
+ Compare the our db_copy with the ONU's active copy
+
+ :param db_copy: (dict) OpenOMCI's copy of the database
+ :param db_active: (dict) ONU's database snapshot
+ :return: (dict), (dict), (list) Differences
+ """
+ self.strobe_watchdog()
+ me_map = self.omci_agent.get_device(self.device_id).me_map
+
+ # Class & Entities only in local copy (OpenOMCI)
+ on_olt_temp = self.get_lhs_only_dict(db_copy, db_active)
+
+ # Remove any entries that are not reported during an upload (but could
+ # be in our database copy. Retain undecodable class IDs.
+ on_olt_only = [(cid, eid) for cid, eid in on_olt_temp
+ if cid not in me_map or not me_map[cid].hidden]
+
+ # Further reduce the on_olt_only MEs reported in an audit to not
+ # include missed MEs that are ONU created. Not all ONUs report MEs
+ # that are ONU created unless we are doing the initial MIB upload.
+ # Adtran does report them, T&W may not as well as a few others
+ on_olt_only = [(cid, eid) for cid, eid in on_olt_only if cid in me_map and
+ (OP.Create in me_map[cid].mandatory_operations or
+ OP.Create in me_map[cid].optional_operations)]
+
+ # Class & Entities only on remote (ONU)
+ on_onu_only = self.get_lhs_only_dict(db_active, db_copy)
+
+ # Class & Entities on both local & remote, but one or more attributes
+ # are different on the ONU. This is the value that the local (OpenOMCI)
+ # thinks should be on the remote (ONU)
+
+ attr_diffs = self.get_attribute_diffs(db_copy, db_active, me_map)
+
+ # TODO: Note that certain MEs are excluded from the MIB upload. In particular,
+ # instances of some general purpose MEs, such as the Managed Entity ME and
+ # and the Attribute ME are not included in the MIB upload. Also all table
+ # attributes are not included in the MIB upload (but we do not yet support
+ # tables in this OpenOMCI implementation (VOLTHA v1.3.0)
+
+ return on_olt_only, on_onu_only, attr_diffs
+
+ def get_lhs_only_dict(self, lhs, rhs):
+ """
+ Compare two MIB database dictionaries and return the ME Class ID and
+ instances that are unique to the lhs dictionary. Both parameters
+ should be in the common MIB Database output dictionary format that
+ is returned by the mib 'query' command.
+
+ :param lhs: (dict) Left-hand-side argument.
+ :param rhs: (dict) Right-hand-side argument
+
+ return: (list(int,int)) List of tuples where (class_id, inst_id)
+ """
+ results = list()
+
+ for cls_id, cls_data in lhs.items():
+ # Get unique classes
+ #
+ # Skip keys that are not class IDs
+ if not isinstance(cls_id, int):
+ continue
+
+ if cls_id not in rhs:
+ results.extend([(cls_id, inst_id) for inst_id in cls_data.keys()
+ if isinstance(inst_id, int)])
+ else:
+ # Get unique instances of a class
+ lhs_cls = cls_data
+ rhs_cls = rhs[cls_id]
+
+ for inst_id, _ in lhs_cls.items():
+ # Skip keys that are not instance IDs
+ if isinstance(cls_id, int) and inst_id not in rhs_cls:
+ results.extend([(cls_id, inst_id)])
+
+ return results
+
+ def get_attribute_diffs(self, omci_copy, onu_copy, me_map):
+ """
+ Compare two OMCI MIBs and return the ME class and instance IDs that exists
+ on both the local copy and the remote ONU that have different attribute
+ values. Both parameters should be in the common MIB Database output
+ dictionary format that is returned by the mib 'query' command.
+
+ :param omci_copy: (dict) OpenOMCI copy (OLT-side) of the MIB Database
+ :param onu_copy: (dict) active ONU latest copy its database
+ :param me_map: (dict) ME Class ID MAP for this ONU
+
+ return: (list(int,int,str)) List of tuples where (class_id, inst_id, attribute)
+ points to the specific ME instance where attributes
+ are different
+ """
+ results = list()
+ ro_set = {AA.R}
+
+ # Get class ID's that are in both
+ class_ids = {cls_id for cls_id, _ in omci_copy.items()
+ if isinstance(cls_id, int) and cls_id in onu_copy}
+
+ for cls_id in class_ids:
+ # Get unique instances of a class
+ olt_cls = omci_copy[cls_id]
+ onu_cls = onu_copy[cls_id]
+
+ # Weed out read-only attributes. Attributes on onu may be read-only. These
+ # will only show up it the OpenOMCI (OLT-side) database if it changed and
+ # an AVC Notification was sourced by the ONU
+ # TODO: These class IDs could be calculated once at ONU startup (at device add)
+ if cls_id in me_map:
+ ro_attrs = {attr.field.name for attr in me_map[cls_id].attributes
+ if attr.access == ro_set}
+ else:
+ # Here if partially defined ME (not defined in ME Map)
+ from voltha.extensions.omci.omci_cc import UNKNOWN_CLASS_ATTRIBUTE_KEY
+ ro_attrs = {UNKNOWN_CLASS_ATTRIBUTE_KEY}
+
+ # Get set of common instance IDs
+ inst_ids = {inst_id for inst_id, _ in olt_cls.items()
+ if isinstance(inst_id, int) and inst_id in onu_cls}
+
+ for inst_id in inst_ids:
+ omci_attributes = {k for k in olt_cls[inst_id][ATTRIBUTES_KEY].iterkeys()}
+ onu_attributes = {k for k in onu_cls[inst_id][ATTRIBUTES_KEY].iterkeys()}
+
+ # Get attributes that exist in one database, but not the other
+ sym_diffs = (omci_attributes ^ onu_attributes) - ro_attrs
+ results.extend([(cls_id, inst_id, attr) for attr in sym_diffs])
+
+ # Get common attributes with different values
+ common_attributes = (omci_attributes & onu_attributes) - ro_attrs
+ results.extend([(cls_id, inst_id, attr) for attr in common_attributes
+ if olt_cls[inst_id][ATTRIBUTES_KEY][attr] !=
+ onu_cls[inst_id][ATTRIBUTES_KEY][attr]])
+ return results
diff --git a/python/adapters/extensions/omci/tasks/mib_upload.py b/python/adapters/extensions/omci/tasks/mib_upload.py
new file mode 100644
index 0000000..4afd234
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/mib_upload.py
@@ -0,0 +1,158 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from twisted.internet.defer import inlineCallbacks, TimeoutError, failure, AlreadyCalledError
+from twisted.internet import reactor
+from voltha.extensions.omci.omci_defs import ReasonCodes
+
+
+class MibUploadFailure(Exception):
+ """
+ This error is raised by default when the upload fails
+ """
+
+
+class MibUploadTask(Task):
+ """
+ OpenOMCI MIB upload task
+
+ On successful completion, this task will call the 'callback' method of the
+ deferred returned by the start method. Only a textual message is provided as
+ the successful results and it lists the number of ME entities successfully
+ retrieved.
+
+ Note that the MIB Synchronization State Machine will get event subscription
+ information for the MIB Reset and MIB Upload Next requests and it is the
+ MIB Synchronization State Machine that actually populates the MIB Database.
+ """
+ task_priority = 250
+ name = "MIB Upload Task"
+
+ def __init__(self, omci_agent, device_id):
+ """
+ Class initialization
+
+ :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+ :param device_id: (str) ONU Device ID
+ """
+ super(MibUploadTask, self).__init__(MibUploadTask.name,
+ omci_agent,
+ device_id,
+ priority=MibUploadTask.task_priority)
+ self._local_deferred = None
+
+ def cancel_deferred(self):
+ super(MibUploadTask, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def start(self):
+ """
+ Start MIB Synchronization tasks
+ """
+ super(MibUploadTask, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_mib_upload)
+
+ def stop(self):
+ """
+ Shutdown MIB Synchronization tasks
+ """
+ self.log.debug('stopping')
+
+ self.cancel_deferred()
+ super(MibUploadTask, self).stop()
+
+ @inlineCallbacks
+ def perform_mib_upload(self):
+ """
+ Perform the MIB Upload sequence
+ """
+ self.log.debug('perform-mib-upload')
+
+ seq_no = 0
+ number_of_commands = 0
+
+ try:
+ device = self.omci_agent.get_device(self.device_id)
+
+ #########################################
+ # MIB Reset
+ self.strobe_watchdog()
+ results = yield device.omci_cc.send_mib_reset()
+
+ status = results.fields['omci_message'].fields['success_code']
+ if status != ReasonCodes.Success.value:
+ raise MibUploadFailure('MIB Reset request failed with status code: {}'.
+ format(status))
+
+ ########################################
+ # Begin MIB Upload
+ self.strobe_watchdog()
+ results = yield device.omci_cc.send_mib_upload()
+
+ number_of_commands = results.fields['omci_message'].fields['number_of_commands']
+
+ for seq_no in xrange(number_of_commands):
+ if not device.active or not device.omci_cc.enabled:
+ raise MibUploadFailure('OMCI and/or ONU is not active')
+
+ for retry in range(0, 3):
+ try:
+ self.log.debug('mib-upload-next-request', seq_no=seq_no,
+ retry=retry,
+ number_of_commands=number_of_commands)
+ self.strobe_watchdog()
+ yield device.omci_cc.send_mib_upload_next(seq_no)
+
+ self.log.debug('mib-upload-next-success', seq_no=seq_no,
+ number_of_commands=number_of_commands)
+ break
+
+ except TimeoutError as e:
+ from common.utils.asleep import asleep
+ self.log.warn('mib-upload-timeout', e=e, seq_no=seq_no,
+ number_of_commands=number_of_commands)
+ if retry >= 2:
+ raise MibUploadFailure('Upload timeout failure on req {} of {}'.
+ format(seq_no + 1, number_of_commands))
+ self.strobe_watchdog()
+ yield asleep(0.3)
+
+ # Successful if here
+ self.log.info('mib-synchronized')
+ self.deferred.callback('success, loaded {} ME Instances'.
+ format(number_of_commands))
+
+ except TimeoutError as e:
+ self.log.warn('mib-upload-timeout-on-reset', e=e, seq_no=seq_no,
+ number_of_commands=number_of_commands)
+ self.deferred.errback(failure.Failure(e))
+
+ except AlreadyCalledError:
+ # Can occur if task canceled due to MIB Sync state change
+ self.log.debug('already-called-exception', seq_no=seq_no,
+ number_of_commands=number_of_commands)
+ assert self.deferred.called, \
+ 'Unexpected AlreadyCalledError exception: seq: {} of {}'.format(seq_no,
+ number_of_commands)
+ except Exception as e:
+ self.log.exception('mib-upload', e=e)
+ self.deferred.errback(failure.Failure(e))
diff --git a/python/adapters/extensions/omci/tasks/omci_create_pm_task.py b/python/adapters/extensions/omci/tasks/omci_create_pm_task.py
new file mode 100644
index 0000000..355e26a
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/omci_create_pm_task.py
@@ -0,0 +1,150 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, failure, TimeoutError
+from voltha.extensions.omci.omci_defs import ReasonCodes, EntityOperations
+from voltha.extensions.omci.omci_frame import OmciFrame
+from voltha.extensions.omci.omci_messages import OmciCreate
+
+RC = ReasonCodes
+OP = EntityOperations
+
+
+class CreatePMException(Exception):
+ pass
+
+
+class OmciCreatePMRequest(Task):
+ """
+ OpenOMCI routine to create the requested PM Interval MEs
+
+ TODO: Support of thresholding crossing alarms will be in a future VOLTHA release
+ """
+ task_priority = Task.DEFAULT_PRIORITY
+ name = "ONU OMCI Create PM ME Task"
+
+ def __init__(self, omci_agent, device_id, me_dict, exclusive=False):
+ """
+ Class initialization
+
+ :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+ :param me_dict: (dict) (pm cid, pm eid) -> (me cid, me eid, upstream)
+ :param exclusive: (bool) True if this Create request Task exclusively own the
+ OMCI-CC while running. Default: False
+ """
+ super(OmciCreatePMRequest, self).__init__(OmciCreatePMRequest.name,
+ omci_agent,
+ device_id,
+ priority=OmciCreatePMRequest.task_priority,
+ exclusive=exclusive)
+ self._device = omci_agent.get_device(device_id)
+ self._me_dict = me_dict
+ self._local_deferred = None
+
+ def cancel_deferred(self):
+ super(OmciCreatePMRequest, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def start(self):
+ """ Start task """
+ super(OmciCreatePMRequest, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_create)
+
+ @inlineCallbacks
+ def perform_create(self):
+ """ Perform the create requests """
+
+ try:
+ for pm, me in self._me_dict.items():
+ pm_class_id = pm[0]
+ pm_entity_id = pm[1]
+ me_class_id = me[0]
+ me_entity_id = me[1]
+ upstream = me[2]
+ self.log.debug('create-pm-me', class_id=pm_class_id, entity_id=pm_entity_id)
+
+ if me_class_id == 0:
+ # Typical/common PM interval format
+ frame = OmciFrame(
+ transaction_id=None, # OMCI-CC will set
+ message_type=OmciCreate.message_id,
+ omci_message=OmciCreate(
+ entity_class=pm_class_id,
+ entity_id=pm_entity_id,
+ data=dict()
+ )
+ )
+ else:
+ # Extended PM interval format. See ITU-T G.988 Section 9.3.32.
+ # Bit 1 - continuous accumulation if set, 15-minute interval if unset
+ # Bit 2 - directionality (0=upstream, 1=downstream)
+ # Bit 3..14 - Reserved
+ # Bit 15 - Use P bits of TCI field to filter
+ # Bit 16 - Use VID bits of TCI field to filter
+ bitmap = 0 if upstream else 1 << 1
+
+ data = {'control_block': [
+ 0, # Threshold data 1/2 ID
+ me_class_id, # Parent ME Class
+ me_entity_id, # Parent ME Instance
+ 0, # Accumulation disable
+ 0, # TCA Disable
+ bitmap, # Control fields bitmap
+ 0, # TCI
+ 0 # Reserved
+ ]}
+ frame = OmciFrame(
+ transaction_id=None, # OMCI-CC will set
+ message_type=OmciCreate.message_id,
+ omci_message=OmciCreate(
+ entity_class=pm_class_id,
+ entity_id=pm_entity_id,
+ data=data
+ )
+ )
+ self.strobe_watchdog()
+ try:
+ results = yield self._device.omci_cc.send(frame)
+ except TimeoutError:
+ self.log.warning('perform-create-timeout', me_class_id=me_class_id, me_entity_id=me_entity_id,
+ pm_class_id=pm_class_id, pm_entity_id=pm_entity_id)
+ raise
+
+ status = results.fields['omci_message'].fields['success_code']
+ self.log.debug('perform-create-status', status=status)
+
+ # Did it fail
+ if status != RC.Success.value and status != RC.InstanceExists.value:
+ msg = 'ME: {}, entity: {} failed with status {}'.format(pm_class_id,
+ pm_entity_id,
+ status)
+ raise CreatePMException(msg)
+
+ self.log.debug('create-pm-success', class_id=pm_class_id,
+ entity_id=pm_entity_id)
+
+ self.deferred.callback(self)
+
+ except Exception as e:
+ self.log.exception('perform-create', e=e)
+ self.deferred.errback(failure.Failure(e))
diff --git a/python/adapters/extensions/omci/tasks/omci_delete_pm_task.py b/python/adapters/extensions/omci/tasks/omci_delete_pm_task.py
new file mode 100644
index 0000000..adf1ce2
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/omci_delete_pm_task.py
@@ -0,0 +1,108 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, failure
+from voltha.extensions.omci.omci_defs import ReasonCodes, EntityOperations
+from voltha.extensions.omci.omci_frame import OmciFrame
+from voltha.extensions.omci.omci_messages import OmciDelete
+
+RC = ReasonCodes
+OP = EntityOperations
+
+
+class DeletePMException(Exception):
+ pass
+
+
+class OmciDeletePMRequest(Task):
+ """
+ OpenOMCI routine to delete the requested PM Interval MEs
+ """
+ task_priority = Task.DEFAULT_PRIORITY
+ name = "ONU OMCI Delete PM ME Task"
+
+ def __init__(self, omci_agent, device_id, me_set, exclusive=False):
+ """
+ Class initialization
+
+ :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+ :param me_set: (set) Tuples of class_id / entity_id to create
+ :param exclusive: (bool) True if this Create request Task exclusively own the
+ OMCI-CC while running. Default: False
+ """
+ super(OmciDeletePMRequest, self).__init__(OmciDeletePMRequest.name,
+ omci_agent,
+ device_id,
+ priority=OmciDeletePMRequest.task_priority,
+ exclusive=exclusive)
+ self._device = omci_agent.get_device(device_id)
+ self._me_tuples = me_set
+ self._local_deferred = None
+
+ def cancel_deferred(self):
+ super(OmciDeletePMRequest, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def start(self):
+ """ Start task """
+ super(OmciDeletePMRequest, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_delete)
+
+ @inlineCallbacks
+ def perform_delete(self):
+ """ Perform the delete requests """
+ self.log.debug('perform-delete')
+
+ try:
+ for me in self._me_tuples:
+ class_id = me[0]
+ entity_id = me[1]
+
+ frame = OmciFrame(
+ transaction_id=None,
+ message_type=OmciDelete.message_id,
+ omci_message=OmciDelete(
+ entity_class=class_id,
+ entity_id=entity_id
+ )
+ )
+ self.strobe_watchdog()
+ results = yield self._device.omci_cc.send(frame)
+
+ status = results.fields['omci_message'].fields['success_code']
+ self.log.debug('perform-delete-status', status=status)
+
+ # Did it fail, it instance does not exist, not an error
+ if status != RC.Success.value and status != RC.UnknownInstance.value:
+ msg = 'ME: {}, entity: {} failed with status {}'.format(class_id,
+ entity_id,
+ status)
+ raise DeletePMException(msg)
+
+ self.log.debug('delete-pm-success', class_id=class_id,
+ entity_id=entity_id)
+ self.deferred.callback(self)
+
+ except Exception as e:
+ self.log.exception('perform-create', e=e)
+ self.deferred.errback(failure.Failure(e))
diff --git a/python/adapters/extensions/omci/tasks/omci_get_request.py b/python/adapters/extensions/omci/tasks/omci_get_request.py
new file mode 100644
index 0000000..c325278
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/omci_get_request.py
@@ -0,0 +1,356 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from twisted.internet import reactor
+from twisted.internet.defer import failure, inlineCallbacks, TimeoutError, returnValue
+from voltha.extensions.omci.omci_defs import ReasonCodes, EntityOperations
+from voltha.extensions.omci.omci_me import MEFrame
+from voltha.extensions.omci.omci_frame import OmciFrame
+from voltha.extensions.omci.omci_cc import DEFAULT_OMCI_TIMEOUT
+from voltha.extensions.omci.omci_messages import OmciGet
+from voltha.extensions.omci.omci_fields import OmciTableField
+
+RC = ReasonCodes
+OP = EntityOperations
+
+
+class GetException(Exception):
+ pass
+
+
+class OmciGetRequest(Task):
+ """
+ OpenOMCI Get an OMCI ME Instance Attributes
+
+ Upon completion, the Task deferred callback is invoked with a reference of
+ this Task object.
+
+ The Task has an initializer option (allow_failure) that will retry all
+ requested attributes if the original request fails with a status code of
+ 9 (Attributes failed or unknown). This result means that an attribute
+ is not supported by the ONU or that a mandatory/optional attribute could
+ not be executed by the ONU, even if it is supported, for example,
+ because of a range or type violation.
+ """
+ task_priority = 128
+ name = "ONU OMCI Get Task"
+
+ def __init__(self, omci_agent, device_id, entity_class, entity_id, attributes,
+ exclusive=False, allow_failure=False):
+ """
+ Class initialization
+
+ :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+ :param device_id: (str) ONU Device ID
+ :param entity_class: (EntityClass) ME Class to retrieve
+ :param entity_id: (int) ME Class instance ID to retrieve
+ :param attributes: (list or set) Name of attributes to retrieve
+ :param exclusive: (bool) True if this GET request Task exclusively own the
+ OMCI-CC while running. Default: False
+ :param allow_failure: (bool) If true, attempt to get all valid attributes
+ if the original request receives an error
+ code of 9 (Attributes failed or unknown).
+ """
+ super(OmciGetRequest, self).__init__(OmciGetRequest.name,
+ omci_agent,
+ device_id,
+ priority=OmciGetRequest.task_priority,
+ exclusive=exclusive)
+ self._device = omci_agent.get_device(device_id)
+ self._entity_class = entity_class
+ self._entity_id = entity_id
+ self._attributes = attributes
+ self._allow_failure = allow_failure
+ self._failed_or_unknown_attributes = set()
+ self._results = None
+ self._local_deferred = None
+
+ def cancel_deferred(self):
+ super(OmciGetRequest, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ @property
+ def me_class(self):
+ """The OMCI Managed Entity Class associated with this request"""
+ return self._entity_class
+
+ @property
+ def entity_id(self):
+ """The ME Entity ID associated with this request"""
+ return self._entity_id
+
+ @property
+ def attributes(self):
+ """
+ Return a dictionary of attributes for the request if the Get was
+ successfully completed. None otherwise
+ """
+ if self._results is None:
+ return None
+
+ omci_msg = self._results.fields['omci_message'].fields
+ return omci_msg['data'] if 'data' in omci_msg else None
+
+ @property
+ def success_code(self):
+ """
+ Return the OMCI success/reason code for the Get Response.
+ """
+ if self._results is None:
+ return None
+
+ return self._results.fields['omci_message'].fields['success_code']
+
+ @property
+ def raw_results(self):
+ """
+ Return the raw Get Response OMCIFrame
+ """
+ return self._results
+
+ def start(self):
+ """
+ Start MIB Capabilities task
+ """
+ super(OmciGetRequest, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_get_omci)
+
+ @property
+ def failed_or_unknown_attributes(self):
+ """
+ Returns a set attributes that failed or unknown in the original get
+ request that resulted in an initial status code of 9 (Attributes
+ failed or unknown).
+
+ :return: (set of str) attributes
+ """
+ return self._failed_or_unknown_attributes
+
+ @inlineCallbacks
+ def perform_get_omci(self):
+ """
+ Perform the initial get request
+ """
+ self.log.info('perform-get', entity_class=self._entity_class,
+ entity_id=self._entity_id, attributes=self._attributes)
+ try:
+ # If one or more attributes is a table attribute, get it separately
+ def is_table_attr(attr):
+ index = self._entity_class.attribute_name_to_index_map[attr]
+ attr_def = self._entity_class.attributes[index]
+ return isinstance(attr_def.field, OmciTableField)
+
+ first_attributes = {attr for attr in self._attributes if not is_table_attr(attr)}
+ table_attributes = {attr for attr in self._attributes if is_table_attr(attr)}
+
+ frame = MEFrame(self._entity_class, self._entity_id, first_attributes).get()
+ self.strobe_watchdog()
+ results = yield self._device.omci_cc.send(frame)
+
+ status = results.fields['omci_message'].fields['success_code']
+ self.log.debug('perform-get-status', status=status)
+
+ # Success?
+ if status == RC.Success.value:
+ self._results = results
+ results_omci = results.fields['omci_message'].fields
+
+ # Were all attributes fetched?
+ missing_attr = frame.fields['omci_message'].fields['attributes_mask'] ^ \
+ results_omci['attributes_mask']
+
+ if missing_attr > 0 or len(table_attributes) > 0:
+ self.log.info('perform-get-missing', num_missing=missing_attr,
+ table_attr=table_attributes)
+ self.strobe_watchdog()
+ self._local_deferred = reactor.callLater(0,
+ self.perform_get_missing_attributes,
+ missing_attr,
+ table_attributes)
+ returnValue(self._local_deferred)
+
+ elif status == RC.AttributeFailure.value:
+ # What failed? Note if only one attribute was attempted, then
+ # that is an overall failure
+
+ if not self._allow_failure or len(self._attributes) <= 1:
+ raise GetException('Get failed with status code: {}'.
+ format(RC.AttributeFailure.value))
+
+ self.strobe_watchdog()
+ self._local_deferred = reactor.callLater(0,
+ self.perform_get_failed_attributes,
+ results,
+ self._attributes)
+ returnValue(self._local_deferred)
+
+ else:
+ raise GetException('Get failed with status code: {}'.format(status))
+
+ self.log.debug('get-completed')
+ self.deferred.callback(self)
+
+ except TimeoutError as e:
+ self.deferred.errback(failure.Failure(e))
+
+ except Exception as e:
+ self.log.exception('perform-get', e=e, class_id=self._entity_class,
+ entity_id=self._entity_id, attributes=self._attributes)
+ self.deferred.errback(failure.Failure(e))
+
+ @inlineCallbacks
+ def perform_get_missing_attributes(self, missing_attr, table_attributes):
+ """
+ This method is called when the original Get requests completes with success
+ but not all attributes were returned. This can happen if one or more of the
+ attributes would have exceeded the space available in the OMCI frame.
+
+ This routine iterates through the missing attributes and attempts to retrieve
+ the ones that were missing.
+
+ :param missing_attr: (int) Missing attributes bitmask
+ :param table_attributes: (set) Attributes that need table get/get-next support
+ """
+ self.log.debug('perform-get-missing', attrs=missing_attr, tbl=table_attributes)
+
+ # Retrieve missing attributes first (if any)
+ results_omci = self._results.fields['omci_message'].fields
+
+ for index in xrange(16):
+ attr_mask = 1 << index
+
+ if attr_mask & missing_attr:
+ # Get this attribute
+ frame = OmciFrame(
+ transaction_id=None, # OMCI-CC will set
+ message_type=OmciGet.message_id,
+ omci_message=OmciGet(
+ entity_class=self._entity_class.class_id,
+ entity_id=self._entity_id,
+ attributes_mask=attr_mask
+ )
+ )
+ try:
+ self.strobe_watchdog()
+ get_results = yield self._device.omci_cc.send(frame)
+
+ get_omci = get_results.fields['omci_message'].fields
+ if get_omci['success_code'] != RC.Success.value:
+ continue
+
+ assert attr_mask == get_omci['attributes_mask'], 'wrong attribute'
+ results_omci['attributes_mask'] |= attr_mask
+
+ if results_omci.get('data') is None:
+ results_omci['data'] = dict()
+
+ results_omci['data'].update(get_omci['data'])
+
+ except TimeoutError:
+ self.log.debug('missing-timeout')
+
+ except Exception as e:
+ self.log.exception('missing-failure', e=e)
+
+ # Now any table attributes. OMCI_CC handles background get/get-next sequencing
+ for tbl_attr in table_attributes:
+ attr_mask = self._entity_class.mask_for(tbl_attr)
+ frame = OmciFrame(
+ transaction_id=None, # OMCI-CC will set
+ message_type=OmciGet.message_id,
+ omci_message=OmciGet(
+ entity_class=self._entity_class.class_id,
+ entity_id=self._entity_id,
+ attributes_mask=attr_mask
+ )
+ )
+ try:
+ timeout = 2 * DEFAULT_OMCI_TIMEOUT # Multiple frames expected
+ self.strobe_watchdog()
+ get_results = yield self._device.omci_cc.send(frame,
+ timeout=timeout)
+ self.strobe_watchdog()
+ get_omci = get_results.fields['omci_message'].fields
+ if get_omci['success_code'] != RC.Success.value:
+ continue
+
+ if results_omci.get('data') is None:
+ results_omci['data'] = dict()
+
+ results_omci['data'].update(get_omci['data'])
+
+ except TimeoutError:
+ self.log.debug('tbl-attr-timeout')
+
+ except Exception as e:
+ self.log.exception('tbl-attr-timeout', e=e)
+
+ self.deferred.callback(self)
+
+ @inlineCallbacks
+ def perform_get_failed_attributes(self, tmp_results, attributes):
+ """
+
+ :param tmp_results:
+ :param attributes:
+ :return:
+ """
+ self.log.debug('perform-get-failed', attrs=attributes)
+
+ for attr in attributes:
+ try:
+ frame = MEFrame(self._entity_class, self._entity_id, {attr}).get()
+
+ self.strobe_watchdog()
+ results = yield self._device.omci_cc.send(frame)
+
+ status = results.fields['omci_message'].fields['success_code']
+
+ if status == RC.AttributeFailure.value:
+ self.log.debug('unknown-or-invalid-attribute', attr=attr, status=status)
+ self._failed_or_unknown_attributes.add(attr)
+
+ elif status != RC.Success.value:
+ self.log.warn('invalid-get', class_id=self._entity_class,
+ attribute=attr, status=status)
+ self._failed_or_unknown_attributes.add(attr)
+
+ else:
+ # Add to partial results and correct the status
+ tmp_results.fields['omci_message'].fields['success_code'] = status
+ tmp_results.fields['omci_message'].fields['attributes_mask'] |= \
+ results.fields['omci_message'].fields['attributes_mask']
+
+ if tmp_results.fields['omci_message'].fields.get('data') is None:
+ tmp_results.fields['omci_message'].fields['data'] = dict()
+
+ tmp_results.fields['omci_message'].fields['data'][attr] = \
+ results.fields['omci_message'].fields['data'][attr]
+
+ except TimeoutError as e:
+ self.log.debug('attr-timeout')
+
+ except Exception as e:
+ self.log.exception('attr-failure', e=e)
+
+ self._results = tmp_results
+ self.deferred.callback(self)
diff --git a/python/adapters/extensions/omci/tasks/omci_modify_request.py b/python/adapters/extensions/omci/tasks/omci_modify_request.py
new file mode 100644
index 0000000..da7bff5
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/omci_modify_request.py
@@ -0,0 +1,171 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, failure, returnValue
+from voltha.extensions.omci.omci_defs import ReasonCodes, EntityOperations
+from voltha.extensions.omci.omci_me import MEFrame
+from voltha.extensions.omci.omci_frame import OmciFrame
+from voltha.extensions.omci.omci_messages import OmciCreate, OmciSet, OmciDelete
+from voltha.extensions.omci.omci_entities import EntityClass
+
+RC = ReasonCodes
+OP = EntityOperations
+
+
+class ModifyException(Exception):
+ pass
+
+
+class OmciModifyRequest(Task):
+ """
+ OpenOMCI Generic Create, Set, or Delete Frame support Task.
+
+ This task allows an ONU to send a Create, Set, or Delete request from any point in their
+ code while properly using the OMCI-CC channel. Direct access to the OMCI-CC object
+ to send requests by an ONU is highly discouraged.
+ """
+ task_priority = 128
+ name = "ONU OMCI Modify Task"
+
+ def __init__(self, omci_agent, device_id, frame, priority=task_priority, exclusive=False):
+ """
+ Class initialization
+
+ :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+ :param device_id: (str) ONU Device ID
+ :param frame: (OmciFrame) Frame to send
+ :param priority: (int) OpenOMCI Task priority (0..255) 255 is the highest
+ :param exclusive: (bool) True if this GET request Task exclusively own the
+ OMCI-CC while running. Default: False
+ """
+ super(OmciModifyRequest, self).__init__(OmciModifyRequest.name,
+ omci_agent,
+ device_id,
+ priority=priority,
+ exclusive=exclusive)
+ self._device = omci_agent.get_device(device_id)
+ self._frame = frame
+ self._results = None
+ self._local_deferred = None
+
+ # Validate message type
+ self._msg_type = frame.fields['message_type']
+ if self._msg_type not in (OmciCreate.message_id, OmciSet.message_id, OmciDelete.message_id):
+ raise TypeError('Invalid Message type: {}, must be Create, Set, or Delete'.
+ format(self._msg_type))
+
+ def cancel_deferred(self):
+ super(OmciModifyRequest, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ @property
+ def success_code(self):
+ """
+ Return the OMCI success/reason code for the Get Response.
+ """
+ if self._results is None:
+ return None
+
+ return self._results.fields['omci_message'].fields['success_code']
+
+ @property
+ def illegal_attributes_mask(self):
+ """
+ For Create & Set requests, a failure may indicate that one or more
+ attributes have an illegal value. This property returns any illegal
+ attributes
+
+ :return: None if not a create/set request, otherwise the attribute mask
+ of illegal attributes
+ """
+ if self._results is None:
+ return None
+
+ omci_msg = self._results.fields['omci_message'].fields
+
+ if self._msg_type == OmciCreate.message_id:
+ if self.success_code != RC.ParameterError:
+ return 0
+ return omci_msg['parameter_error_attributes_mask']
+
+ elif self._msg_type == OmciSet.message_id:
+ if self.success_code != RC.AttributeFailure:
+ return 0
+ return omci_msg['failed_attributes_mask']
+
+ return None
+
+ @property
+ def unsupported_attributes_mask(self):
+ """
+ For Set requests, a failure may indicate that one or more attributes
+ are not supported by this ONU. This property returns any those unsupported attributes
+
+ :return: None if not a set request, otherwise the attribute mask of any illegal
+ parameters
+ """
+ if self._msg_type != OmciSet.message_id or self._results is None:
+ return None
+
+ if self.success_code != RC.AttributeFailure:
+ return 0
+
+ return self._results.fields['omci_message'].fields['unsupported_attributes_mask']
+
+ @property
+ def raw_results(self):
+ """
+ Return the raw Response OMCIFrame
+ """
+ return self._results
+
+ def start(self):
+ """
+ Start MIB Capabilities task
+ """
+ super(OmciModifyRequest, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_omci)
+
+ @inlineCallbacks
+ def perform_omci(self):
+ """
+ Perform the request
+ """
+ self.log.debug('perform-request')
+
+ try:
+ self.strobe_watchdog()
+ self._results = yield self._device.omci_cc.send(self._frame)
+
+ status = self._results.fields['omci_message'].fields['success_code']
+ self.log.debug('response-status', status=status)
+
+ # Success?
+ if status in (RC.Success.value, RC.InstanceExists):
+ self.deferred.callback(self)
+ else:
+ raise ModifyException('Failed with status {}'.format(status))
+
+ except Exception as e:
+ self.log.exception('perform-modify', e=e)
+ self.deferred.errback(failure.Failure(e))
diff --git a/python/adapters/extensions/omci/tasks/omci_sw_image_upgrade_task.py b/python/adapters/extensions/omci/tasks/omci_sw_image_upgrade_task.py
new file mode 100644
index 0000000..5eaa87c
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/omci_sw_image_upgrade_task.py
@@ -0,0 +1,64 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import structlog
+from task import Task
+from twisted.internet import reactor
+from voltha.protos.voltha_pb2 import ImageDownload
+
+class OmciSwImageUpgradeTask(Task):
+ name = "OMCI Software Image Upgrade Task"
+
+
+ def __init__(self, img_id, omci_upgrade_sm_cls, omci_agent, image_download, clock=None):
+ super(OmciSwImageUpgradeTask, self).__init__(OmciSwImageUpgradeTask.name, omci_agent, image_download.id,
+ exclusive=False,
+ watchdog_timeout=45)
+ self.log.debug("OmciSwImageUpgradeTask create ", image_id=img_id)
+ self._image_id = img_id
+ self._omci_upgrade_sm_cls = omci_upgrade_sm_cls
+ # self._omci_agent = omci_agent
+ self._image_download = image_download
+ self.reactor = clock if clock is not None else reactor
+ self._omci_upgrade_sm = None
+ self.log.debug("OmciSwImageUpgradeTask create end", image_id=img_id)
+
+ @property
+ def status(self):
+ return self._image_download
+
+ def start(self):
+ self.log.debug("OmciSwImageUpgradeTask start")
+ super(OmciSwImageUpgradeTask, self).start()
+ if self._omci_upgrade_sm is None:
+ self._omci_upgrade_sm = self._omci_upgrade_sm_cls(self._image_id, self.omci_agent, self._image_download, clock=self.reactor)
+ d = self._omci_upgrade_sm.start()
+ d.chainDeferred(self.deferred)
+ #else:
+ # if restart:
+ # self._omci_upgrade_sm.reset_image()
+
+ def stop(self):
+ self.log.debug("OmciSwImageUpgradeTask stop")
+ if self._omci_upgrade_sm is not None:
+ self._omci_upgrade_sm.stop()
+ self._omci_upgrade_sm = None
+
+ def onu_bootup(self):
+ self.log.debug("onu_bootup", state=self._omci_upgrade_sm.status.image_state);
+ if self._omci_upgrade_sm is not None \
+ and self._omci_upgrade_sm.status.image_state == ImageDownload.IMAGE_ACTIVATE:
+ self._omci_upgrade_sm.do_commit()
+
diff --git a/python/adapters/extensions/omci/tasks/onu_capabilities_task.py b/python/adapters/extensions/omci/tasks/onu_capabilities_task.py
new file mode 100644
index 0000000..048382c
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/onu_capabilities_task.py
@@ -0,0 +1,282 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from binascii import hexlify
+from twisted.internet.defer import inlineCallbacks, failure, returnValue
+from twisted.internet import reactor
+from voltha.extensions.omci.omci_defs import ReasonCodes
+from voltha.extensions.omci.omci_me import OmciFrame
+from voltha.extensions.omci.omci import EntityOperations
+
+
+class GetNextException(Exception):
+ pass
+
+
+class GetCapabilitiesFailure(Exception):
+ pass
+
+
+class OnuCapabilitiesTask(Task):
+ """
+ OpenOMCI MIB Capabilities Task
+
+ This task requests information on supported MEs via the OMCI (ME#287)
+ Managed entity.
+
+ This task should be ran after MIB Synchronization and before any MIB
+ Downloads to the ONU.
+
+ Upon completion, the Task deferred callback is invoked with dictionary
+ containing the supported managed entities and message types.
+
+ results = {
+ 'supported-managed-entities': {set of supported managed entities},
+ 'supported-message-types': {set of supported message types}
+ }
+ """
+ task_priority = 240
+ name = "ONU Capabilities Task"
+
+ max_mib_get_next_retries = 3
+ mib_get_next_delay = 5
+ DEFAULT_OCTETS_PER_MESSAGE = 29
+
+ def __init__(self, omci_agent, device_id, omci_pdu_size=DEFAULT_OCTETS_PER_MESSAGE):
+ """
+ Class initialization
+
+ :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+ :param device_id: (str) ONU Device ID
+ :param omci_pdu_size: (int) OMCI Data payload size (not counting any trailers)
+ """
+ super(OnuCapabilitiesTask, self).__init__(OnuCapabilitiesTask.name,
+ omci_agent,
+ device_id,
+ priority=OnuCapabilitiesTask.task_priority)
+ self._local_deferred = None
+ self._device = omci_agent.get_device(device_id)
+ self._pdu_size = omci_pdu_size
+ self._supported_entities = set()
+ self._supported_msg_types = set()
+
+ def cancel_deferred(self):
+ super(OnuCapabilitiesTask, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ @property
+ def supported_managed_entities(self):
+ """
+ Return a set of the Managed Entity class IDs supported on this ONU
+
+ None is returned if no MEs have been discovered
+
+ :return: (set of ints)
+ """
+ return frozenset(self._supported_entities) if len(self._supported_entities) else None
+
+ @property
+ def supported_message_types(self):
+ """
+ Return a set of the Message Types supported on this ONU
+
+ None is returned if no message types have been discovered
+
+ :return: (set of EntityOperations)
+ """
+ return frozenset(self._supported_msg_types) if len(self._supported_msg_types) else None
+
+ def start(self):
+ """
+ Start MIB Capabilities task
+ """
+ super(OnuCapabilitiesTask, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_get_capabilities)
+
+ def stop(self):
+ """
+ Shutdown MIB Capabilities task
+ """
+ self.log.debug('stopping')
+
+ self.cancel_deferred()
+ self._device = None
+ super(OnuCapabilitiesTask, self).stop()
+
+ @inlineCallbacks
+ def perform_get_capabilities(self):
+ """
+ Perform the MIB Capabilities sequence.
+
+ The sequence is to perform a Get request with the attribute mask equal
+ to 'me_type_table'. The response to this request will carry the size
+ of (number of get-next sequences).
+
+ Then a loop is entered and get-next commands are sent for each sequence
+ requested.
+ """
+ self.log.debug('perform-get')
+
+ try:
+ self.strobe_watchdog()
+ self._supported_entities = yield self.get_supported_entities()
+
+ self.strobe_watchdog()
+ self._supported_msg_types = yield self.get_supported_message_types()
+
+ self.log.debug('get-success',
+ supported_entities=self.supported_managed_entities,
+ supported_msg_types=self.supported_message_types)
+ results = {
+ 'supported-managed-entities': self.supported_managed_entities,
+ 'supported-message-types': self.supported_message_types
+ }
+ self.deferred.callback(results)
+
+ except Exception as e:
+ self.log.exception('perform-get', e=e)
+ self.deferred.errback(failure.Failure(e))
+
+ def get_count_from_data_buffer(self, data):
+ """
+ Extract the 4 octet buffer length from the OMCI PDU contents
+ """
+ self.log.debug('get-count-buffer', data=hexlify(data))
+ return int(hexlify(data[:4]), 16)
+
+ @inlineCallbacks
+ def get_supported_entities(self):
+ """
+ Get the supported ME Types for this ONU.
+ """
+ try:
+ # Get the number of requests needed
+ frame = OmciFrame(me_type_table=True).get()
+ self.strobe_watchdog()
+ results = yield self._device.omci_cc.send(frame)
+
+ omci_msg = results.fields['omci_message']
+ status = omci_msg.fields['success_code']
+
+ if status != ReasonCodes.Success.value:
+ raise GetCapabilitiesFailure('Get count of supported entities failed with status code: {}'.
+ format(status))
+ data = omci_msg.fields['data']['me_type_table']
+ count = self.get_count_from_data_buffer(bytearray(data))
+
+ seq_no = 0
+ data_buffer = bytearray(0)
+ self.log.debug('me-type-count', octets=count, data=hexlify(data))
+
+ # Start the loop
+ for offset in xrange(0, count, self._pdu_size):
+ frame = OmciFrame(me_type_table=seq_no).get_next()
+ seq_no += 1
+ self.strobe_watchdog()
+ results = yield self._device.omci_cc.send(frame)
+
+ omci_msg = results.fields['omci_message']
+ status = omci_msg.fields['success_code']
+
+ if status != ReasonCodes.Success.value:
+ raise GetCapabilitiesFailure(
+ 'Get supported entities request at offset {} of {} failed with status code: {}'.
+ format(offset + 1, count, status))
+
+ # Extract the data
+ num_octets = count - offset
+ if num_octets > self._pdu_size:
+ num_octets = self._pdu_size
+
+ data = omci_msg.fields['data']['me_type_table']
+ data_buffer += bytearray(data[:num_octets])
+
+ me_types = {(data_buffer[x] << 8) + data_buffer[x + 1]
+ for x in xrange(0, len(data_buffer), 2)}
+ returnValue(me_types)
+
+ except Exception as e:
+ self.log.exception('get-entities', e=e)
+ self.deferred.errback(failure.Failure(e))
+
+ @inlineCallbacks
+ def get_supported_message_types(self):
+ """
+ Get the supported Message Types (actions) for this ONU.
+ """
+ try:
+ # Get the number of requests needed
+ frame = OmciFrame(message_type_table=True).get()
+ self.strobe_watchdog()
+ results = yield self._device.omci_cc.send(frame)
+
+ omci_msg = results.fields['omci_message']
+ status = omci_msg.fields['success_code']
+
+ if status != ReasonCodes.Success.value:
+ raise GetCapabilitiesFailure('Get count of supported msg types failed with status code: {}'.
+ format(status))
+
+ data = omci_msg.fields['data']['message_type_table']
+ count = self.get_count_from_data_buffer(bytearray(data))
+
+ seq_no = 0
+ data_buffer = list()
+ self.log.debug('me-type-count', octets=count, data=hexlify(data))
+
+ # Start the loop
+ for offset in xrange(0, count, self._pdu_size):
+ frame = OmciFrame(message_type_table=seq_no).get_next()
+ seq_no += 1
+ self.strobe_watchdog()
+ results = yield self._device.omci_cc.send(frame)
+
+ omci_msg = results.fields['omci_message']
+ status = omci_msg.fields['success_code']
+
+ if status != ReasonCodes.Success.value:
+ raise GetCapabilitiesFailure(
+ 'Get supported msg types request at offset {} of {} failed with status code: {}'.
+ format(offset + 1, count, status))
+
+ # Extract the data
+ num_octets = count - offset
+ if num_octets > self._pdu_size:
+ num_octets = self._pdu_size
+
+ data = omci_msg.fields['data']['message_type_table']
+ data_buffer += data[:num_octets]
+
+ def buffer_to_message_type(value):
+ """
+ Convert an integer value to the appropriate EntityOperations enumeration
+ :param value: (int) Message type value (4..29)
+ :return: (EntityOperations) Enumeration, None on failure
+ """
+ next((v for k, v in EntityOperations.__members__.items() if v.value == value), None)
+
+ msg_types = {buffer_to_message_type(v) for v in data_buffer if v is not None}
+ returnValue({msg_type for msg_type in msg_types if msg_type is not None})
+
+ except Exception as e:
+ self.log.exception('get-msg-types', e=e)
+ self.deferred.errback(failure.Failure(e))
diff --git a/python/adapters/extensions/omci/tasks/reboot_task.py b/python/adapters/extensions/omci/tasks/reboot_task.py
new file mode 100644
index 0000000..316e23b
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/reboot_task.py
@@ -0,0 +1,125 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from enum import IntEnum
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, failure, TimeoutError
+from voltha.extensions.omci.omci_defs import ReasonCodes, EntityOperations
+from voltha.extensions.omci.omci_me import OntGFrame
+from voltha.extensions.omci.omci_cc import DEFAULT_OMCI_TIMEOUT
+
+RC = ReasonCodes
+OP = EntityOperations
+
+
+class RebootException(Exception):
+ pass
+
+
+class DeviceBusy(Exception):
+ pass
+
+
+class RebootFlags(IntEnum):
+ Reboot_Unconditionally = 0,
+ Reboot_If_No_POTS_VoIP_In_Progress = 1,
+ Reboot_If_No_Emergency_Call_In_Progress = 2
+
+
+class OmciRebootRequest(Task):
+ """
+ OpenOMCI routine to request reboot of an ONU
+ """
+ task_priority = Task.MAX_PRIORITY
+ name = "ONU OMCI Reboot Task"
+ # adopt the global default
+ DEFAULT_REBOOT_TIMEOUT = DEFAULT_OMCI_TIMEOUT
+
+ def __init__(self, omci_agent, device_id,
+ flags=RebootFlags.Reboot_Unconditionally,
+ timeout=DEFAULT_REBOOT_TIMEOUT):
+ """
+ Class initialization
+
+ :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+ :param flags: (RebootFlags) Reboot condition
+ """
+ super(OmciRebootRequest, self).__init__(OmciRebootRequest.name,
+ omci_agent,
+ device_id,
+ priority=OmciRebootRequest.task_priority,
+ exclusive=True)
+ self._device = omci_agent.get_device(device_id)
+ self._flags = flags
+ self._timeout = timeout
+ self._local_deferred = None
+
+ def cancel_deferred(self):
+ super(OmciRebootRequest, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def start(self):
+ """ Start task """
+ super(OmciRebootRequest, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_reboot)
+
+ @inlineCallbacks
+ def perform_reboot(self):
+ """
+ Perform the reboot requests
+
+ Depending on the ONU implementation, a response may not be returned. For this
+ reason, a timeout is considered successful.
+ """
+ self.log.info('perform-reboot')
+
+ try:
+ frame = OntGFrame().reboot(reboot_code=self._flags)
+ self.strobe_watchdog()
+ results = yield self._device.omci_cc.send(frame, timeout=self._timeout)
+
+ status = results.fields['omci_message'].fields['success_code']
+ self.log.debug('reboot-status', status=status)
+
+ # Did it fail
+ if status != RC.Success.value:
+ if self._flags != RebootFlags.Reboot_Unconditionally and\
+ status == RC.DeviceBusy.value:
+ raise DeviceBusy('ONU is busy, try again later')
+ else:
+ msg = 'Reboot request failed with status {}'.format(status)
+ raise RebootException(msg)
+
+ self.log.info('reboot-success')
+ self.deferred.callback(self)
+
+ except TimeoutError:
+ self.log.info('timeout', msg='Request timeout is not considered an error')
+ self.deferred.callback(None)
+
+ except DeviceBusy as e:
+ self.log.warn('perform-reboot', msg=e)
+ self.deferred.errback(failure.Failure(e))
+
+ except Exception as e:
+ self.log.exception('perform-reboot', e=e)
+ self.deferred.errback(failure.Failure(e))
diff --git a/python/adapters/extensions/omci/tasks/sync_time_task.py b/python/adapters/extensions/omci/tasks/sync_time_task.py
new file mode 100644
index 0000000..b5b1dc9
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/sync_time_task.py
@@ -0,0 +1,107 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from task import Task
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, TimeoutError, failure
+from voltha.extensions.omci.omci_me import OntGFrame
+from voltha.extensions.omci.omci_defs import ReasonCodes as RC
+from datetime import datetime
+
+
+class SyncTimeTask(Task):
+ """
+ OpenOMCI - Synchronize the ONU time with server
+ """
+ task_priority = Task.DEFAULT_PRIORITY + 10
+ name = "Sync Time Task"
+
+ def __init__(self, omci_agent, device_id, use_utc=True):
+ """
+ Class initialization
+
+ :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+ :param device_id: (str) ONU Device ID
+ :param use_utc: (bool) Use UTC time if True, otherwise local time
+ """
+ super(SyncTimeTask, self).__init__(SyncTimeTask.name,
+ omci_agent,
+ device_id,
+ priority=SyncTimeTask.task_priority,
+ exclusive=False)
+ self._local_deferred = None
+ self._use_utc = use_utc
+
+ def cancel_deferred(self):
+ super(SyncTimeTask, self).cancel_deferred()
+
+ d, self._local_deferred = self._local_deferred, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def start(self):
+ """
+ Start the tasks
+ """
+ super(SyncTimeTask, self).start()
+ self._local_deferred = reactor.callLater(0, self.perform_sync_time)
+
+ def stop(self):
+ """
+ Shutdown the tasks
+ """
+ self.log.debug('stopping')
+
+ self.cancel_deferred()
+ super(SyncTimeTask, self).stop()
+
+ @inlineCallbacks
+ def perform_sync_time(self):
+ """
+ Sync the time
+ """
+ self.log.debug('perform-sync-time')
+
+ try:
+ device = self.omci_agent.get_device(self.device_id)
+
+ #########################################
+ # ONT-G (ME #256)
+ dt = datetime.utcnow() if self._use_utc else datetime.now()
+
+ results = yield device.omci_cc.send(OntGFrame().synchronize_time(dt))
+
+ omci_msg = results.fields['omci_message'].fields
+ status = omci_msg['success_code']
+ self.log.debug('sync-time', status=status)
+
+ if status == RC.Success:
+ self.log.info('sync-time', success_info=omci_msg['success_info'] & 0x0f)
+
+ assert status == RC.Success, 'Unexpected Response Status: {}'.format(status)
+
+ # Successful if here
+ self.deferred.callback(results)
+
+ except TimeoutError as e:
+ self.log.warn('sync-time-timeout', e=e)
+ self.deferred.errback(failure.Failure(e))
+
+ except Exception as e:
+ self.log.exception('sync-time', e=e)
+ self.deferred.errback(failure.Failure(e))
diff --git a/python/adapters/extensions/omci/tasks/task.py b/python/adapters/extensions/omci/tasks/task.py
new file mode 100644
index 0000000..36020c0
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/task.py
@@ -0,0 +1,188 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import structlog
+from twisted.internet import defer, reactor
+from twisted.internet.defer import failure
+
+
+class WatchdogTimeoutFailure(Exception):
+ """Task callback/errback not called properly before watchdog expiration"""
+ pass
+
+
+class Task(object):
+ """
+ OpenOMCI Base Task implementation
+
+ An OMCI task can be one or more OMCI requests, comparisons, or whatever
+ is needed to do a specific unit of work that needs to be ran to completion
+ successfully.
+
+ On successful completion, the task should called the 'callback' method of
+ the deferred and pass back whatever is meaningful to the user/state-machine
+ that launched it.
+
+ On failure, the 'errback' routine should be called with an appropriate
+ Failure object.
+ """
+ DEFAULT_PRIORITY = 128
+ MIN_PRIORITY = 0
+ MAX_PRIORITY = 255
+ DEFAULT_WATCHDOG_SECS = 10 # 10 seconds
+ MIN_WATCHDOG_SECS = 3 # 3 seconds
+ MAX_WATCHDOG_SECS = 60 # 60 seconds
+
+ _next_task_id = 0
+
+ def __init__(self, name, omci_agent, device_id, priority=DEFAULT_PRIORITY,
+ exclusive=True, watchdog_timeout=DEFAULT_WATCHDOG_SECS):
+ """
+ Class initialization
+
+ :param name: (str) Task Name
+ :param device_id: (str) ONU Device ID
+ :param priority: (int) Task priority (0..255) 255 Highest
+ :param exclusive: (bool) If True, this task needs exclusive access to the
+ OMCI Communications channel when it runs
+ :param watchdog_timeout (int or float) Watchdog timeout (seconds) after task start, to
+ run longer, periodically call 'strobe_watchdog()' to reschedule.
+ """
+ assert Task.MIN_PRIORITY <= priority <= Task.MAX_PRIORITY, \
+ 'Priority should be {}..{}'.format(Task.MIN_PRIORITY, Task.MAX_PRIORITY)
+
+ assert Task.MIN_WATCHDOG_SECS <= watchdog_timeout <= Task.MAX_WATCHDOG_SECS, \
+ 'Watchdog timeout should be {}..{} seconds'
+
+ Task._next_task_id += 1
+ self._task_id = Task._next_task_id
+ self.log = structlog.get_logger(device_id=device_id, name=name,
+ task_id=self._task_id)
+ self.name = name
+ self.device_id = device_id
+ self.omci_agent = omci_agent
+ self._running = False
+ self._exclusive = exclusive
+ self._deferred = defer.Deferred() # Fires upon completion
+ self._watchdog = None
+ self._watchdog_timeout = watchdog_timeout
+ self._priority = priority
+
+ def __str__(self):
+ return 'Task: {}, ID:{}, Priority: {}, Exclusive: {}, Watchdog: {}'.format(
+ self.name, self.task_id, self.priority, self.exclusive, self.watchdog_timeout)
+
+ @property
+ def priority(self):
+ return self._priority
+
+ @property
+ def task_id(self):
+ return self._task_id
+
+ @property
+ def exclusive(self):
+ return self._exclusive
+
+ @property
+ def watchdog_timeout(self):
+ return self._watchdog_timeout
+
+ @property
+ def deferred(self):
+ return self._deferred
+
+ @property
+ def running(self):
+ # Is the Task running?
+ #
+ # Can be useful for tasks that use inline callbacks to detect
+ # if the task has been canceled.
+ #
+ return self._running
+
+ def cancel_deferred(self):
+ d1, self._deferred = self._deferred, None
+ d2, self._watchdog = self._watchdog, None
+
+ for d in [d1, d2]:
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def start(self):
+ """
+ Start task operations
+ """
+ self.log.debug('starting')
+ assert self._deferred is not None and not self._deferred.called, \
+ 'Cannot re-use the same task'
+ self._running = True
+ self.strobe_watchdog()
+
+ def stop(self):
+ """
+ Stop task synchronization
+ """
+ self.log.debug('stopping')
+ self._running = False
+ self.cancel_deferred()
+ self.omci_agent = None # Should only start/stop once
+
+ def task_cleanup(self):
+ """
+ This method should only be called from the TaskRunner's callback/errback
+ that is added when the task is initially queued. It is responsible for
+ clearing of the 'running' flag and canceling of the watchdog time
+ """
+ self._running = False
+ d, self._watchdog = self._watchdog, None
+ try:
+ if d is not None and not d.called:
+ d.cancel()
+ except:
+ pass
+
+ def strobe_watchdog(self):
+ """
+ Signal that we have not hung/deadlocked
+ """
+ # Create if first time (called at Task start)
+
+ def watchdog_timeout():
+ # Task may have hung (blocked) or failed to call proper success/error
+ # completion callback/errback
+ if not self.deferred.called:
+ err_msg = 'Task {}:{} watchdog timeout'.format(self.name, self.task_id)
+ self.log.error("task-watchdog-timeout", running=self.running,
+ timeout=self.watchdog_timeout, error=err_msg)
+
+ self.deferred.errback(failure.Failure(WatchdogTimeoutFailure(err_msg)))
+ self.deferred.cancel()
+
+ if self._watchdog is not None:
+ if self._watchdog.called:
+ # Too late, timeout failure in progress
+ self.log.warn('task-watchdog-tripped', running=self.running,
+ timeout=self.watchdog_timeout)
+ return
+
+ d, self._watchdog = self._watchdog, None
+ d.cancel()
+
+ # Schedule/re-schedule the watchdog timer
+ self._watchdog = reactor.callLater(self.watchdog_timeout, watchdog_timeout)
diff --git a/python/adapters/extensions/omci/tasks/task_runner.py b/python/adapters/extensions/omci/tasks/task_runner.py
new file mode 100644
index 0000000..eb7a252
--- /dev/null
+++ b/python/adapters/extensions/omci/tasks/task_runner.py
@@ -0,0 +1,285 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import structlog
+from twisted.internet import reactor
+
+
+class TaskRunner(object):
+ """
+ Control the number of running tasks utilizing the OMCI Communications
+ channel (OMCI_CC
+ """
+ def __init__(self, device_id, clock=None):
+ self.log = structlog.get_logger(device_id=device_id)
+ self._pending_queue = dict() # task-priority -> [tasks]
+ self._running_queue = dict() # task-id -> task
+ self._active = False
+
+ self._successful_tasks = 0
+ self._failed_tasks = 0
+ self._watchdog_timeouts = 0
+ self._last_watchdog_failure_task = ''
+ self.reactor = clock if clock is not None else reactor
+
+ def __str__(self):
+ return 'TaskRunner: Pending: {}, Running:{}'.format(self.pending_tasks,
+ self.running_tasks)
+
+ @property
+ def active(self):
+ return self._active
+
+ @property
+ def pending_tasks(self):
+ """
+ Get the number of tasks pending to run
+ """
+ count = 0
+ for tasks in self._pending_queue.itervalues():
+ count += len(tasks)
+ return count
+
+ @property
+ def running_tasks(self):
+ """
+ Get the number of tasks currently running
+ """
+ return len(self._running_queue)
+
+ @property
+ def successful_tasks_completed(self):
+ return self._successful_tasks
+
+ @property
+ def failed_tasks(self):
+ return self._failed_tasks
+
+ @property
+ def watchdog_timeouts(self):
+ return self._watchdog_timeouts
+
+ @property
+ def last_watchdog_failure_task(self):
+ """ Task name of last tasks to fail due to watchdog"""
+ return self._last_watchdog_failure_task
+
+ # TODO: add properties for various stats as needed
+
+ def start(self):
+ """
+ Start the Task runner
+ """
+ self.log.debug('starting', active=self._active)
+
+ if not self._active:
+ assert len(self._running_queue) == 0, 'Running task queue not empty'
+ self._active = True
+ self._run_next_task()
+
+ def stop(self):
+ """
+ Stop the Task runner, first stopping any tasks and flushing the queue
+ """
+ self.log.debug('stopping', active=self._active)
+
+ if self._active:
+ self._active = False
+
+ pq, self._pending_queue = self._pending_queue, dict()
+ rq, self._running_queue = self._running_queue, dict()
+
+ # Stop running tasks
+ for task in rq.itervalues():
+ try:
+ task.stop()
+ except:
+ pass
+
+ # Kill pending tasks
+ for d in pq.iterkeys():
+ try:
+ d.cancel()
+ except:
+ pass
+
+ def _run_next_task(self):
+ """
+ Search for next task to run, if one can
+ :return:
+ """
+ self.log.debug('run-next', active=self._active,
+ num_running=len(self._running_queue),
+ num_pending=len(self._pending_queue))
+
+ if self._active and len(self._pending_queue) > 0:
+ # Cannot run a new task if a running one needs the OMCI_CC exclusively
+
+ if any(task.exclusive for task in self._running_queue.itervalues()):
+ self.log.debug('exclusive-running')
+ return # An exclusive task is already running
+
+ try:
+ priorities = [k for k in self._pending_queue.iterkeys()]
+ priorities.sort(reverse=True)
+ highest_priority = priorities[0] if len(priorities) else None
+
+ if highest_priority is not None:
+ queue = self._pending_queue[highest_priority]
+ next_task = queue[0] if len(queue) else None
+
+ if next_task is not None:
+ if next_task.exclusive and len(self._running_queue) > 0:
+ self.log.debug('next-is-exclusive', task=str(next_task))
+ return # Next task to run needs exclusive access
+
+ queue.pop(0)
+ if len(queue) == 0:
+ del self._pending_queue[highest_priority]
+
+ self.log.debug('starting-task', task=str(next_task),
+ running=len(self._running_queue),
+ pending=len(self._pending_queue))
+
+ self._running_queue[next_task.task_id] = next_task
+ self.reactor.callLater(0, next_task.start)
+
+ # Run again if others are waiting
+ if len(self._pending_queue):
+ self._run_next_task()
+
+ except Exception as e:
+ self.log.exception('run-next', e=e)
+
+ def _on_task_success(self, results, task):
+ """
+ A task completed successfully callback
+ :param results: deferred results
+ :param task: (Task) The task that succeeded
+ :return: deferred results
+ """
+ self.log.debug('task-success', task_id=str(task),
+ running=len(self._running_queue),
+ pending=len(self._pending_queue))
+ try:
+ assert task is not None and task.task_id in self._running_queue,\
+ 'Task not found in running queue'
+
+ task.task_cleanup()
+ self._successful_tasks += 1
+ del self._running_queue[task.task_id]
+
+ except Exception as e:
+ self.log.exception('task-error', task=str(task), e=e)
+
+ finally:
+ reactor.callLater(0, self._run_next_task)
+
+ return results
+
+ def _on_task_failure(self, failure, task):
+ """
+ A task completed with failure callback
+ :param failure: (Failure) Failure results
+ :param task: (Task) The task that failed
+ :return: (Failure) Failure results
+ """
+ from voltha.extensions.omci.tasks.task import WatchdogTimeoutFailure
+
+ self.log.debug('task-failure', task_id=str(task),
+ running=len(self._running_queue),
+ pending=len(self._pending_queue))
+ try:
+ assert task is not None and task.task_id in self._running_queue,\
+ 'Task not found in running queue'
+
+ task.task_cleanup()
+ self._failed_tasks += 1
+ del self._running_queue[task.task_id]
+
+ if isinstance(failure.value, WatchdogTimeoutFailure):
+ self._watchdog_timeouts += 1
+ self._last_watchdog_failure_task = task.name
+
+ except Exception as e:
+ # Check the pending queue
+
+ for priority, tasks in self._pending_queue.iteritems():
+ found = next((t for t in tasks if t.task_id == task.task_id), None)
+
+ if found is not None:
+ self._pending_queue[task.priority].remove(task)
+ if len(self._pending_queue[task.priority]) == 0:
+ del self._pending_queue[task.priority]
+ return failure
+
+ self.log.exception('task-error', task=str(task), e=e)
+ raise
+
+ finally:
+ reactor.callLater(0, self._run_next_task)
+
+ return failure
+
+ def queue_task(self, task):
+ """
+ Place a task on the queue to run
+
+ :param task: (Task) task to run
+ :return: (deferred) Deferred that will fire on task completion
+ """
+ self.log.debug('queue-task', active=self._active, task=str(task),
+ running=len(self._running_queue),
+ pending=len(self._pending_queue))
+
+ if task.priority not in self._pending_queue:
+ self._pending_queue[task.priority] = []
+
+ task.deferred.addCallbacks(self._on_task_success, self._on_task_failure,
+ callbackArgs=[task], errbackArgs=[task])
+
+ self._pending_queue[task.priority].append(task)
+ self._run_next_task()
+
+ return task.deferred
+
+ def cancel_task(self, task_id):
+ """
+ Cancel a pending or running task. The cancel method will be called
+ for the task's deferred
+
+ :param task_id: (int) Task identifier
+ """
+ task = self._running_queue.get(task_id, None)
+
+ if task is not None:
+ try:
+ task.stop()
+ except Exception as e:
+ self.log.exception('stop-error', task=str(task), e=e)
+
+ reactor.callLater(0, self._run_next_task)
+
+ else:
+ for priority, tasks in self._pending_queue.iteritems():
+ task = next((t for t in tasks if t.task_id == task_id), None)
+
+ if task is not None:
+ try:
+ task.deferred.cancel()
+ except Exception as e:
+ self.log.exception('cancel-error', task=str(task), e=e)
+ return
+
diff --git a/python/adapters/iadapter.py b/python/adapters/iadapter.py
new file mode 100644
index 0000000..abe1bbc
--- /dev/null
+++ b/python/adapters/iadapter.py
@@ -0,0 +1,358 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Adapter abstract base class
+"""
+
+import structlog
+from twisted.internet import reactor
+from zope.interface import implementer
+
+from interface import IAdapterInterface
+from python.protos.adapter_pb2 import Adapter
+from python.protos.adapter_pb2 import AdapterConfig
+from python.protos.common_pb2 import AdminState
+from python.protos.common_pb2 import LogLevel
+from python.protos.device_pb2 import DeviceType, DeviceTypes
+from python.protos.health_pb2 import HealthStatus
+
+
+log = structlog.get_logger()
+
+
+@implementer(IAdapterInterface)
+class IAdapter(object):
+ def __init__(self,
+ core_proxy,
+ adapter_proxy,
+ config,
+ device_handler_class,
+ name,
+ vendor,
+ version,
+ device_type, vendor_id,
+ accepts_bulk_flow_update=True,
+ accepts_add_remove_flow_updates=False):
+ log.debug(
+ 'Initializing adapter: {} {} {}'.format(vendor, name, version))
+ self.core_proxy = core_proxy
+ self.adapter_proxy = adapter_proxy
+ self.config = config
+ self.name = name
+ self.supported_device_types = [
+ DeviceType(
+ id=device_type,
+ vendor_id=vendor_id,
+ adapter=name,
+ accepts_bulk_flow_update=accepts_bulk_flow_update,
+ accepts_add_remove_flow_updates=accepts_add_remove_flow_updates
+ )
+ ]
+ self.descriptor = Adapter(
+ id=self.name,
+ vendor=vendor,
+ version=version,
+ config=AdapterConfig(log_level=LogLevel.INFO)
+ )
+ self.devices_handlers = dict() # device_id -> Olt/OnuHandler()
+ self.device_handler_class = device_handler_class
+
+ def start(self):
+ log.info('Starting adapter: {}'.format(self.name))
+
+ def stop(self):
+ log.info('Stopping adapter: {}'.format(self.name))
+
+ def adapter_descriptor(self):
+ return self.descriptor
+
+ def device_types(self):
+ return DeviceTypes(items=self.supported_device_types)
+
+ def health(self):
+ # return HealthStatus(state=HealthStatus.HealthState.HEALTHY)
+ return HealthStatus(state=HealthStatus.HEALTHY)
+
+ def change_master_state(self, master):
+ raise NotImplementedError()
+
+ def get_ofp_device_info(self, device):
+ log.debug('get_ofp_device_info_start', device_id=device.id)
+ ofp_device_info = self.devices_handlers[device.id].get_ofp_device_info(
+ device)
+ log.debug('get_ofp_device_info_ends', device_id=device.id)
+ return ofp_device_info
+
+ def get_ofp_port_info(self, device, port_no):
+ log.debug('get_ofp_port_info_start', device_id=device.id,
+ port_no=port_no)
+ ofp_port_info = self.devices_handlers[device.id].get_ofp_port_info(
+ device, port_no)
+ log.debug('get_ofp_port_info_ends', device_id=device.id,
+ port_no=port_no)
+ return ofp_port_info
+
+ def adopt_device(self, device):
+ log.debug('adopt_device', device_id=device.id)
+ self.devices_handlers[device.id] = self.device_handler_class(self,
+ device.id)
+ reactor.callLater(0, self.devices_handlers[device.id].activate, device)
+ log.debug('adopt_device_done', device_id=device.id)
+ return device
+
+ def reconcile_device(self, device):
+ raise NotImplementedError()
+
+ def abandon_device(self, device):
+ raise NotImplementedError()
+
+ def disable_device(self, device):
+ log.info('disable-device', device_id=device.id)
+ reactor.callLater(0, self.devices_handlers[device.id].disable)
+ log.debug('disable-device-done', device_id=device.id)
+ return device
+
+ def reenable_device(self, device):
+ log.info('reenable-device', device_id=device.id)
+ reactor.callLater(0, self.devices_handlers[device.id].reenable)
+ log.info('reenable-device-done', device_id=device.id)
+ return device
+
+ def reboot_device(self, device):
+ log.info('reboot-device', device_id=device.id)
+ reactor.callLater(0, self.devices_handlers[device.id].reboot)
+ log.info('reboot-device-done', device_id=device.id)
+ return device
+
+ def download_image(self, device, request):
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ raise NotImplementedError()
+
+ def self_test_device(self, device):
+ log.info('self-test', device_id=device.id)
+ result = reactor.callLater(0, self.devices_handlers[
+ device.id].self_test_device)
+ log.info('self-test-done', device_id=device.id)
+ return result
+
+ def delete_device(self, device):
+ log.info('delete-device', device_id=device.id)
+ reactor.callLater(0, self.devices_handlers[device.id].delete)
+ log.info('delete-device-done', device_id=device.id)
+ return device
+
+ def get_device_details(self, device):
+ raise NotImplementedError()
+
+ def update_flows_bulk(self, device, flows, groups):
+ log.info('bulk-flow-update', device_id=device.id,
+ flows=flows, groups=groups)
+ assert len(groups.items) == 0
+ reactor.callLater(0, self.devices_handlers[device.id].update_flow_table,
+ flows.items)
+ return device
+
+ def update_flows_incrementally(self, device, flow_changes, group_changes):
+ log.info('incremental-flow-update', device_id=device.id,
+ flows=flow_changes, groups=group_changes)
+ # For now, there is no support for group changes
+ assert len(group_changes.to_add.items) == 0
+ assert len(group_changes.to_remove.items) == 0
+
+ handler = self.devices_handlers[device.id]
+ # Remove flows
+ if len(flow_changes.to_remove.items) != 0:
+ reactor.callLater(0, handler.remove_from_flow_table,
+ flow_changes.to_remove.items)
+
+ # Add flows
+ if len(flow_changes.to_add.items) != 0:
+ reactor.callLater(0, handler.add_to_flow_table,
+ flow_changes.to_add.items)
+ return device
+
+ def update_pm_config(self, device, pm_config):
+ log.info("adapter-update-pm-config", device=device,
+ pm_config=pm_config)
+ handler = self.devices_handlers[device.id]
+ if handler:
+ reactor.callLater(0, handler.update_pm_config, device, pm_config)
+
+ def process_inter_adapter_message(self, msg):
+ raise NotImplementedError()
+
+ def receive_packet_out(self, device_id, egress_port_no, msg):
+ raise NotImplementedError()
+
+ def suppress_alarm(self, filter):
+ raise NotImplementedError()
+
+ def unsuppress_alarm(self, filter):
+ raise NotImplementedError()
+
+ def _get_handler(self, device):
+ if device.id in self.devices_handlers:
+ handler = self.devices_handlers[device.id]
+ if handler is not None:
+ return handler
+ return None
+
+
+"""
+OLT Adapter base class
+"""
+
+
+class OltAdapter(IAdapter):
+ def __init__(self,
+ core_proxy,
+ adapter_proxy,
+ config,
+ device_handler_class,
+ name,
+ vendor,
+ version, device_type,
+ accepts_bulk_flow_update=True,
+ accepts_add_remove_flow_updates=False):
+ super(OltAdapter, self).__init__(core_proxy=core_proxy,
+ adapter_proxy=adapter_proxy,
+ config=config,
+ device_handler_class=device_handler_class,
+ name=name,
+ vendor=vendor,
+ version=version,
+ device_type=device_type,
+ vendor_id=None,
+ accepts_bulk_flow_update=accepts_bulk_flow_update,
+ accepts_add_remove_flow_updates=accepts_add_remove_flow_updates)
+ self.logical_device_id_to_root_device_id = dict()
+
+ def reconcile_device(self, device):
+ try:
+ self.devices_handlers[device.id] = self.device_handler_class(self,
+ device.id)
+ # Work only required for devices that are in ENABLED state
+ if device.admin_state == AdminState.ENABLED:
+ reactor.callLater(0,
+ self.devices_handlers[device.id].reconcile,
+ device)
+ else:
+ # Invoke the children reconciliation which would setup the
+ # basic children data structures
+ self.core_proxy.reconcile_child_devices(device.id)
+ return device
+ except Exception, e:
+ log.exception('Exception', e=e)
+
+ def send_proxied_message(self, proxy_address, msg):
+ log.info('send-proxied-message', proxy_address=proxy_address, msg=msg)
+ handler = self.devices_handlers[proxy_address.device_id]
+ handler.send_proxied_message(proxy_address, msg)
+
+ def process_inter_adapter_message(self, msg):
+ log.debug('process-inter-adapter-message', msg=msg)
+ # Unpack the header to know which device needs to handle this message
+ handler = None
+ if msg.header.proxy_device_id:
+ # typical request
+ handler = self.devices_handlers[msg.header.proxy_device_id]
+ elif msg.header.to_device_id and \
+ msg.header.to_device_id in self.devices_handlers:
+ # typical response
+ handler = self.devices_handlers[msg.header.to_device_id]
+ if handler:
+ reactor.callLater(0, handler.process_inter_adapter_message, msg)
+
+ def receive_packet_out(self, device_id, egress_port_no, msg):
+ try:
+ log.info('receive_packet_out', device_id=device_id,
+ egress_port=egress_port_no, msg=msg)
+ handler = self.devices_handlers[device_id]
+ if handler:
+ reactor.callLater(0, handler.packet_out, egress_port_no, msg.data)
+ except Exception, e:
+ log.exception('packet-out-failure', e=e)
+
+
+"""
+ONU Adapter base class
+"""
+
+
+class OnuAdapter(IAdapter):
+ def __init__(self,
+ core_proxy,
+ adapter_proxy,
+ config,
+ device_handler_class,
+ name,
+ vendor,
+ version,
+ device_type,
+ vendor_id,
+ accepts_bulk_flow_update=True,
+ accepts_add_remove_flow_updates=False):
+ super(OnuAdapter, self).__init__(core_proxy=core_proxy,
+ adapter_proxy=adapter_proxy,
+ config=config,
+ device_handler_class=device_handler_class,
+ name=name,
+ vendor=vendor,
+ version=version,
+ device_type=device_type,
+ vendor_id=vendor_id,
+ accepts_bulk_flow_update=accepts_bulk_flow_update,
+ accepts_add_remove_flow_updates=accepts_add_remove_flow_updates)
+
+ def reconcile_device(self, device):
+ self.devices_handlers[device.id] = self.device_handler_class(self,
+ device.id)
+ # Reconcile only if state was ENABLED
+ if device.admin_state == AdminState.ENABLED:
+ reactor.callLater(0,
+ self.devices_handlers[device.id].reconcile,
+ device)
+ return device
+
+ def receive_proxied_message(self, proxy_address, msg):
+ log.info('receive-proxied-message', proxy_address=proxy_address,
+ device_id=proxy_address.device_id, msg=msg)
+ # Device_id from the proxy_address is the olt device id. We need to
+ # get the onu device id using the port number in the proxy_address
+ device = self.core_proxy. \
+ get_child_device_with_proxy_address(proxy_address)
+ if device:
+ handler = self.devices_handlers[device.id]
+ handler.receive_message(msg)
+
+ def process_inter_adapter_message(self, msg):
+ log.info('process-inter-adapter-message', msg=msg)
+ # Unpack the header to know which device needs to handle this message
+ if msg.header:
+ handler = self.devices_handlers[msg.header.to_device_id]
+ handler.process_inter_adapter_message(msg)
diff --git a/python/adapters/interface.py b/python/adapters/interface.py
new file mode 100644
index 0000000..b0390d8
--- /dev/null
+++ b/python/adapters/interface.py
@@ -0,0 +1,459 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Interface definition for Voltha Adapters
+"""
+from zope.interface import Interface
+
+
+class IAdapterInterface(Interface):
+ """
+ A Voltha adapter. This interface is used by the Voltha Core to initiate
+ requests towards a voltha adapter.
+ """
+
+ def adapter_descriptor():
+ """
+ Return the adapter descriptor object for this adapter.
+ :return: voltha.Adapter grpc object (see voltha/protos/adapter.proto),
+ with adapter-specific information and config extensions.
+ """
+
+ def device_types():
+ """
+ Return list of device types supported by the adapter.
+ :return: voltha.DeviceTypes protobuf object, with optional type
+ specific extensions.
+ """
+
+ def health():
+ """
+ Return a 3-state health status using the voltha.HealthStatus message.
+ :return: Deferred or direct return with voltha.HealthStatus message
+ """
+
+ def adopt_device(device):
+ """
+ Make sure the adapter looks after given device. Called when a device
+ is provisioned top-down and needs to be activated by the adapter.
+ :param device: A voltha.Device object, with possible device-type
+ specific extensions. Such extensions shall be described as part of
+ the device type specification returned by device_types().
+ :return: (Deferred) Shall be fired to acknowledge device ownership.
+ """
+
+ def reconcile_device(device):
+ """
+ Make sure the adapter looks after given device. Called when this
+ device has changed ownership from another Voltha instance to
+ this one (typically, this occurs when the previous voltha
+ instance went down).
+ :param device: A voltha.Device object, with possible device-type
+ specific extensions. Such extensions shall be described as part of
+ the device type specification returned by device_types().
+ :return: (Deferred) Shall be fired to acknowledge device ownership.
+ """
+
+ def abandon_device(device):
+ """
+ Make sur ethe adapter no longer looks after device. This is called
+ if device ownership is taken over by another Voltha instance.
+ :param device: A Voltha.Device object.
+ :return: (Deferred) Shall be fired to acknowledge abandonment.
+ """
+
+ def disable_device(device):
+ """
+ This is called when a previously enabled device needs to be disabled
+ based on a NBI call.
+ :param device: A Voltha.Device object.
+ :return: (Deferred) Shall be fired to acknowledge disabling the device.
+ """
+
+ def reenable_device(device):
+ """
+ This is called when a previously disabled device needs to be enabled
+ based on a NBI call.
+ :param device: A Voltha.Device object.
+ :return: (Deferred) Shall be fired to acknowledge re-enabling the
+ device.
+ """
+
+ def reboot_device(device):
+ """
+ This is called to reboot a device based on a NBI call. The admin
+ state of the device will not change after the reboot
+ :param device: A Voltha.Device object.
+ :return: (Deferred) Shall be fired to acknowledge the reboot.
+ """
+
+ def download_image(device, request):
+ """
+ This is called to request downloading a specified image into
+ the standby partition of a device based on a NBI call.
+ This call is expected to be non-blocking.
+ :param device: A Voltha.Device object.
+ A Voltha.ImageDownload object.
+ :return: (Deferred) Shall be fired to acknowledge the download.
+ """
+
+ def get_image_download_status(device, request):
+ """
+ This is called to inquire about a requested image download
+ status based on a NBI call.
+ The adapter is expected to update the DownloadImage DB object
+ with the query result
+ :param device: A Voltha.Device object.
+ A Voltha.ImageDownload object.
+ :return: (Deferred) Shall be fired to acknowledge
+ """
+
+ def cancel_image_download(device, request):
+ """
+ This is called to cancel a requested image download
+ based on a NBI call. The admin state of the device will not
+ change after the download.
+ :param device: A Voltha.Device object.
+ A Voltha.ImageDownload object.
+ :return: (Deferred) Shall be fired to acknowledge
+ """
+
+ def activate_image_update(device, request):
+ """
+ This is called to activate a downloaded image from
+ a standby partition into active partition.
+ Depending on the device implementation, this call
+ may or may not cause device reboot.
+ If no reboot, then a reboot is required to make the
+ activated image running on device
+ This call is expected to be non-blocking.
+ :param device: A Voltha.Device object.
+ A Voltha.ImageDownload object.
+ :return: (Deferred) OperationResponse object.
+ """
+
+ def revert_image_update(device, request):
+ """
+ This is called to deactivate the specified image at
+ active partition, and revert to previous image at
+ standby partition.
+ Depending on the device implementation, this call
+ may or may not cause device reboot.
+ If no reboot, then a reboot is required to make the
+ previous image running on device
+ This call is expected to be non-blocking.
+ :param device: A Voltha.Device object.
+ A Voltha.ImageDownload object.
+ :return: (Deferred) OperationResponse object.
+ """
+
+ def self_test_device(device):
+ """
+ This is called to Self a device based on a NBI call.
+ :param device: A Voltha.Device object.
+ :return: Will return result of self test
+ """
+
+ def delete_device(device):
+ """
+ This is called to delete a device from the PON based on a NBI call.
+ If the device is an OLT then the whole PON will be deleted.
+ :param device: A Voltha.Device object.
+ :return: (Deferred) Shall be fired to acknowledge the deletion.
+ """
+
+ def get_device_details(device):
+ """
+ This is called to get additional device details based on a NBI call.
+ :param device: A Voltha.Device object.
+ :return: (Deferred) Shall be fired to acknowledge the retrieval of
+ additional details.
+ """
+
+ def update_flows_bulk(device, flows, groups):
+ """
+ Called after any flow table change, but only if the device supports
+ bulk mode, which is expressed by the 'accepts_bulk_flow_update'
+ capability attribute of the device type.
+ :param device: A Voltha.Device object.
+ :param flows: An openflow_v13.Flows object
+ :param groups: An openflow_v13.Flows object
+ :return: (Deferred or None)
+ """
+
+ def update_flows_incrementally(device, flow_changes, group_changes):
+ """
+ Called after a flow table update, but only if the device supports
+ non-bulk mode, which is expressed by the 'accepts_add_remove_flow_updates'
+ capability attribute of the device type.
+ :param device: A Voltha.Device object.
+ :param flow_changes: An openflow_v13.FlowChanges object
+ :param group_changes: An openflow_v13.FlowGroupChanges object
+ :return: (Deferred or None)
+ """
+
+ def update_pm_config(device, pm_configs):
+ """
+ Called every time a request is made to change pm collection behavior
+ :param device: A Voltha.Device object
+ :param pm_collection_config: A Pms
+ """
+
+ def receive_packet_out(device_id, egress_port_no, msg):
+ """
+ Pass a packet_out message content to adapter so that it can forward
+ it out to the device. This is only called on root devices.
+ :param device_id: device ID
+ :param egress_port: egress logical port number
+ :param msg: actual message
+ :return: None
+ """
+
+ def suppress_alarm(filter):
+ """
+ Inform an adapter that all incoming alarms should be suppressed
+ :param filter: A Voltha.AlarmFilter object.
+ :return: (Deferred) Shall be fired to acknowledge the suppression.
+ """
+
+ def unsuppress_alarm(filter):
+ """
+ Inform an adapter that all incoming alarms should resume
+ :param filter: A Voltha.AlarmFilter object.
+ :return: (Deferred) Shall be fired to acknowledge the unsuppression.
+ """
+
+ def get_ofp_device_info(device):
+ """
+ Retrieve the OLT device info. This includes the ofp_desc and
+ ofp_switch_features. The existing ofp structures can be used,
+ or all the attributes get added to the Device definition or a new proto
+ definition gets created. This API will allow the Core to create a
+ LogicalDevice associated with this device (OLT only).
+ :param device: device
+ :return: Proto Message (TBD)
+ """
+
+ def get_ofp_port_info(device, port_no):
+ """
+ Retrieve the port info. This includes the ofp_port. The existing ofp
+ structure can be used, or all the attributes get added to the Port
+ definitions or a new proto definition gets created. This API will allow
+ the Core to create a LogicalPort associated with this device.
+ :param device: device
+ :param port_no: port number
+ :return: Proto Message (TBD)
+ """
+
+ def process_inter_adapter_message(msg):
+ """
+ Called when the adapter receives a message that was sent to it directly
+ from another adapter. An adapter is automatically registered for these
+ messages when creating the inter-container kafka proxy. Note that it is
+ the responsibility of the sending and receiving adapters to properly encode
+ and decode the message.
+ :param msg: Proto Message (any)
+ :return: Proto Message Response
+ """
+
+
+class ICoreSouthBoundInterface(Interface):
+ """
+ Represents a Voltha Core. This is used by an adapter to initiate async
+ calls towards Voltha Core.
+ """
+
+ def get_device(device_id):
+ """
+ Retrieve a device using its ID.
+ :param device_id: a device ID
+ :return: Device Object or None
+ """
+
+ def get_child_device(parent_device_id, **kwargs):
+ """
+ Retrieve a child device object belonging to the specified parent
+ device based on some match criteria. The first child device that
+ matches the provided criteria is returned.
+ :param parent_device_id: parent's device protobuf ID
+ :param **kwargs: arbitrary list of match criteria where the Value
+ in each key-value pair must be a protobuf type
+ :return: Child Device Object or None
+ """
+
+ def get_ports(device_id, port_type):
+ """
+ Retrieve all the ports of a given type of a Device.
+ :param device_id: a device ID
+ :param port_type: type of port
+ :return Ports object
+ """
+
+ def get_child_devices(parent_device_id):
+ """
+ Get all child devices given a parent device id
+ :param parent_device_id: The parent device ID
+ :return: Devices object
+ """
+
+ def get_child_device_with_proxy_address(proxy_address):
+ """
+ Get a child device based on its proxy address. Proxy address is
+ defined as {parent id, channel_id}
+ :param proxy_address: A Device.ProxyAddress object
+ :return: Device object or None
+ """
+
+ def device_state_update(device_id,
+ oper_status=None,
+ connect_status=None):
+ """
+ Update a device state.
+ :param device_id: The device ID
+ :param oper_state: Operational state of device
+ :param conn_state: Connection state of device
+ :return: None
+ """
+
+ def child_device_detected(parent_device_id,
+ parent_port_no,
+ child_device_type,
+ channel_id,
+ **kw):
+ """
+ A child device has been detected. Core will create the device along
+ with its unique ID.
+ :param parent_device_id: The parent device ID
+ :param parent_port_no: The parent port number
+ :param device_type: The child device type
+ :param channel_id: A unique identifier for that child device within
+ the parent device (e.g. vlan_id)
+ :param kw: A list of key-value pair where the value is a protobuf
+ message
+ :return: None
+ """
+
+ def device_update(device):
+ """
+ Event corresponding to a device update.
+ :param device: Device Object
+ :return: None
+ """
+
+ def child_device_removed(parent_device_id, child_device_id):
+ """
+ Event indicating a child device has been removed from a parent.
+ :param parent_device_id: Device ID of the parent
+ :param child_device_id: Device ID of the child
+ :return: None
+ """
+
+ def child_devices_state_update(parent_device_id,
+ oper_status=None,
+ connect_status=None,
+ admin_status=None):
+ """
+ Event indicating the status of all child devices have been changed.
+ :param parent_device_id: Device ID of the parent
+ :param oper_status: Operational status
+ :param connect_status: Connection status
+ :param admin_status: Admin status
+ :return: None
+ """
+
+ def child_devices_removed(parent_device_id):
+ """
+ Event indicating all child devices have been removed from a parent.
+ :param parent_device_id: Device ID of the parent device
+ :return: None
+ """
+
+ def device_pm_config_update(device_pm_config, init=False):
+ """
+ Event corresponding to a PM config update of a device.
+ :param device_pm_config: a PmConfigs object
+ :param init: True indicates initializing stage
+ :return: None
+ """
+
+ def port_created(device_id, port):
+ """
+ A port has been created and needs to be added to a device.
+ :param device_id: a device ID
+ :param port: Port object
+ :return None
+ """
+
+ def port_removed(device_id, port):
+ """
+ A port has been removed and it needs to be removed from a Device.
+ :param device_id: a device ID
+ :param port: a Port object
+ :return None
+ """
+
+ def ports_enabled(device_id):
+ """
+ All ports on that device have been re-enabled. The Core will change
+ the admin state to ENABLED and operational state to ACTIVE for all
+ ports on that device.
+ :param device_id: a device ID
+ :return: None
+ """
+
+ def ports_disabled(device_id):
+ """
+ All ports on that device have been disabled. The Core will change the
+ admin status to DISABLED and operational state to UNKNOWN for all
+ ports on that device.
+ :param device_id: a device ID
+ :return: None
+ """
+
+ def ports_oper_status_update(device_id, oper_status):
+ """
+ The operational status of all ports of a Device has been changed.
+ The Core will update the operational status for all ports on the
+ device.
+ :param device_id: a device ID
+ :param oper_status: operational Status
+ :return None
+ """
+
+ def image_download_update(img_dnld):
+ """
+ Event corresponding to an image download update.
+ :param img_dnld: a ImageDownload object
+ :return: None
+ """
+
+ def image_download_deleted(img_dnld):
+ """
+ Event corresponding to the deletion of a downloaded image. The
+ references of this image needs to be removed from the Core.
+ :param img_dnld: a ImageDownload object
+ :return: None
+ """
+
+ def packet_in(device_id, egress_port_no, packet):
+ """
+ Sends a packet to the SDN controller via voltha Core
+ :param device_id: The OLT device ID
+ :param egress_port_no: The port number representing the ONU (cvid)
+ :param packet: The actual packet
+ :return: None
+ """
diff --git a/python/adapters/kafka/__init__.py b/python/adapters/kafka/__init__.py
new file mode 100644
index 0000000..58aca1e
--- /dev/null
+++ b/python/adapters/kafka/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/kafka/adapter_proxy.py b/python/adapters/kafka/adapter_proxy.py
new file mode 100644
index 0000000..657a681
--- /dev/null
+++ b/python/adapters/kafka/adapter_proxy.py
@@ -0,0 +1,110 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Agent to play gateway between adapters.
+"""
+
+import structlog
+from uuid import uuid4
+from twisted.internet.defer import inlineCallbacks, returnValue
+from container_proxy import ContainerProxy
+from python.protos import third_party
+from python.protos.inter_container_pb2 import InterAdapterHeader, \
+ InterAdapterMessage
+import time
+
+_ = third_party
+log = structlog.get_logger()
+
+
+class AdapterProxy(ContainerProxy):
+
+ def __init__(self, kafka_proxy, core_topic, my_listening_topic):
+ super(AdapterProxy, self).__init__(kafka_proxy,
+ core_topic,
+ my_listening_topic)
+
+ def _to_string(self, unicode_str):
+ if unicode_str is not None:
+ if type(unicode_str) == unicode:
+ return unicode_str.encode('ascii', 'ignore')
+ else:
+ return unicode_str
+ else:
+ return ""
+
+ @ContainerProxy.wrap_request(None)
+ @inlineCallbacks
+ def send_inter_adapter_message(self,
+ msg,
+ type,
+ from_adapter,
+ to_adapter,
+ to_device_id=None,
+ proxy_device_id=None,
+ message_id=None):
+ """
+ Sends a message directly to an adapter. This is typically used to send
+ proxied messages from one adapter to another. An initial ACK response
+ is sent back to the invoking adapter. If there is subsequent response
+ to be sent back (async) then the adapter receiving this request will
+ use this same API to send back the async response.
+ :param msg : GRPC message to send
+ :param type : InterAdapterMessageType of the message to send
+ :param from_adapter: Name of the adapter making the request.
+ :param to_adapter: Name of the remote adapter.
+ :param to_device_id: The ID of the device for to the message is
+ intended. if it's None then the message is not intended to a specific
+ device. Its interpretation is adapter specific.
+ :param proxy_device_id: The ID of the device which will proxy that
+ message. If it's None then there is no specific device to proxy the
+ message. Its interpretation is adapter specific.
+ :param message_id: A unique number for this transaction that the
+ adapter may use to correlate a request and an async response.
+ """
+
+ try:
+ # validate params
+ assert msg
+ assert from_adapter
+ assert to_adapter
+
+ # Build the inter adapter message
+ h = InterAdapterHeader()
+ h.type = type
+ h.from_topic = self._to_string(from_adapter)
+ h.to_topic = self._to_string(to_adapter)
+ h.to_device_id = self._to_string(to_device_id)
+ h.proxy_device_id = self._to_string(proxy_device_id)
+
+ if message_id:
+ h.id = self._to_string(message_id)
+ else:
+ h.id = uuid4().hex
+
+ h.timestamp = int(round(time.time() * 1000))
+ iaMsg = InterAdapterMessage()
+ iaMsg.header.CopyFrom(h)
+ iaMsg.body.Pack(msg)
+
+ log.debug("sending-inter-adapter-message", header=iaMsg.header)
+ res = yield self.invoke(rpc="process_inter_adapter_message",
+ to_topic=iaMsg.header.to_topic,
+ msg=iaMsg)
+ returnValue(res)
+ except Exception as e:
+ log.exception("error-sending-request", e=e)
diff --git a/python/adapters/kafka/adapter_request_facade.py b/python/adapters/kafka/adapter_request_facade.py
new file mode 100644
index 0000000..fccb049
--- /dev/null
+++ b/python/adapters/kafka/adapter_request_facade.py
@@ -0,0 +1,337 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+This facade handles kafka-formatted messages from the Core, extracts the kafka
+formatting and forwards the request to the concrete handler.
+"""
+import structlog
+from twisted.internet.defer import inlineCallbacks
+from zope.interface import implementer
+from twisted.internet import reactor
+
+from afkak.consumer import OFFSET_LATEST, OFFSET_EARLIEST
+from python.adapters.interface import IAdapterInterface
+from python.protos.inter_container_pb2 import IntType, InterAdapterMessage, StrType, Error, ErrorCode
+from python.protos.device_pb2 import Device, ImageDownload
+from python.protos.openflow_13_pb2 import FlowChanges, FlowGroups, Flows, \
+ FlowGroupChanges, ofp_packet_out
+from python.adapters.kafka.kafka_inter_container_library import IKafkaMessagingProxy, \
+ get_messaging_proxy, KAFKA_OFFSET_LATEST, KAFKA_OFFSET_EARLIEST
+
+log = structlog.get_logger()
+
+class MacAddressError(BaseException):
+ def __init__(self, error):
+ self.error = error
+
+
+class IDError(BaseException):
+ def __init__(self, error):
+ self.error = error
+
+
+@implementer(IAdapterInterface)
+class AdapterRequestFacade(object):
+ """
+ Gate-keeper between CORE and device adapters.
+
+ On one side it interacts with Core's internal model and update/dispatch
+ mechanisms.
+
+ On the other side, it interacts with the adapters standard interface as
+ defined in
+ """
+
+ def __init__(self, adapter):
+ self.adapter = adapter
+
+ @inlineCallbacks
+ def start(self):
+ log.debug('starting')
+
+ @inlineCallbacks
+ def stop(self):
+ log.debug('stopping')
+
+ @inlineCallbacks
+ def createKafkaDeviceTopic(self, deviceId):
+ log.debug("subscribing-to-topic", device_id=deviceId)
+ kafka_proxy = get_messaging_proxy()
+ device_topic = kafka_proxy.get_default_topic() + "_" + deviceId
+ # yield kafka_proxy.create_topic(topic=device_topic)
+ yield kafka_proxy.subscribe(topic=device_topic, group_id=device_topic, target_cls=self, offset=KAFKA_OFFSET_EARLIEST)
+ log.debug("subscribed-to-topic", topic=device_topic)
+
+ def adopt_device(self, device):
+ d = Device()
+ if device:
+ device.Unpack(d)
+
+ # Start the creation of a device specific topic to handle all
+ # subsequent requests from the Core. This adapter instance will
+ # handle all requests for that device.
+ reactor.callLater(0, self.createKafkaDeviceTopic, d.id)
+
+ result = self.adapter.adopt_device(d)
+ # return True, self.adapter.adopt_device(d)
+
+ return True, result
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+
+ def get_ofp_device_info(self, device):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ return True, self.adapter.get_ofp_device_info(d)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+
+ def get_ofp_port_info(self, device, port_no):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+ p = IntType()
+ if port_no:
+ port_no.Unpack(p)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="port-no-invalid")
+
+ return True, self.adapter.get_ofp_port_info(d, p.val)
+
+ def reconcile_device(self, device):
+ return self.adapter.reconcile_device(device)
+
+ def abandon_device(self, device):
+ return self.adapter.abandon_device(device)
+
+ def disable_device(self, device):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ return True, self.adapter.disable_device(d)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+
+ def reenable_device(self, device):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ return True, self.adapter.reenable_device(d)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+
+ def reboot_device(self, device):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ return (True, self.adapter.reboot_device(d))
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+
+ def download_image(self, device, request):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+ img = ImageDownload()
+ if request:
+ request.Unpack(img)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="port-no-invalid")
+
+ return True, self.adapter.download_image(device, request)
+
+ def get_image_download_status(self, device, request):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+ img = ImageDownload()
+ if request:
+ request.Unpack(img)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="port-no-invalid")
+
+ return True, self.adapter.get_image_download_status(device, request)
+
+ def cancel_image_download(self, device, request):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+ img = ImageDownload()
+ if request:
+ request.Unpack(img)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="port-no-invalid")
+
+ return True, self.adapter.cancel_image_download(device, request)
+
+ def activate_image_update(self, device, request):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+ img = ImageDownload()
+ if request:
+ request.Unpack(img)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="port-no-invalid")
+
+ return True, self.adapter.activate_image_update(device, request)
+
+ def revert_image_update(self, device, request):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+ img = ImageDownload()
+ if request:
+ request.Unpack(img)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="port-no-invalid")
+
+ return True, self.adapter.revert_image_update(device, request)
+
+
+ def self_test(self, device):
+ return self.adapter.self_test_device(device)
+
+ def delete_device(self, device):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ result = self.adapter.delete_device(d)
+ # return (True, self.adapter.delete_device(d))
+
+ # Before we return, delete the device specific topic as we will no
+ # longer receive requests from the Core for that device
+ kafka_proxy = get_messaging_proxy()
+ device_topic = kafka_proxy.get_default_topic() + "/" + d.id
+ kafka_proxy.unsubscribe(topic=device_topic)
+
+ return (True, result)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+
+ def get_device_details(self, device):
+ return self.adapter.get_device_details(device)
+
+ def update_flows_bulk(self, device, flows, groups):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+ f = Flows()
+ if flows:
+ flows.Unpack(f)
+
+ g = FlowGroups()
+ if groups:
+ groups.Unpack(g)
+
+ return (True, self.adapter.update_flows_bulk(d, f, g))
+
+ def update_flows_incrementally(self, device, flow_changes, group_changes):
+ d = Device()
+ if device:
+ device.Unpack(d)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="device-invalid")
+ f = FlowChanges()
+ if flow_changes:
+ flow_changes.Unpack(f)
+
+ g = FlowGroupChanges()
+ if group_changes:
+ group_changes.Unpack(g)
+
+ return (True, self.adapter.update_flows_incrementally(d, f, g))
+
+ def suppress_alarm(self, filter):
+ return self.adapter.suppress_alarm(filter)
+
+ def unsuppress_alarm(self, filter):
+ return self.adapter.unsuppress_alarm(filter)
+
+ def process_inter_adapter_message(self, msg):
+ m = InterAdapterMessage()
+ if msg:
+ msg.Unpack(m)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="msg-invalid")
+
+ return (True, self.adapter.process_inter_adapter_message(m))
+
+
+ def receive_packet_out(self, deviceId, outPort, packet):
+ try:
+ d_id = StrType()
+ if deviceId:
+ deviceId.Unpack(d_id)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="deviceid-invalid")
+
+ op = IntType()
+ if outPort:
+ outPort.Unpack(op)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="outport-invalid")
+
+ p = ofp_packet_out()
+ if packet:
+ packet.Unpack(p)
+ else:
+ return False, Error(code=ErrorCode.INVALID_PARAMETERS,
+ reason="packet-invalid")
+
+ return (True, self.adapter.receive_packet_out(d_id.val, op.val, p))
+ except Exception as e:
+ log.exception("error-processing-receive_packet_out", e=e)
+
diff --git a/python/adapters/kafka/container_proxy.py b/python/adapters/kafka/container_proxy.py
new file mode 100644
index 0000000..efcf558
--- /dev/null
+++ b/python/adapters/kafka/container_proxy.py
@@ -0,0 +1,133 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+The superclass for all kafka proxy subclasses.
+"""
+
+import structlog
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python import failure
+from zope.interface import implementer
+
+from python.common.utils.deferred_utils import DeferredWithTimeout, \
+ TimeOutError
+from python.common.utils.registry import IComponent
+
+log = structlog.get_logger()
+
+
+class KafkaMessagingError(BaseException):
+ def __init__(self, error):
+ self.error = error
+
+
+@implementer(IComponent)
+class ContainerProxy(object):
+
+ def __init__(self, kafka_proxy, core_topic, my_listening_topic):
+ self.kafka_proxy = kafka_proxy
+ self.listening_topic = my_listening_topic
+ self.core_topic = core_topic
+ self.default_timeout = 3
+
+ def start(self):
+ log.info('started')
+
+ return self
+
+ def stop(self):
+ log.info('stopped')
+
+ @classmethod
+ def wrap_request(cls, return_cls):
+ def real_wrapper(func):
+ @inlineCallbacks
+ def wrapper(*args, **kw):
+ try:
+ (success, d) = yield func(*args, **kw)
+ if success:
+ log.debug("successful-response", func=func, val=d)
+ if return_cls is not None:
+ rc = return_cls()
+ if d is not None:
+ d.Unpack(rc)
+ returnValue(rc)
+ else:
+ log.debug("successful-response-none", func=func,
+ val=None)
+ returnValue(None)
+ else:
+ log.warn("unsuccessful-request", func=func, args=args,
+ kw=kw)
+ returnValue(d)
+ except Exception as e:
+ log.exception("request-wrapper-exception", func=func, e=e)
+ raise
+
+ return wrapper
+
+ return real_wrapper
+
+ @inlineCallbacks
+ def invoke(self, rpc, to_topic=None, reply_topic=None, **kwargs):
+ @inlineCallbacks
+ def _send_request(rpc, m_callback, to_topic, reply_topic, **kwargs):
+ try:
+ log.debug("sending-request",
+ rpc=rpc,
+ to_topic=to_topic,
+ reply_topic=reply_topic)
+ if to_topic is None:
+ to_topic = self.core_topic
+ if reply_topic is None:
+ reply_topic = self.listening_topic
+ result = yield self.kafka_proxy.send_request(rpc=rpc,
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ callback=None,
+ **kwargs)
+ if not m_callback.called:
+ m_callback.callback(result)
+ else:
+ log.debug('timeout-already-occurred', rpc=rpc)
+ except Exception as e:
+ log.exception("Failure-sending-request", rpc=rpc, kw=kwargs)
+ if not m_callback.called:
+ m_callback.errback(failure.Failure())
+
+ # We are going to resend the request on the to_topic if there is a
+ # timeout error. This time the timeout will be longer. If the second
+ # request times out then we will send the request to the default
+ # core_topic.
+ timeouts = [self.default_timeout,
+ self.default_timeout*2,
+ self.default_timeout]
+ retry = 0
+ max_retry = 2
+ for timeout in timeouts:
+ cb = DeferredWithTimeout(timeout=timeout)
+ _send_request(rpc, cb, to_topic, reply_topic, **kwargs)
+ try:
+ res = yield cb
+ returnValue(res)
+ except TimeOutError as e:
+ log.warn('invoke-timeout', e=e)
+ if retry == max_retry:
+ raise e
+ retry += 1
+ if retry == max_retry:
+ to_topic = self.core_topic
diff --git a/python/adapters/kafka/core_proxy.py b/python/adapters/kafka/core_proxy.py
new file mode 100644
index 0000000..b897188
--- /dev/null
+++ b/python/adapters/kafka/core_proxy.py
@@ -0,0 +1,344 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Agent to play gateway between CORE and an adapter.
+"""
+import structlog
+from google.protobuf.message import Message
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from container_proxy import ContainerProxy
+from python.protos.common_pb2 import ID, ConnectStatus, OperStatus
+from python.protos.inter_container_pb2 import StrType, BoolType, IntType, Packet
+from python.protos.device_pb2 import Device, Ports
+from python.protos.voltha_pb2 import CoreInstance
+
+log = structlog.get_logger()
+
+
+def createSubTopic(*args):
+ return '_'.join(args)
+
+class CoreProxy(ContainerProxy):
+
+ def __init__(self, kafka_proxy, core_topic, my_listening_topic):
+ super(CoreProxy, self).__init__(kafka_proxy, core_topic,
+ my_listening_topic)
+
+ @ContainerProxy.wrap_request(CoreInstance)
+ @inlineCallbacks
+ def register(self, adapter, deviceTypes):
+ log.debug("register")
+ try:
+ res = yield self.invoke(rpc="Register",
+ adapter=adapter,
+ deviceTypes=deviceTypes)
+ log.info("registration-returned", res=res)
+ returnValue(res)
+ except Exception as e:
+ log.exception("registration-exception", e=e)
+ raise
+
+ @ContainerProxy.wrap_request(Device)
+ @inlineCallbacks
+ def get_device(self, device_id):
+ log.debug("get-device")
+ id = ID()
+ id.id = device_id
+ # Once we have a device being managed, all communications between the
+ # the adapter and the core occurs over a topic associated with that
+ # device
+ to_topic = createSubTopic(self.core_topic, device_id)
+ reply_topic = createSubTopic(self.listening_topic, device_id)
+ res = yield self.invoke(rpc="GetDevice",
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ device_id=id)
+ returnValue(res)
+
+ @ContainerProxy.wrap_request(Device)
+ @inlineCallbacks
+ def get_child_device(self, parent_device_id, **kwargs):
+ raise NotImplementedError()
+
+ @ContainerProxy.wrap_request(Ports)
+ @inlineCallbacks
+ def get_ports(self, device_id, port_type):
+ id = ID()
+ id.id = device_id
+ p_type = IntType()
+ p_type.val = port_type
+ to_topic = createSubTopic(self.core_topic, device_id)
+ reply_topic = createSubTopic(self.listening_topic, device_id)
+ res = yield self.invoke(rpc="GetPorts",
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ device_id=id,
+ port_type=p_type)
+ returnValue(res)
+
+ def get_child_devices(self, parent_device_id):
+ raise NotImplementedError()
+
+ def get_child_device_with_proxy_address(self, proxy_address):
+ raise NotImplementedError()
+
+ def _to_proto(self, **kwargs):
+ encoded = {}
+ for k, v in kwargs.iteritems():
+ if isinstance(v, Message):
+ encoded[k] = v
+ elif type(v) == int:
+ i_proto = IntType()
+ i_proto.val = v
+ encoded[k] = i_proto
+ elif type(v) == str:
+ s_proto = StrType()
+ s_proto.val = v
+ encoded[k] = s_proto
+ elif type(v) == bool:
+ b_proto = BoolType()
+ b_proto.val = v
+ encoded[k] = b_proto
+ return encoded
+
+ @ContainerProxy.wrap_request(None)
+ @inlineCallbacks
+ def child_device_detected(self,
+ parent_device_id,
+ parent_port_no,
+ child_device_type,
+ channel_id,
+ **kw):
+ id = ID()
+ id.id = parent_device_id
+ ppn = IntType()
+ ppn.val = parent_port_no
+ cdt = StrType()
+ cdt.val = child_device_type
+ channel = IntType()
+ channel.val = channel_id
+ to_topic = createSubTopic(self.core_topic, parent_device_id)
+ reply_topic = createSubTopic(self.listening_topic, parent_device_id)
+ args = self._to_proto(**kw)
+ res = yield self.invoke(rpc="ChildDeviceDetected",
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ parent_device_id=id,
+ parent_port_no=ppn,
+ child_device_type=cdt,
+ channel_id=channel,
+ **args)
+ returnValue(res)
+
+ @ContainerProxy.wrap_request(None)
+ @inlineCallbacks
+ def device_update(self, device):
+ log.debug("device_update")
+ to_topic = createSubTopic(self.core_topic, device.id)
+ reply_topic = createSubTopic(self.listening_topic, device.id)
+ res = yield self.invoke(rpc="DeviceUpdate",
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ device=device)
+ returnValue(res)
+
+ def child_device_removed(parent_device_id, child_device_id):
+ raise NotImplementedError()
+
+ @ContainerProxy.wrap_request(None)
+ @inlineCallbacks
+ def device_state_update(self, device_id,
+ oper_status=None,
+ connect_status=None):
+ id = ID()
+ id.id = device_id
+ o_status = IntType()
+ if oper_status or oper_status == OperStatus.UNKNOWN:
+ o_status.val = oper_status
+ else:
+ o_status.val = -1
+ c_status = IntType()
+ if connect_status or connect_status == ConnectStatus.UNKNOWN:
+ c_status.val = connect_status
+ else:
+ c_status.val = -1
+
+ to_topic = createSubTopic(self.core_topic, device_id)
+ reply_topic = createSubTopic(self.listening_topic, device_id)
+ res = yield self.invoke(rpc="DeviceStateUpdate",
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ device_id=id,
+ oper_status=o_status,
+ connect_status=c_status)
+ returnValue(res)
+
+ @ContainerProxy.wrap_request(None)
+ @inlineCallbacks
+ def children_state_update(self, device_id,
+ oper_status=None,
+ connect_status=None):
+ id = ID()
+ id.id = device_id
+ o_status = IntType()
+ if oper_status or oper_status == OperStatus.UNKNOWN:
+ o_status.val = oper_status
+ else:
+ o_status.val = -1
+ c_status = IntType()
+ if connect_status or connect_status == ConnectStatus.UNKNOWN:
+ c_status.val = connect_status
+ else:
+ c_status.val = -1
+
+ to_topic = createSubTopic(self.core_topic, device_id)
+ reply_topic = createSubTopic(self.listening_topic, device_id)
+ res = yield self.invoke(rpc="ChildrenStateUpdate",
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ device_id=id,
+ oper_status=o_status,
+ connect_status=c_status)
+ returnValue(res)
+
+ @ContainerProxy.wrap_request(None)
+ @inlineCallbacks
+ def port_state_update(self,
+ device_id,
+ port_type,
+ port_no,
+ oper_status):
+ id = ID()
+ id.id = device_id
+ pt = IntType()
+ pt.val = port_type
+ pNo = IntType()
+ pNo.val = port_no
+ o_status = IntType()
+ o_status.val = oper_status
+
+ to_topic = createSubTopic(self.core_topic, device_id)
+ reply_topic = createSubTopic(self.listening_topic, device_id)
+ res = yield self.invoke(rpc="PortStateUpdate",
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ device_id=id,
+ port_type=pt,
+ port_no=pNo,
+ oper_status=o_status)
+ returnValue(res)
+
+ @ContainerProxy.wrap_request(None)
+ @inlineCallbacks
+ def child_devices_state_update(self, parent_device_id,
+ oper_status=None,
+ connect_status=None):
+
+ id = ID()
+ id.id = parent_device_id
+ o_status = IntType()
+ if oper_status or oper_status == OperStatus.UNKNOWN:
+ o_status.val = oper_status
+ else:
+ o_status.val = -1
+ c_status = IntType()
+ if connect_status or connect_status == ConnectStatus.UNKNOWN:
+ c_status.val = connect_status
+ else:
+ c_status.val = -1
+
+ to_topic = createSubTopic(self.core_topic, parent_device_id)
+ reply_topic = createSubTopic(self.listening_topic, parent_device_id)
+ res = yield self.invoke(rpc="child_devices_state_update",
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ parent_device_id=id,
+ oper_status=o_status,
+ connect_status=c_status)
+ returnValue(res)
+
+ def child_devices_removed(parent_device_id):
+ raise NotImplementedError()
+
+ @ContainerProxy.wrap_request(None)
+ @inlineCallbacks
+ def device_pm_config_update(self, device_pm_config, init=False):
+ log.debug("device_pm_config_update")
+ b = BoolType()
+ b.val = init
+ to_topic = createSubTopic(self.core_topic, device_pm_config.id)
+ reply_topic = createSubTopic(self.listening_topic, device_pm_config.id)
+ res = yield self.invoke(rpc="DevicePMConfigUpdate",
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ device_pm_config=device_pm_config,
+ init=b)
+ returnValue(res)
+
+ @ContainerProxy.wrap_request(None)
+ @inlineCallbacks
+ def port_created(self, device_id, port):
+ log.debug("port_created")
+ proto_id = ID()
+ proto_id.id = device_id
+ to_topic = createSubTopic(self.core_topic, device_id)
+ reply_topic = createSubTopic(self.listening_topic, device_id)
+ res = yield self.invoke(rpc="PortCreated",
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ device_id=proto_id,
+ port=port)
+ returnValue(res)
+
+ def port_removed(device_id, port):
+ raise NotImplementedError()
+
+ def ports_enabled(device_id):
+ raise NotImplementedError()
+
+ def ports_disabled(device_id):
+ raise NotImplementedError()
+
+ def ports_oper_status_update(device_id, oper_status):
+ raise NotImplementedError()
+
+ def image_download_update(img_dnld):
+ raise NotImplementedError()
+
+ def image_download_deleted(img_dnld):
+ raise NotImplementedError()
+
+ @ContainerProxy.wrap_request(None)
+ @inlineCallbacks
+ def send_packet_in(self, device_id, port, packet):
+ log.debug("send_packet_in", device_id=device_id)
+ proto_id = ID()
+ proto_id.id = device_id
+ p = IntType()
+ p.val = port
+ pac = Packet()
+ pac.payload = packet
+ to_topic = createSubTopic(self.core_topic, device_id)
+ reply_topic = createSubTopic(self.listening_topic, device_id)
+ res = yield self.invoke(rpc="PacketIn",
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ device_id=proto_id,
+ port=p,
+ packet=pac)
+ returnValue(res)
diff --git a/python/adapters/kafka/event_bus_publisher.py b/python/adapters/kafka/event_bus_publisher.py
new file mode 100644
index 0000000..89b3385
--- /dev/null
+++ b/python/adapters/kafka/event_bus_publisher.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+A gateway between the internal event bus and the Kafka publisher proxy
+to publish select topics and messages posted to the Voltha-internal event
+bus toward the external world.
+"""
+import structlog
+from google.protobuf.json_format import MessageToDict
+from google.protobuf.message import Message
+from simplejson import dumps
+
+from python.common.event_bus import EventBusClient
+
+log = structlog.get_logger()
+
+
+class EventBusPublisher(object):
+
+ def __init__(self, kafka_proxy, config):
+ self.kafka_proxy = kafka_proxy
+ self.config = config
+ self.topic_mappings = config.get('topic_mappings', {})
+ self.event_bus = EventBusClient()
+ self.subscriptions = None
+
+ def start(self):
+ log.debug('starting')
+ self.subscriptions = list()
+ self._setup_subscriptions(self.topic_mappings)
+ log.info('started')
+ return self
+
+ def stop(self):
+ try:
+ log.debug('stopping-event-bus')
+ if self.subscriptions:
+ for subscription in self.subscriptions:
+ self.event_bus.unsubscribe(subscription)
+ log.info('stopped-event-bus')
+ except Exception, e:
+ log.exception('failed-stopping-event-bus', e=e)
+ return
+
+ def _setup_subscriptions(self, mappings):
+
+ for event_bus_topic, mapping in mappings.iteritems():
+
+ kafka_topic = mapping.get('kafka_topic', None)
+
+ if kafka_topic is None:
+ log.error('no-kafka-topic-in-config',
+ event_bus_topic=event_bus_topic,
+ mapping=mapping)
+ continue
+
+ self.subscriptions.append(self.event_bus.subscribe(
+ event_bus_topic,
+ # to avoid Python late-binding to the last registered
+ # kafka_topic, we force instant binding with the default arg
+ lambda _, m, k=kafka_topic: self.forward(k, m)))
+
+ log.info('event-to-kafka', kafka_topic=kafka_topic,
+ event_bus_topic=event_bus_topic)
+
+ def forward(self, kafka_topic, msg):
+ try:
+ # convert to JSON string if msg is a protobuf msg
+ if isinstance(msg, Message):
+ msg = dumps(MessageToDict(msg, True, True))
+ log.debug('forward-event-bus-publisher')
+ self.kafka_proxy.send_message(kafka_topic, msg)
+ except Exception, e:
+ log.exception('failed-forward-event-bus-publisher', e=e)
+
diff --git a/python/adapters/kafka/kafka_inter_container_library.py b/python/adapters/kafka/kafka_inter_container_library.py
new file mode 100644
index 0000000..5cad2e8
--- /dev/null
+++ b/python/adapters/kafka/kafka_inter_container_library.py
@@ -0,0 +1,570 @@
+#!/usr/bin/env python
+
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import time
+from uuid import uuid4
+
+import structlog
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, Deferred, \
+ DeferredQueue, gatherResults
+from zope.interface import implementer
+
+from python.common.utils import asleep
+from python.common.utils.registry import IComponent
+from kafka_proxy import KafkaProxy, get_kafka_proxy
+from python.protos.inter_container_pb2 import MessageType, Argument, \
+ InterContainerRequestBody, InterContainerMessage, Header, \
+ InterContainerResponseBody
+
+log = structlog.get_logger()
+
+KAFKA_OFFSET_LATEST = 'latest'
+KAFKA_OFFSET_EARLIEST = 'earliest'
+
+
+class KafkaMessagingError(BaseException):
+ def __init__(self, error):
+ self.error = error
+
+
+@implementer(IComponent)
+class IKafkaMessagingProxy(object):
+ _kafka_messaging_instance = None
+
+ def __init__(self,
+ kafka_host_port,
+ kv_store,
+ default_topic,
+ group_id_prefix,
+ target_cls):
+ """
+ Initialize the kafka proxy. This is a singleton (may change to
+ non-singleton if performance is better)
+ :param kafka_host_port: Kafka host and port
+ :param kv_store: Key-Value store
+ :param default_topic: Default topic to subscribe to
+ :param target_cls: target class - method of that class is invoked
+ when a message is received on the default_topic
+ """
+ # return an exception if the object already exist
+ if IKafkaMessagingProxy._kafka_messaging_instance:
+ raise Exception(
+ 'Singleton-exist', cls=IKafkaMessagingProxy)
+
+ log.debug("Initializing-KafkaProxy")
+ self.kafka_host_port = kafka_host_port
+ self.kv_store = kv_store
+ self.default_topic = default_topic
+ self.default_group_id = "_".join((group_id_prefix, default_topic))
+ self.target_cls = target_cls
+ self.topic_target_cls_map = {}
+ self.topic_callback_map = {}
+ self.subscribers = {}
+ self.kafka_proxy = None
+ self.transaction_id_deferred_map = {}
+ self.received_msg_queue = DeferredQueue()
+ self.stopped = False
+
+ self.init_time = 0
+ self.init_received_time = 0
+
+ self.init_resp_time = 0
+ self.init_received_resp_time = 0
+
+ self.num_messages = 0
+ self.total_time = 0
+ self.num_responses = 0
+ self.total_time_responses = 0
+ log.debug("KafkaProxy-initialized")
+
+ def start(self):
+ try:
+ log.debug("KafkaProxy-starting")
+
+ # Get the kafka proxy instance. If it does not exist then
+ # create it
+ self.kafka_proxy = get_kafka_proxy()
+ if self.kafka_proxy == None:
+ KafkaProxy(kafka_endpoint=self.kafka_host_port).start()
+ self.kafka_proxy = get_kafka_proxy()
+
+ # Subscribe the default topic and target_cls
+ self.topic_target_cls_map[self.default_topic] = self.target_cls
+
+ # Start the queue to handle incoming messages
+ reactor.callLater(0, self._received_message_processing_loop)
+
+ # Subscribe using the default topic and default group id. Whenever
+ # a message is received on that topic then teh target_cls will be
+ # invoked.
+ reactor.callLater(0, self.subscribe,
+ topic=self.default_topic,
+ target_cls=self.target_cls,
+ group_id=self.default_group_id)
+
+ # Setup the singleton instance
+ IKafkaMessagingProxy._kafka_messaging_instance = self
+ log.debug("KafkaProxy-started")
+ except Exception as e:
+ log.exception("Failed-to-start-proxy", e=e)
+
+ def stop(self):
+ """
+ Invoked to stop the kafka proxy
+ :return: None on success, Exception on failure
+ """
+ log.debug("Stopping-messaging-proxy ...")
+ try:
+ # Stop the kafka proxy. This will stop all the consumers
+ # and producers
+ self.stopped = True
+ self.kafka_proxy.stop()
+ log.debug("Messaging-proxy-stopped.")
+ except Exception as e:
+ log.exception("Exception-when-stopping-messaging-proxy:", e=e)
+
+ def get_target_cls(self):
+ return self.target_cls
+
+ def get_default_topic(self):
+ return self.default_topic
+
+ @inlineCallbacks
+ def _subscribe_group_consumer(self, group_id, topic, offset, callback=None,
+ target_cls=None):
+ try:
+ log.debug("subscribing-to-topic-start", topic=topic)
+ yield self.kafka_proxy.subscribe(topic,
+ self._enqueue_received_group_message,
+ group_id, offset)
+
+ if target_cls is not None and callback is None:
+ # Scenario #1
+ if topic not in self.topic_target_cls_map:
+ self.topic_target_cls_map[topic] = target_cls
+ elif target_cls is None and callback is not None:
+ # Scenario #2
+ log.debug("custom-callback", topic=topic,
+ callback_map=self.topic_callback_map)
+ if topic not in self.topic_callback_map:
+ self.topic_callback_map[topic] = [callback]
+ else:
+ self.topic_callback_map[topic].extend([callback])
+ else:
+ log.warn("invalid-parameters")
+
+ returnValue(True)
+ except Exception as e:
+ log.exception("Exception-during-subscription", e=e)
+ returnValue(False)
+
+ @inlineCallbacks
+ def subscribe(self, topic, callback=None, target_cls=None,
+ max_retry=3, group_id=None, offset=KAFKA_OFFSET_LATEST):
+ """
+ Scenario 1: invoked to subscribe to a specific topic with a
+ target_cls to invoke when a message is received on that topic. This
+ handles the case of request/response where this library performs the
+ heavy lifting. In this case the m_callback must to be None
+
+ Scenario 2: invoked to subscribe to a specific topic with a
+ specific callback to invoke when a message is received on that topic.
+ This handles the case where the caller wants to process the message
+ received itself. In this case the target_cls must to be None
+
+ :param topic: topic to subscribe to
+ :param callback: Callback to invoke when a message is received on
+ the topic. Either one of callback or target_cls needs can be none
+ :param target_cls: Target class to use when a message is
+ received on the topic. There can only be 1 target_cls per topic.
+ Either one of callback or target_cls needs can be none
+ :param max_retry: the number of retries before reporting failure
+ to subscribe. This caters for scenario where the kafka topic is not
+ ready.
+ :param group_id: The ID of the group the consumer is subscribing to
+ :param offset: The topic offset on the kafka bus from where message consumption will start
+ :return: True on success, False on failure
+ """
+ RETRY_BACKOFF = [0.05, 0.1, 0.2, 0.5, 1, 2, 5]
+
+ def _backoff(msg, retries):
+ wait_time = RETRY_BACKOFF[min(retries,
+ len(RETRY_BACKOFF) - 1)]
+ log.info(msg, retry_in=wait_time)
+ return asleep.asleep(wait_time)
+
+ log.debug("subscribing", topic=topic, group_id=group_id,
+ callback=callback, target=target_cls)
+
+ retry = 0
+ subscribed = False
+ if group_id is None:
+ group_id = self.default_group_id
+ while not subscribed:
+ subscribed = yield self._subscribe_group_consumer(group_id, topic,
+ callback=callback,
+ target_cls=target_cls,
+ offset=offset)
+ if subscribed:
+ returnValue(True)
+ elif retry > max_retry:
+ returnValue(False)
+ else:
+ _backoff("subscription-not-complete", retry)
+ retry += 1
+
+ def unsubscribe(self, topic, callback=None, target_cls=None):
+ """
+ Invoked when unsubscribing to a topic
+ :param topic: topic to unsubscribe from
+ :param callback: the callback used when subscribing to the topic, if any
+ :param target_cls: the targert class used when subscribing to the topic, if any
+ :return: None on success or Exception on failure
+ """
+ log.debug("Unsubscribing-to-topic", topic=topic)
+
+ try:
+ self.kafka_proxy.unsubscribe(topic,
+ self._enqueue_received_group_message)
+
+ if callback is None and target_cls is None:
+ log.error("both-call-and-target-cls-cannot-be-none",
+ topic=topic)
+ raise KafkaMessagingError(
+ error="both-call-and-target-cls-cannot-be-none")
+
+ if target_cls is not None and topic in self.topic_target_cls_map:
+ del self.topic_target_cls_map[topic]
+
+ if callback is not None and topic in self.topic_callback_map:
+ index = 0
+ for cb in self.topic_callback_map[topic]:
+ if cb == callback:
+ break
+ index += 1
+ if index < len(self.topic_callback_map[topic]):
+ self.topic_callback_map[topic].pop(index)
+
+ if len(self.topic_callback_map[topic]) == 0:
+ del self.topic_callback_map[topic]
+ except Exception as e:
+ log.exception("Exception-when-unsubscribing-to-topic", topic=topic,
+ e=e)
+ return e
+
+ @inlineCallbacks
+ def _enqueue_received_group_message(self, msg):
+ """
+ Internal method to continuously queue all received messaged
+ irrespective of topic
+ :param msg: Received message
+ :return: None on success, Exception on failure
+ """
+ try:
+ log.debug("received-msg", msg=msg)
+ yield self.received_msg_queue.put(msg)
+ except Exception as e:
+ log.exception("Failed-enqueueing-received-message", e=e)
+
+ @inlineCallbacks
+ def _received_message_processing_loop(self):
+ """
+ Internal method to continuously process all received messages one
+ at a time
+ :return: None on success, Exception on failure
+ """
+ while True:
+ try:
+ message = yield self.received_msg_queue.get()
+ yield self._process_message(message)
+ if self.stopped:
+ break
+ except Exception as e:
+ log.exception("Failed-dequeueing-received-message", e=e)
+
+ def _to_string(self, unicode_str):
+ if unicode_str is not None:
+ if type(unicode_str) == unicode:
+ return unicode_str.encode('ascii', 'ignore')
+ else:
+ return unicode_str
+ else:
+ return None
+
+ def _format_request(self,
+ rpc,
+ to_topic,
+ reply_topic,
+ **kwargs):
+ """
+ Format a request to send over kafka
+ :param rpc: Requested remote API
+ :param to_topic: Topic to send the request
+ :param reply_topic: Topic to receive the resulting response, if any
+ :param kwargs: Dictionary of key-value pairs to pass as arguments to
+ the remote rpc API.
+ :return: A InterContainerMessage message type on success or None on
+ failure
+ """
+ try:
+ transaction_id = uuid4().hex
+ request = InterContainerMessage()
+ request_body = InterContainerRequestBody()
+ request.header.id = transaction_id
+ request.header.type = MessageType.Value("REQUEST")
+ request.header.from_topic = reply_topic
+ request.header.to_topic = to_topic
+
+ response_required = False
+ if reply_topic:
+ request_body.reply_to_topic = reply_topic
+ request_body.response_required = True
+ response_required = True
+
+ request.header.timestamp = int(round(time.time() * 1000))
+ request_body.rpc = rpc
+ for a, b in kwargs.iteritems():
+ arg = Argument()
+ arg.key = a
+ try:
+ arg.value.Pack(b)
+ request_body.args.extend([arg])
+ except Exception as e:
+ log.exception("Failed-parsing-value", e=e)
+ request.body.Pack(request_body)
+ return request, transaction_id, response_required
+ except Exception as e:
+ log.exception("formatting-request-failed",
+ rpc=rpc,
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ args=kwargs)
+ return None, None, None
+
+ def _format_response(self, msg_header, msg_body, status):
+ """
+ Format a response
+ :param msg_header: The header portion of a received request
+ :param msg_body: The response body
+ :param status: True is this represents a successful response
+ :return: a InterContainerMessage message type
+ """
+ try:
+ assert isinstance(msg_header, Header)
+ response = InterContainerMessage()
+ response_body = InterContainerResponseBody()
+ response.header.id = msg_header.id
+ response.header.timestamp = int(
+ round(time.time() * 1000))
+ response.header.type = MessageType.Value("RESPONSE")
+ response.header.from_topic = msg_header.to_topic
+ response.header.to_topic = msg_header.from_topic
+ if msg_body is not None:
+ response_body.result.Pack(msg_body)
+ response_body.success = status
+ response.body.Pack(response_body)
+ return response
+ except Exception as e:
+ log.exception("formatting-response-failed", header=msg_header,
+ body=msg_body, status=status, e=e)
+ return None
+
+ def _parse_response(self, msg):
+ try:
+ message = InterContainerMessage()
+ message.ParseFromString(msg)
+ resp = InterContainerResponseBody()
+ if message.body.Is(InterContainerResponseBody.DESCRIPTOR):
+ message.body.Unpack(resp)
+ else:
+ log.debug("unsupported-msg", msg_type=type(message.body))
+ return None
+ log.debug("parsed-response", input=message, output=resp)
+ return resp
+ except Exception as e:
+ log.exception("parsing-response-failed", msg=msg, e=e)
+ return None
+
+ @inlineCallbacks
+ def _process_message(self, m):
+ """
+ Default internal method invoked for every batch of messages received
+ from Kafka.
+ """
+
+ def _toDict(args):
+ """
+ Convert a repeatable Argument type into a python dictionary
+ :param args: Repeatable core_adapter.Argument type
+ :return: a python dictionary
+ """
+ if args is None:
+ return None
+ result = {}
+ for arg in args:
+ assert isinstance(arg, Argument)
+ result[arg.key] = arg.value
+ return result
+
+ current_time = int(round(time.time() * 1000))
+ # log.debug("Got Message", message=m)
+ try:
+ val = m.value()
+ # val = m.message.value
+ # print m.topic
+
+ # Go over customized callbacks first
+ m_topic = m.topic()
+ if m_topic in self.topic_callback_map:
+ for c in self.topic_callback_map[m_topic]:
+ yield c(val)
+
+ # Check whether we need to process request/response scenario
+ if m_topic not in self.topic_target_cls_map:
+ return
+
+ # Process request/response scenario
+ message = InterContainerMessage()
+ message.ParseFromString(val)
+
+ if message.header.type == MessageType.Value("REQUEST"):
+ # Get the target class for that specific topic
+ targetted_topic = self._to_string(message.header.to_topic)
+ msg_body = InterContainerRequestBody()
+ if message.body.Is(InterContainerRequestBody.DESCRIPTOR):
+ message.body.Unpack(msg_body)
+ else:
+ log.debug("unsupported-msg", msg_type=type(message.body))
+ return
+ if targetted_topic in self.topic_target_cls_map:
+ if msg_body.args:
+ log.debug("message-body-args-present", body=msg_body)
+ (status, res) = yield getattr(
+ self.topic_target_cls_map[targetted_topic],
+ self._to_string(msg_body.rpc))(
+ **_toDict(msg_body.args))
+ else:
+ log.debug("message-body-args-absent", body=msg_body,
+ rpc=msg_body.rpc)
+ (status, res) = yield getattr(
+ self.topic_target_cls_map[targetted_topic],
+ self._to_string(msg_body.rpc))()
+ if msg_body.response_required:
+ response = self._format_response(
+ msg_header=message.header,
+ msg_body=res,
+ status=status,
+ )
+ if response is not None:
+ res_topic = self._to_string(
+ response.header.to_topic)
+ self._send_kafka_message(res_topic, response)
+
+ log.debug("Response-sent", response=response.body,
+ to_topic=res_topic)
+ elif message.header.type == MessageType.Value("RESPONSE"):
+ trns_id = self._to_string(message.header.id)
+ if trns_id in self.transaction_id_deferred_map:
+ resp = self._parse_response(val)
+
+ self.transaction_id_deferred_map[trns_id].callback(resp)
+ else:
+ log.error("!!INVALID-TRANSACTION-TYPE!!")
+
+ except Exception as e:
+ log.exception("Failed-to-process-message", message=m, e=e)
+
+ @inlineCallbacks
+ def _send_kafka_message(self, topic, msg):
+ try:
+ yield self.kafka_proxy.send_message(topic, msg.SerializeToString())
+ except Exception, e:
+ log.exception("Failed-sending-message", message=msg, e=e)
+
+ @inlineCallbacks
+ def send_request(self,
+ rpc,
+ to_topic,
+ reply_topic=None,
+ callback=None,
+ **kwargs):
+ """
+ Invoked to send a message to a remote container and receive a
+ response if required.
+ :param rpc: The remote API to invoke
+ :param to_topic: Send the message to this kafka topic
+ :param reply_topic: If not None then a response is expected on this
+ topic. If set to None then no response is required.
+ :param callback: Callback to invoke when a response is received.
+ :param kwargs: Key-value pairs representing arguments to pass to the
+ rpc remote API.
+ :return: Either no response is required, or a response is returned
+ via the callback or the response is a tuple of (status, return_cls)
+ """
+ try:
+ # Ensure all strings are not unicode encoded
+ rpc = self._to_string(rpc)
+ to_topic = self._to_string(to_topic)
+ reply_topic = self._to_string(reply_topic)
+
+ request, transaction_id, response_required = \
+ self._format_request(
+ rpc=rpc,
+ to_topic=to_topic,
+ reply_topic=reply_topic,
+ **kwargs)
+
+ if request is None:
+ return
+
+ # Add the transaction to the transaction map before sending the
+ # request. This will guarantee the eventual response will be
+ # processed.
+ wait_for_result = None
+ if response_required:
+ wait_for_result = Deferred()
+ self.transaction_id_deferred_map[
+ self._to_string(request.header.id)] = wait_for_result
+
+ yield self._send_kafka_message(to_topic, request)
+ log.debug("message-sent", to_topic=to_topic,
+ from_topic=reply_topic)
+
+ if response_required:
+ res = yield wait_for_result
+
+ if res is None or not res.success:
+ raise KafkaMessagingError(error="Failed-response:{"
+ "}".format(res))
+
+ # Remove the transaction from the transaction map
+ del self.transaction_id_deferred_map[transaction_id]
+
+ log.debug("send-message-response", rpc=rpc, result=res)
+
+ if callback:
+ callback((res.success, res.result))
+ else:
+ returnValue((res.success, res.result))
+ except Exception as e:
+ log.exception("Exception-sending-request", e=e)
+ raise KafkaMessagingError(error=e)
+
+
+# Common method to get the singleton instance of the kafka proxy class
+def get_messaging_proxy():
+ return IKafkaMessagingProxy._kafka_messaging_instance
diff --git a/python/adapters/kafka/kafka_proxy.py b/python/adapters/kafka/kafka_proxy.py
new file mode 100644
index 0000000..cefc590
--- /dev/null
+++ b/python/adapters/kafka/kafka_proxy.py
@@ -0,0 +1,338 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+from confluent_kafka import Producer as _kafkaProducer
+from structlog import get_logger
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.threads import deferToThread
+from zope.interface import implementer
+
+from python.common.utils.consulhelpers import get_endpoint_from_consul
+from event_bus_publisher import EventBusPublisher
+from python.common.utils.registry import IComponent
+from confluent_kafka import Consumer, KafkaError
+import threading
+
+log = get_logger()
+
+
+@implementer(IComponent)
+class KafkaProxy(object):
+ """
+ This is a singleton proxy kafka class to hide the kafka client details. This
+ proxy uses confluent-kafka-python as the kafka client. Since that client is
+ not a Twisted client then requests to that client are wrapped with
+ twisted.internet.threads.deferToThread to avoid any potential blocking of
+ the Twisted loop.
+ """
+ _kafka_instance = None
+
+ def __init__(self,
+ consul_endpoint='localhost:8500',
+ kafka_endpoint='localhost:9092',
+ ack_timeout=1000,
+ max_req_attempts=10,
+ consumer_poll_timeout=10,
+ config={}):
+
+ # return an exception if the object already exist
+ if KafkaProxy._kafka_instance:
+ raise Exception('Singleton exist for :{}'.format(KafkaProxy))
+
+ log.debug('initializing', endpoint=kafka_endpoint)
+ self.ack_timeout = ack_timeout
+ self.max_req_attempts = max_req_attempts
+ self.consul_endpoint = consul_endpoint
+ self.kafka_endpoint = kafka_endpoint
+ self.config = config
+ self.kclient = None
+ self.kproducer = None
+ self.event_bus_publisher = None
+ self.stopping = False
+ self.faulty = False
+ self.consumer_poll_timeout = consumer_poll_timeout
+ self.topic_consumer_map = {}
+ self.topic_callbacks_map = {}
+ self.topic_any_map_lock = threading.Lock()
+ log.debug('initialized', endpoint=kafka_endpoint)
+
+ @inlineCallbacks
+ def start(self):
+ log.debug('starting')
+ self._get_kafka_producer()
+ KafkaProxy._kafka_instance = self
+ self.event_bus_publisher = yield EventBusPublisher(
+ self, self.config.get('event_bus_publisher', {})).start()
+ log.info('started')
+ KafkaProxy.faulty = False
+ self.stopping = False
+ returnValue(self)
+
+ @inlineCallbacks
+ def stop(self):
+ try:
+ log.debug('stopping-kafka-proxy')
+ self.stopping = True
+ try:
+ if self.kclient:
+ yield self.kclient.close()
+ self.kclient = None
+ log.debug('stopped-kclient-kafka-proxy')
+ except Exception, e:
+ log.exception('failed-stopped-kclient-kafka-proxy', e=e)
+
+ try:
+ if self.kproducer:
+ yield self.kproducer.flush()
+ self.kproducer = None
+ log.debug('stopped-kproducer-kafka-proxy')
+ except Exception, e:
+ log.exception('failed-stopped-kproducer-kafka-proxy', e=e)
+
+ # Stop all consumers
+ try:
+ self.topic_any_map_lock.acquire()
+ log.debug('stopping-consumers-kafka-proxy')
+ for _, c in self.topic_consumer_map.iteritems():
+ yield deferToThread(c.close)
+ self.topic_consumer_map.clear()
+ self.topic_callbacks_map.clear()
+ log.debug('stopped-consumers-kafka-proxy')
+ except Exception, e:
+ log.exception('failed-stopped-consumers-kafka-proxy', e=e)
+ finally:
+ self.topic_any_map_lock.release()
+ log.debug('stopping-consumers-kafka-proxy-released-lock')
+
+ # try:
+ # if self.event_bus_publisher:
+ # yield self.event_bus_publisher.stop()
+ # self.event_bus_publisher = None
+ # log.debug('stopped-event-bus-publisher-kafka-proxy')
+ # except Exception, e:
+ # log.debug('failed-stopped-event-bus-publisher-kafka-proxy')
+ # pass
+
+ log.debug('stopped-kafka-proxy')
+
+ except Exception, e:
+ self.kclient = None
+ self.kproducer = None
+ # self.event_bus_publisher = None
+ log.exception('failed-stopped-kafka-proxy', e=e)
+ pass
+
+ def _get_kafka_producer(self):
+
+ try:
+
+ if self.kafka_endpoint.startswith('@'):
+ try:
+ _k_endpoint = get_endpoint_from_consul(self.consul_endpoint,
+ self.kafka_endpoint[
+ 1:])
+ log.debug('found-kafka-service', endpoint=_k_endpoint)
+
+ except Exception as e:
+ log.exception('no-kafka-service-in-consul', e=e)
+
+ self.kproducer = None
+ self.kclient = None
+ return
+ else:
+ _k_endpoint = self.kafka_endpoint
+ self.kproducer = _kafkaProducer(
+ {'bootstrap.servers': _k_endpoint,
+ }
+ )
+ pass
+ except Exception, e:
+ log.exception('failed-get-kafka-producer', e=e)
+ return
+
+ @inlineCallbacks
+ def _wait_for_messages(self, consumer, topic):
+ while True:
+ try:
+ msg = yield deferToThread(consumer.poll,
+ self.consumer_poll_timeout)
+
+ if self.stopping:
+ log.debug("stop-request-recieved", topic=topic)
+ break
+
+ if msg is None:
+ continue
+ if msg.error():
+ # This typically is received when there are no more messages
+ # to read from kafka. Ignore.
+ continue
+
+ # Invoke callbacks
+ for cb in self.topic_callbacks_map[topic]:
+ yield cb(msg)
+ except Exception as e:
+ log.debug("exception-receiving-msg", topic=topic, e=e)
+
+ @inlineCallbacks
+ def subscribe(self, topic, callback, groupId, offset='latest'):
+ """
+ subscribe allows a caller to subscribe to a given kafka topic. This API
+ always create a group consumer.
+ :param topic - the topic to subscribe to
+ :param callback - the callback to invoke whenever a message is received
+ on that topic
+ :param groupId - the groupId for this consumer. In the current
+ implementation there is a one-to-one mapping between a topic and a
+ groupId. In other words, once a groupId is used for a given topic then
+ we won't be able to create another groupId for the same topic.
+ :param offset: the kafka offset from where the consumer will start
+ consuming messages
+ """
+ try:
+ self.topic_any_map_lock.acquire()
+ if topic in self.topic_consumer_map:
+ # Just add the callback
+ if topic in self.topic_callbacks_map:
+ self.topic_callbacks_map[topic].append(callback)
+ else:
+ self.topic_callbacks_map[topic] = [callback]
+ return
+
+ # Create consumer for that topic
+ c = Consumer({
+ 'bootstrap.servers': self.kafka_endpoint,
+ 'group.id': groupId,
+ 'auto.offset.reset': offset
+ })
+ yield deferToThread(c.subscribe, [topic])
+ # c.subscribe([topic])
+ self.topic_consumer_map[topic] = c
+ self.topic_callbacks_map[topic] = [callback]
+ # Start the consumer
+ reactor.callLater(0, self._wait_for_messages, c, topic)
+ except Exception, e:
+ log.exception("topic-subscription-error", e=e)
+ finally:
+ self.topic_any_map_lock.release()
+
+ @inlineCallbacks
+ def unsubscribe(self, topic, callback):
+ """
+ Unsubscribe to a given topic. Since there they be multiple callers
+ consuming from the same topic then to ensure only the relevant caller
+ gets unsubscribe then the callback is used as a differentiator. The
+ kafka consumer will be closed when there are no callbacks required.
+ :param topic: topic to unsubscribe
+ :param callback: callback the caller used when subscribing to the topic.
+ If multiple callers have subscribed to a topic using the same callback
+ then the first callback on the list will be removed.
+ :return:None
+ """
+ try:
+ self.topic_any_map_lock.acquire()
+ log.debug("unsubscribing-to-topic", topic=topic)
+ if topic in self.topic_callbacks_map:
+ index = 0
+ for cb in self.topic_callbacks_map[topic]:
+ if cb == callback:
+ break
+ index += 1
+ if index < len(self.topic_callbacks_map[topic]):
+ self.topic_callbacks_map[topic].pop(index)
+
+ if len(self.topic_callbacks_map[topic]) == 0:
+ # Stop the consumer
+ if topic in self.topic_consumer_map:
+ yield deferToThread(
+ self.topic_consumer_map[topic].close)
+ del self.topic_consumer_map[topic]
+ del self.topic_callbacks_map[topic]
+ log.debug("unsubscribed-to-topic", topic=topic)
+ else:
+ log.debug("consumers-for-topic-still-exist", topic=topic,
+ num=len(self.topic_callbacks_map[topic]))
+ except Exception, e:
+ log.exception("topic-unsubscription-error", e=e)
+ finally:
+ self.topic_any_map_lock.release()
+ log.debug("unsubscribing-to-topic-release-lock", topic=topic)
+
+ @inlineCallbacks
+ def send_message(self, topic, msg, key=None):
+ assert topic is not None
+ assert msg is not None
+
+ # first check whether we have a kafka producer. If there is none
+ # then try to get one - this happens only when we try to lookup the
+ # kafka service from consul
+ try:
+ if self.faulty is False:
+
+ if self.kproducer is None:
+ self._get_kafka_producer()
+ # Lets the next message request do the retry if still a failure
+ if self.kproducer is None:
+ log.error('no-kafka-producer',
+ endpoint=self.kafka_endpoint)
+ return
+
+ log.debug('sending-kafka-msg', topic=topic, kafka_msg=msg)
+ msgs = [msg]
+
+ if self.kproducer is not None and self.event_bus_publisher and self.faulty is False:
+ d = deferToThread(self.kproducer.produce, topic, msg, key)
+ yield d
+ log.debug('sent-kafka-msg', topic=topic, kafka_msg=msg)
+ # send a lightweight poll to avoid an exception after 100k messages.
+ d1 = deferToThread(self.kproducer.poll, 0)
+ yield d1
+ else:
+ return
+
+ except Exception, e:
+ self.faulty = True
+ log.error('failed-to-send-kafka-msg', topic=topic, kafka_msg=msg,
+ e=e)
+
+ # set the kafka producer to None. This is needed if the
+ # kafka docker went down and comes back up with a different
+ # port number.
+ if self.stopping is False:
+ log.debug('stopping-kafka-proxy')
+ try:
+ self.stopping = True
+ self.stop()
+ self.stopping = False
+ self.faulty = False
+ log.debug('stopped-kafka-proxy')
+ except Exception, e:
+ log.exception('failed-stopping-kafka-proxy', e=e)
+ pass
+ else:
+ log.info('already-stopping-kafka-proxy')
+
+ return
+
+ def is_faulty(self):
+ return self.faulty
+
+
+# Common method to get the singleton instance of the kafka proxy class
+def get_kafka_proxy():
+ return KafkaProxy._kafka_instance
diff --git a/python/adapters/openolt/VERSION b/python/adapters/openolt/VERSION
new file mode 100644
index 0000000..c0ab82c
--- /dev/null
+++ b/python/adapters/openolt/VERSION
@@ -0,0 +1 @@
+0.0.1-dev
diff --git a/python/adapters/openolt/__init__.py b/python/adapters/openolt/__init__.py
new file mode 100644
index 0000000..4a82628
--- /dev/null
+++ b/python/adapters/openolt/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/adapters/openolt/main.py b/python/adapters/openolt/main.py
new file mode 100755
index 0000000..273ff13
--- /dev/null
+++ b/python/adapters/openolt/main.py
@@ -0,0 +1,500 @@
+#!/usr/bin/env python
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Ponsim OLT Adapter main entry point"""
+
+import argparse
+import os
+import time
+
+import arrow
+import yaml
+from packaging.version import Version
+from simplejson import dumps
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.task import LoopingCall
+from zope.interface import implementer
+
+from python.common.structlog_setup import setup_logging, update_logging
+from python.common.utils.asleep import asleep
+from python.common.utils.deferred_utils import TimeOutError
+from python.common.utils.dockerhelpers import get_my_containers_name
+from python.common.utils.nethelpers import get_my_primary_local_ipv4, \
+ get_my_primary_interface
+from python.common.utils.registry import registry, IComponent
+from python.adapters.kafka.adapter_proxy import AdapterProxy
+from python.adapters.kafka.adapter_request_facade import AdapterRequestFacade
+from python.adapters.kafka.core_proxy import CoreProxy
+from python.adapters.kafka.kafka_inter_container_library import IKafkaMessagingProxy, \
+ get_messaging_proxy
+from python.adapters.kafka.kafka_proxy import KafkaProxy, get_kafka_proxy
+from openolt import OpenoltAdapter
+from python.protos import third_party
+from python.protos.adapter_pb2 import AdapterConfig
+
+_ = third_party
+
+defs = dict(
+ version_file='./VERSION',
+ config=os.environ.get('CONFIG', './openolt.yml'),
+ container_name_regex=os.environ.get('CONTAINER_NUMBER_EXTRACTOR', '^.*\.(['
+ '0-9]+)\..*$'),
+ consul=os.environ.get('CONSUL', 'localhost:8500'),
+ name=os.environ.get('NAME', 'openolt'),
+ vendor=os.environ.get('VENDOR', 'Voltha Project'),
+ device_type=os.environ.get('DEVICE_TYPE', 'openoltolt'),
+ accept_bulk_flow=os.environ.get('ACCEPT_BULK_FLOW', True),
+ accept_atomic_flow=os.environ.get('ACCEPT_ATOMIC_FLOW', True),
+ etcd=os.environ.get('ETCD', 'localhost:2379'),
+ core_topic=os.environ.get('CORE_TOPIC', 'rwcore'),
+ interface=os.environ.get('INTERFACE', get_my_primary_interface()),
+ instance_id=os.environ.get('INSTANCE_ID', os.environ.get('HOSTNAME', '1')),
+ kafka_adapter=os.environ.get('KAFKA_ADAPTER', '192.168.0.20:9092'),
+ kafka_cluster=os.environ.get('KAFKA_CLUSTER', '10.100.198.220:9092'),
+ backend=os.environ.get('BACKEND', 'none'),
+ retry_interval=os.environ.get('RETRY_INTERVAL', 2),
+ heartbeat_topic=os.environ.get('HEARTBEAT_TOPIC', "adapters.heartbeat"),
+)
+
+
+def parse_args():
+ parser = argparse.ArgumentParser()
+
+ _help = ('Path to openolt.yml config file (default: %s). '
+ 'If relative, it is relative to main.py of openolt adapter.'
+ % defs['config'])
+ parser.add_argument('-c', '--config',
+ dest='config',
+ action='store',
+ default=defs['config'],
+ help=_help)
+
+ _help = 'Regular expression for extracting conatiner number from ' \
+ 'container name (default: %s)' % defs['container_name_regex']
+ parser.add_argument('-X', '--container-number-extractor',
+ dest='container_name_regex',
+ action='store',
+ default=defs['container_name_regex'],
+ help=_help)
+
+ _help = '<hostname>:<port> to consul agent (default: %s)' % defs['consul']
+ parser.add_argument('-C', '--consul',
+ dest='consul',
+ action='store',
+ default=defs['consul'],
+ help=_help)
+
+ _help = 'name of this adapter (default: %s)' % defs['name']
+ parser.add_argument('-na', '--name',
+ dest='name',
+ action='store',
+ default=defs['name'],
+ help=_help)
+
+ _help = 'vendor of this adapter (default: %s)' % defs['vendor']
+ parser.add_argument('-ven', '--vendor',
+ dest='vendor',
+ action='store',
+ default=defs['vendor'],
+ help=_help)
+
+ _help = 'supported device type of this adapter (default: %s)' % defs[
+ 'device_type']
+ parser.add_argument('-dt', '--device_type',
+ dest='device_type',
+ action='store',
+ default=defs['device_type'],
+ help=_help)
+
+ _help = 'specifies whether the device type accepts bulk flow updates ' \
+ 'adapter (default: %s)' % defs['accept_bulk_flow']
+ parser.add_argument('-abf', '--accept_bulk_flow',
+ dest='accept_bulk_flow',
+ action='store',
+ default=defs['accept_bulk_flow'],
+ help=_help)
+
+ _help = 'specifies whether the device type accepts add/remove flow ' \
+ '(default: %s)' % defs['accept_atomic_flow']
+ parser.add_argument('-aaf', '--accept_atomic_flow',
+ dest='accept_atomic_flow',
+ action='store',
+ default=defs['accept_atomic_flow'],
+ help=_help)
+
+ _help = '<hostname>:<port> to etcd server (default: %s)' % defs['etcd']
+ parser.add_argument('-e', '--etcd',
+ dest='etcd',
+ action='store',
+ default=defs['etcd'],
+ help=_help)
+
+ _help = ('unique string id of this container instance (default: %s)'
+ % defs['instance_id'])
+ parser.add_argument('-i', '--instance-id',
+ dest='instance_id',
+ action='store',
+ default=defs['instance_id'],
+ help=_help)
+
+ _help = 'ETH interface to recieve (default: %s)' % defs['interface']
+ parser.add_argument('-I', '--interface',
+ dest='interface',
+ action='store',
+ default=defs['interface'],
+ help=_help)
+
+ _help = 'omit startup banner log lines'
+ parser.add_argument('-n', '--no-banner',
+ dest='no_banner',
+ action='store_true',
+ default=False,
+ help=_help)
+
+ _help = 'do not emit periodic heartbeat log messages'
+ parser.add_argument('-N', '--no-heartbeat',
+ dest='no_heartbeat',
+ action='store_true',
+ default=False,
+ help=_help)
+
+ _help = "suppress debug and info logs"
+ parser.add_argument('-q', '--quiet',
+ dest='quiet',
+ action='count',
+ help=_help)
+
+ _help = 'enable verbose logging'
+ parser.add_argument('-v', '--verbose',
+ dest='verbose',
+ action='count',
+ help=_help)
+
+ _help = ('use docker container name as conatiner instance id'
+ ' (overrides -i/--instance-id option)')
+ parser.add_argument('--instance-id-is-container-name',
+ dest='instance_id_is_container_name',
+ action='store_true',
+ default=False,
+ help=_help)
+
+ _help = ('<hostname>:<port> of the kafka adapter broker (default: %s). ('
+ 'If not '
+ 'specified (None), the address from the config file is used'
+ % defs['kafka_adapter'])
+ parser.add_argument('-KA', '--kafka_adapter',
+ dest='kafka_adapter',
+ action='store',
+ default=defs['kafka_adapter'],
+ help=_help)
+
+ _help = ('<hostname>:<port> of the kafka cluster broker (default: %s). ('
+ 'If not '
+ 'specified (None), the address from the config file is used'
+ % defs['kafka_cluster'])
+ parser.add_argument('-KC', '--kafka_cluster',
+ dest='kafka_cluster',
+ action='store',
+ default=defs['kafka_cluster'],
+ help=_help)
+
+ _help = 'backend to use for config persitence'
+ parser.add_argument('-b', '--backend',
+ default=defs['backend'],
+ choices=['none', 'consul', 'etcd'],
+ help=_help)
+
+ _help = 'topic of core on the kafka bus'
+ parser.add_argument('-ct', '--core_topic',
+ dest='core_topic',
+ action='store',
+ default=defs['core_topic'],
+ help=_help)
+
+ args = parser.parse_args()
+
+ # post-processing
+
+ if args.instance_id_is_container_name:
+ args.instance_id = get_my_containers_name()
+
+ return args
+
+
+def load_config(args):
+ path = args.config
+ if path.startswith('.'):
+ dir = os.path.dirname(os.path.abspath(__file__))
+ path = os.path.join(dir, path)
+ path = os.path.abspath(path)
+ with open(path) as fd:
+ config = yaml.load(fd)
+ return config
+
+
+
+
+def print_banner(log):
+ log.info(' ____ _____ ___ _ ___ ___ _ _____ ')
+ log.info('/ _ \| _ \| __| | / | / _ \| | |_ _| ')
+ log.info('| | | | | )_| | | | / /| | | | | | | | | ')
+ log.info('| |_| | __/ |_|_| |/ / | | |_| | |___| | ')
+ log.info('\____/|_| |___|_| |_| \___/|_____|_| ')
+ log.info(' ')
+ log.info(' _ _ _ ')
+ log.info(' / \ __| | __ _ _ __ | |_ ___ _ __ ')
+ log.info(' / _ \ / _` |/ _` | \'_ \| __/ _ \ \'__| ')
+ log.info(' / ___ \ (_| | (_| | |_) | || __/ | ')
+ log.info('/_/ \_\__,_|\__,_| .__/ \__\___|_| ')
+ log.info(' |_| ')
+ log.info('(to stop: press Ctrl-C)')
+
+@implementer(IComponent)
+class Main(object):
+
+ def __init__(self):
+
+ self.args = args = parse_args()
+ self.config = load_config(args)
+
+ verbosity_adjust = (args.verbose or 0) - (args.quiet or 0)
+ self.log = setup_logging(self.config.get('logging', {}),
+ args.instance_id,
+ verbosity_adjust=verbosity_adjust)
+ self.log.info('container-number-extractor',
+ regex=args.container_name_regex)
+
+ self.openolt_adapter_version = self.get_version()
+ self.log.info('Open-OLT-Adapter-Version', version=
+ self.openolt_adapter_version)
+
+ if not args.no_banner:
+ print_banner(self.log)
+
+ self.adapter = None
+ # Create a unique instance id using the passed-in instance id and
+ # UTC timestamp
+ current_time = arrow.utcnow().timestamp
+ self.instance_id = self.args.instance_id + '_' + str(current_time)
+
+ self.core_topic = args.core_topic
+ self.listening_topic = args.name
+ self.startup_components()
+
+ if not args.no_heartbeat:
+ self.start_heartbeat()
+ self.start_kafka_cluster_heartbeat(self.instance_id)
+
+ def get_version(self):
+ path = defs['version_file']
+ if not path.startswith('/'):
+ dir = os.path.dirname(os.path.abspath(__file__))
+ path = os.path.join(dir, path)
+
+ path = os.path.abspath(path)
+ version_file = open(path, 'r')
+ v = version_file.read()
+
+ # Use Version to validate the version string - exception will be raised
+ # if the version is invalid
+ Version(v)
+
+ version_file.close()
+ return v
+
+ def start(self):
+ self.start_reactor() # will not return except Keyboard interrupt
+
+ def stop(self):
+ pass
+
+ def get_args(self):
+ """Allow access to command line args"""
+ return self.args
+
+ def get_config(self):
+ """Allow access to content of config file"""
+ return self.config
+
+ def _get_adapter_config(self):
+ cfg = AdapterConfig()
+ return cfg
+
+ @inlineCallbacks
+ def startup_components(self):
+ try:
+ self.log.info('starting-internal-components',
+ consul=self.args.consul,
+ etcd=self.args.etcd)
+
+ registry.register('main', self)
+
+ # Update the logger to output the vcore id.
+ self.log = update_logging(instance_id=self.instance_id,
+ vcore_id=None)
+
+ yield registry.register(
+ 'kafka_cluster_proxy',
+ KafkaProxy(
+ self.args.consul,
+ self.args.kafka_cluster,
+ config=self.config.get('kafka-cluster-proxy', {})
+ )
+ ).start()
+
+ config = self._get_adapter_config()
+
+ self.core_proxy = CoreProxy(
+ kafka_proxy=None,
+ core_topic=self.core_topic,
+ my_listening_topic=self.listening_topic)
+
+ self.adapter_proxy = AdapterProxy(
+ kafka_proxy=None,
+ core_topic=self.core_topic,
+ my_listening_topic=self.listening_topic)
+
+ self.adapter = OpenoltAdapter(core_proxy=self.core_proxy,
+ adapter_proxy=self.adapter_proxy,
+ config=config)
+
+ openolt_request_handler = AdapterRequestFacade(adapter=self.adapter)
+
+ yield registry.register(
+ 'kafka_adapter_proxy',
+ IKafkaMessagingProxy(
+ kafka_host_port=self.args.kafka_adapter,
+ # TODO: Add KV Store object reference
+ kv_store=self.args.backend,
+ default_topic=self.args.name,
+ group_id_prefix=self.args.instance_id,
+ # Needs to assign a real class
+ target_cls=openolt_request_handler
+
+ )
+ ).start()
+
+ self.core_proxy.kafka_proxy = get_messaging_proxy()
+ self.adapter_proxy.kafka_proxy = get_messaging_proxy()
+
+ # retry for ever
+ res = yield self._register_with_core(-1)
+
+ self.log.info('started-internal-services')
+
+ except Exception as e:
+ self.log.exception('Failure-to-start-all-components', e=e)
+
+ @inlineCallbacks
+ def shutdown_components(self):
+ """Execute before the reactor is shut down"""
+ self.log.info('exiting-on-keyboard-interrupt')
+ for component in reversed(registry.iterate()):
+ yield component.stop()
+
+ import threading
+ self.log.info('THREADS:')
+ main_thread = threading.current_thread()
+ for t in threading.enumerate():
+ if t is main_thread:
+ continue
+ if not t.isDaemon():
+ continue
+ self.log.info('joining thread {} {}'.format(
+ t.getName(), "daemon" if t.isDaemon() else "not-daemon"))
+ t.join()
+
+ def start_reactor(self):
+ from twisted.internet import reactor
+ reactor.callWhenRunning(
+ lambda: self.log.info('twisted-reactor-started'))
+ reactor.addSystemEventTrigger('before', 'shutdown',
+ self.shutdown_components)
+ reactor.run()
+
+ @inlineCallbacks
+ def _register_with_core(self, retries):
+ while 1:
+ try:
+ resp = yield self.core_proxy.register(
+ self.adapter.adapter_descriptor(),
+ self.adapter.device_types())
+ if resp:
+ self.log.info('registered-with-core',
+ coreId=resp.instance_id)
+ returnValue(resp)
+ except TimeOutError as e:
+ self.log.warn("timeout-when-registering-with-core", e=e)
+ if retries == 0:
+ self.log.exception("no-more-retries", e=e)
+ raise
+ else:
+ retries = retries if retries < 0 else retries - 1
+ yield asleep(defs['retry_interval'])
+ except Exception as e:
+ self.log.exception("failed-registration", e=e)
+ raise
+
+ def start_heartbeat(self):
+
+ t0 = time.time()
+ t0s = time.ctime(t0)
+
+ def heartbeat():
+ self.log.debug(status='up', since=t0s, uptime=time.time() - t0)
+
+ lc = LoopingCall(heartbeat)
+ lc.start(10)
+
+ # Temporary function to send a heartbeat message to the external kafka
+ # broker
+ def start_kafka_cluster_heartbeat(self, instance_id):
+ # For heartbeat we will send a message to a specific "voltha-heartbeat"
+ # topic. The message is a protocol buf
+ # message
+ message = dict(
+ type='heartbeat',
+ adapter=self.args.name,
+ instance=instance_id,
+ ip=get_my_primary_local_ipv4()
+ )
+ topic = defs['heartbeat_topic']
+
+ def send_msg(start_time):
+ try:
+ kafka_cluster_proxy = get_kafka_proxy()
+ if kafka_cluster_proxy and not kafka_cluster_proxy.is_faulty():
+ # self.log.debug('kafka-proxy-available')
+ message['ts'] = arrow.utcnow().timestamp
+ message['uptime'] = time.time() - start_time
+ # self.log.debug('start-kafka-heartbeat')
+ kafka_cluster_proxy.send_message(topic, dumps(message))
+ else:
+ self.log.error('kafka-proxy-unavailable')
+ except Exception, e:
+ self.log.exception('failed-sending-message-heartbeat', e=e)
+
+ try:
+ t0 = time.time()
+ lc = LoopingCall(send_msg, t0)
+ lc.start(10)
+ except Exception, e:
+ self.log.exception('failed-kafka-heartbeat', e=e)
+
+
+if __name__ == '__main__':
+ Main().start()
diff --git a/python/adapters/openolt/openolt.py b/python/adapters/openolt/openolt.py
new file mode 100644
index 0000000..2c87730
--- /dev/null
+++ b/python/adapters/openolt/openolt.py
@@ -0,0 +1,897 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Openolt adapter.
+"""
+import arrow
+import grpc
+import structlog
+from google.protobuf.empty_pb2 import Empty
+from google.protobuf.json_format import MessageToDict
+from scapy.layers.inet import Raw
+import json
+from google.protobuf.message import Message
+from grpc._channel import _Rendezvous
+from scapy.layers.l2 import Ether, Dot1Q
+from simplejson import dumps
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.task import LoopingCall
+
+from python.adapters.common.frameio.frameio import BpfProgramFilter, hexify
+from python.adapters.iadapter import OltAdapter
+from python.common.utils.asleep import asleep
+from python.common.utils.registry import registry
+from python.adapters.kafka.kafka_proxy import get_kafka_proxy
+from python.protos import openolt_pb2
+from python.protos import third_party
+from python.protos.common_pb2 import OperStatus, ConnectStatus
+from python.protos.common_pb2 import LogLevel
+from python.protos.common_pb2 import OperationResp
+from python.protos.inter_container_pb2 import SwitchCapability, PortCapability, \
+ InterAdapterMessageType, InterAdapterResponseBody
+from python.protos.device_pb2 import Port, PmConfig, PmConfigs, \
+ DeviceType, DeviceTypes
+from python.protos.adapter_pb2 import Adapter
+from python.protos.adapter_pb2 import AdapterConfig
+
+
+from python.protos.events_pb2 import KpiEvent, KpiEventType, MetricValuePairs
+from python.protos.logical_device_pb2 import LogicalPort
+from python.protos.openflow_13_pb2 import OFPPS_LIVE, OFPPF_FIBER, \
+ OFPPF_1GB_FD, \
+ OFPC_GROUP_STATS, OFPC_PORT_STATS, OFPC_TABLE_STATS, OFPC_FLOW_STATS, \
+ ofp_switch_features, ofp_desc
+from python.protos.openflow_13_pb2 import ofp_port
+from python.protos.ponsim_pb2 import FlowTable, PonSimFrame, PonSimMetricsRequest, PonSimStub
+
+_ = third_party
+log = structlog.get_logger()
+#OpenOltDefaults = {
+# 'support_classes': {
+# 'platform': OpenOltPlatform,
+# 'resource_mgr': OpenOltResourceMgr,
+# 'flow_mgr': OpenOltFlowMgr,
+# 'alarm_mgr': OpenOltAlarmMgr,
+# 'stats_mgr': OpenOltStatisticsMgr,
+# 'bw_mgr': OpenOltBW
+# }
+#}
+
+class AdapterPmMetrics:
+ def __init__(self, device):
+ self.pm_names = {'tx_64_pkts', 'tx_65_127_pkts', 'tx_128_255_pkts',
+ 'tx_256_511_pkts', 'tx_512_1023_pkts',
+ 'tx_1024_1518_pkts', 'tx_1519_9k_pkts',
+ 'rx_64_pkts', 'rx_65_127_pkts',
+ 'rx_128_255_pkts', 'rx_256_511_pkts',
+ 'rx_512_1023_pkts', 'rx_1024_1518_pkts',
+ 'rx_1519_9k_pkts'}
+ self.device = device
+ self.id = device.id
+ self.name = 'ponsim_olt'
+ self.default_freq = 150
+ self.grouped = False
+ self.freq_override = False
+ self.pon_metrics_config = dict()
+ self.nni_metrics_config = dict()
+ self.lc = None
+ for m in self.pm_names:
+ self.pon_metrics_config[m] = PmConfig(name=m,
+ type=PmConfig.COUNTER,
+ enabled=True)
+ self.nni_metrics_config[m] = PmConfig(name=m,
+ type=PmConfig.COUNTER,
+ enabled=True)
+
+ def update(self, pm_config):
+ if self.default_freq != pm_config.default_freq:
+ # Update the callback to the new frequency.
+ self.default_freq = pm_config.default_freq
+ self.lc.stop()
+ self.lc.start(interval=self.default_freq / 10)
+ for m in pm_config.metrics:
+ self.pon_metrics_config[m.name].enabled = m.enabled
+ self.nni_metrics_config[m.name].enabled = m.enabled
+
+ def make_proto(self):
+ pm_config = PmConfigs(
+ id=self.id,
+ default_freq=self.default_freq,
+ grouped=False,
+ freq_override=False)
+ for m in sorted(self.pon_metrics_config):
+ pm = self.pon_metrics_config[m] # Either will do they're the same
+ pm_config.metrics.extend([PmConfig(name=pm.name,
+ type=pm.type,
+ enabled=pm.enabled)])
+ return pm_config
+
+ def collect_port_metrics(self, channel):
+ rtrn_port_metrics = dict()
+ stub = ponsim_pb2.PonSimStub(channel)
+ stats = stub.GetStats(Empty())
+ rtrn_port_metrics['pon'] = self.extract_pon_metrics(stats)
+ rtrn_port_metrics['nni'] = self.extract_nni_metrics(stats)
+ return rtrn_port_metrics
+
+ def extract_pon_metrics(self, stats):
+ rtrn_pon_metrics = dict()
+ for m in stats.metrics:
+ if m.port_name == "pon":
+ for p in m.packets:
+ if self.pon_metrics_config[p.name].enabled:
+ rtrn_pon_metrics[p.name] = p.value
+ return rtrn_pon_metrics
+
+ def extract_nni_metrics(self, stats):
+ rtrn_pon_metrics = dict()
+ for m in stats.metrics:
+ if m.port_name == "nni":
+ for p in m.packets:
+ if self.pon_metrics_config[p.name].enabled:
+ rtrn_pon_metrics[p.name] = p.value
+ return rtrn_pon_metrics
+
+ def start_collector(self, callback):
+ log.info("starting-pm-collection", device_name=self.name,
+ device_id=self.device.id)
+ prefix = 'voltha.{}.{}'.format(self.name, self.device.id)
+ self.lc = LoopingCall(callback, self.device.id, prefix)
+ self.lc.start(interval=self.default_freq / 10)
+
+ def stop_collector(self):
+ log.info("stopping-pm-collection", device_name=self.name,
+ device_id=self.device.id)
+ self.lc.stop()
+
+
+class AdapterAlarms:
+ def __init__(self, adapter, device):
+ self.adapter = adapter
+ self.device = device
+ self.lc = None
+
+ # TODO: Implement code to send to kafka cluster directly instead of
+ # going through the voltha core.
+ def send_alarm(self, context_data, alarm_data):
+ log.debug("send-alarm-not-implemented")
+ return
+
+
+
+class OpenoltAdapter(OltAdapter):
+ name = 'openolt'
+
+ supported_device_types = [
+ DeviceType(
+ id=name,
+ adapter=name,
+ accepts_bulk_flow_update=True,
+ accepts_add_remove_flow_updates=True
+ )
+ ]
+
+ # System Init Methods #
+ def __init__(self, core_proxy, adapter_proxy, config):
+ super(OpenoltAdapter, self).__init__(core_proxy=core_proxy,
+ adapter_proxy=adapter_proxy,
+ config=config,
+ device_handler_class=OpenoltHandler,
+ name='openolt',
+ vendor='Voltha project',
+ version='0.4',
+ device_type='openolt',
+ accepts_bulk_flow_update=True,
+ accepts_add_remove_flow_updates=False)
+ self.adapter_proxy = adapter_proxy
+ self.core_proxy = core_proxy
+ self.config = config
+ self.descriptor = Adapter(
+ id=self.name,
+ vendor='OLT white box vendor',
+ version='0.1',
+ config=config
+ )
+ log.debug('openolt.__init__', adapter_proxy=adapter_proxy)
+ self.devices = dict() # device_id -> OpenoltDevice()
+ self.interface = registry('main').get_args().interface
+ self.logical_device_id_to_root_device_id = dict()
+ self.num_devices = 0
+
+ def start(self):
+ log.info('started', interface=self.interface)
+
+ def stop(self):
+ log.info('stopped', interface=self.interface)
+
+
+ # Info Methods #
+ def adapter_descriptor(self):
+ log.debug('get descriptor', interface=self.interface)
+ return self.descriptor
+
+ def device_types(self):
+ log.debug('get device_types', interface=self.interface,
+ items=self.supported_device_types)
+ return DeviceTypes(items=self.supported_device_types)
+
+ def health(self):
+ log.debug('get health', interface=self.interface)
+ raise NotImplementedError()
+
+ def get_device_details(self, device):
+ log.debug('get_device_details', device=device)
+ raise NotImplementedError()
+
+
+ # Device Operation Methods #
+ def change_master_state(self, master):
+ log.debug('change_master_state', interface=self.interface,
+ master=master)
+ raise NotImplementedError()
+
+ def abandon_device(self, device):
+ log.info('abandon-device', device=device)
+ raise NotImplementedError()
+
+
+ # Configuration Methods #
+ def update_flows_incrementally(self, device, flow_changes, group_changes):
+ log.debug('update_flows_incrementally', device=device,
+ flow_changes=flow_changes, group_changes=group_changes)
+ log.info('This device does not allow this, therefore it is Not '
+ 'implemented')
+ raise NotImplementedError()
+
+ def update_pm_config(self, device, pm_configs):
+ log.info('update_pm_config - Not implemented yet', device=device,
+ pm_configs=pm_configs)
+ raise NotImplementedError()
+
+ def receive_proxied_message(self, proxy_address, msg):
+ log.debug('receive_proxied_message - Not implemented',
+ proxy_address=proxy_address,
+ proxied_msg=msg)
+ raise NotImplementedError()
+
+ def receive_inter_adapter_message(self, msg):
+ log.info('rx_inter_adapter_msg - Not implemented')
+ raise NotImplementedError()
+
+
+ # Image Operations Methods #
+ def download_image(self, device, request):
+ log.info('image_download - Not implemented yet', device=device,
+ request=request)
+ raise NotImplementedError()
+
+ def get_image_download_status(self, device, request):
+ log.info('get_image_download - Not implemented yet', device=device,
+ request=request)
+ raise NotImplementedError()
+
+ def cancel_image_download(self, device, request):
+ log.info('cancel_image_download - Not implemented yet', device=device)
+ raise NotImplementedError()
+
+ def activate_image_update(self, device, request):
+ log.info('activate_image_update - Not implemented yet',
+ device=device, request=request)
+ raise NotImplementedError()
+
+ def revert_image_update(self, device, request):
+ log.info('revert_image_update - Not implemented yet',
+ device=device, request=request)
+ raise NotImplementedError()
+
+ def self_test_device(self, device):
+ # from voltha.protos.voltha_pb2 import SelfTestResponse
+ log.info('Not implemented yet')
+ raise NotImplementedError()
+
+
+ # PON Operations Methods #
+ def create_interface(self, device, data):
+ log.debug('create-interface - Not implemented - We do not use this',
+ data=data)
+ raise NotImplementedError()
+
+ def update_interface(self, device, data):
+ log.debug('update-interface - Not implemented - We do not use this',
+ data=data)
+ raise NotImplementedError()
+
+ def remove_interface(self, device, data):
+ log.debug('remove-interface - Not implemented - We do not use this',
+ data=data)
+ raise NotImplementedError()
+
+ def receive_onu_detect_state(self, proxy_address, state):
+ log.debug('receive-onu-detect-state - Not implemented - We do not '
+ 'use this', proxy_address=proxy_address,
+ state=state)
+ raise NotImplementedError()
+
+ def create_tcont(self, device, tcont_data, traffic_descriptor_data):
+ log.info('create-tcont - Not implemented - We do not use this',
+ tcont_data=tcont_data,
+ traffic_descriptor_data=traffic_descriptor_data)
+ raise NotImplementedError()
+
+ def update_tcont(self, device, tcont_data, traffic_descriptor_data):
+ log.info('update-tcont - Not implemented - We do not use this',
+ tcont_data=tcont_data,
+ traffic_descriptor_data=traffic_descriptor_data)
+ raise NotImplementedError()
+
+ def remove_tcont(self, device, tcont_data, traffic_descriptor_data):
+ log.info('remove-tcont - Not implemented - We do not use this',
+ tcont_data=tcont_data,
+ traffic_descriptor_data=traffic_descriptor_data)
+ raise NotImplementedError()
+
+ def create_gemport(self, device, data):
+ log.info('create-gemport - Not implemented - We do not use this',
+ data=data)
+ raise NotImplementedError()
+
+ def update_gemport(self, device, data):
+ log.info('update-gemport - Not implemented - We do not use this',
+ data=data)
+ raise NotImplementedError()
+
+ def remove_gemport(self, device, data):
+ log.info('remove-gemport - Not implemented - We do not use this',
+ data=data)
+ raise NotImplementedError()
+
+ def create_multicast_gemport(self, device, data):
+ log.info('create-mcast-gemport - Not implemented - We do not use '
+ 'this', data=data)
+ raise NotImplementedError()
+
+ def update_multicast_gemport(self, device, data):
+ log.info('update-mcast-gemport - Not implemented - We do not use '
+ 'this', data=data)
+ raise NotImplementedError()
+
+ def remove_multicast_gemport(self, device, data):
+ log.info('remove-mcast-gemport - Not implemented - We do not use '
+ 'this', data=data)
+ raise NotImplementedError()
+
+ def create_multicast_distribution_set(self, device, data):
+ log.info('create-mcast-distribution-set - Not implemented - We do '
+ 'not use this', data=data)
+ raise NotImplementedError()
+
+ def update_multicast_distribution_set(self, device, data):
+ log.info('update-mcast-distribution-set - Not implemented - We do '
+ 'not use this', data=data)
+ raise NotImplementedError()
+
+ def remove_multicast_distribution_set(self, device, data):
+ log.info('remove-mcast-distribution-set - Not implemented - We do '
+ 'not use this', data=data)
+ raise NotImplementedError()
+
+
+ # Alarm Methods #
+ def suppress_alarm(self, filter):
+ log.info('suppress_alarm - Not implemented yet', filter=filter)
+ raise NotImplementedError()
+
+ def unsuppress_alarm(self, filter):
+ log.info('unsuppress_alarm - Not implemented yet', filter=filter)
+ raise NotImplementedError()
+
+class OpenoltHandler(object):
+ def __init__(self, adapter, device_id):
+ self.adapter = adapter
+ self.core_proxy = adapter.core_proxy
+ self.adapter_proxy = adapter.adapter_proxy
+ self.device_id = device_id
+ self.log = structlog.get_logger(device_id=device_id)
+ self.channel = None
+ self.io_port = None
+ self.logical_device_id = None
+ self.nni_port = None
+ self.ofp_port_no = None
+ self.interface = registry('main').get_args().interface
+ self.pm_metrics = None
+ self.alarms = None
+ self.frames = None
+
+ @inlineCallbacks
+ def get_channel(self):
+ if self.channel is None:
+ try:
+ device = yield self.core_proxy.get_device(self.device_id)
+ self.log.info('device-info', device=device,
+ host_port=device.host_and_port)
+ self.channel = grpc.insecure_channel(device.host_and_port)
+ except Exception as e:
+ log.exception("ponsim-connection-failure", e=e)
+
+ # returnValue(self.channel)
+
+ def close_channel(self):
+ if self.channel is None:
+ self.log.info('grpc-channel-already-closed')
+ return
+ else:
+ if self.frames is not None:
+ self.frames.cancel()
+ self.frames = None
+ self.log.info('cancelled-grpc-frame-stream')
+
+ self.channel.unsubscribe(lambda *args: None)
+ self.channel = None
+
+ self.log.info('grpc-channel-closed')
+
+ @inlineCallbacks
+ def _get_nni_port(self):
+ ports = yield self.core_proxy.get_ports(self.device_id,
+ Port.ETHERNET_NNI)
+ returnValue(ports)
+
+ @inlineCallbacks
+ def activate(self, device):
+ try:
+ self.log.info('activating')
+ print (dir(device))
+ if not device.host_and_port:
+ device.oper_status = OperStatus.FAILED
+ device.reason = 'No host_and_port field provided'
+ self.core_proxy.device_update(device)
+ return
+ """
+ kwargs = {
+ 'support_classes': OpenOltDefaults['support_classes'],
+ 'adapter_agent': self.adapter_proxy,
+ 'device': device,
+ 'device_num': self.num_devices + 1
+ }
+ try:
+ self.devices[device.id] = OpenoltDevice(**kwargs)
+ except Exception as e:
+ log.error('Failed to adopt OpenOLT device', error=e)
+ # TODO set status to ERROR so that is clear something went wrong
+ del self.devices[device.id]
+ raise
+ else:
+ self.num_devices += 1
+
+ """
+ yield self.get_channel()
+ stub = PonSimStub(self.channel)
+ info = stub.GetDeviceInfo(Empty())
+ log.info('got-info', info=info, device_id=device.id)
+ self.ofp_port_no = info.nni_port
+
+ device.root = True
+ device.vendor = 'ponsim'
+ device.model = 'n/a'
+ device.serial_number = device.host_and_port
+ device.mac_address = "AA:BB:CC:DD:EE:FF"
+ yield self.core_proxy.device_update(device)
+
+ # Now set the initial PM configuration for this device
+ self.pm_metrics = AdapterPmMetrics(device)
+ pm_config = self.pm_metrics.make_proto()
+ log.info("initial-pm-config", pm_config=pm_config)
+ self.core_proxy.device_pm_config_update(pm_config, init=True)
+
+ # Setup alarm handler
+ self.alarms = AdapterAlarms(self.adapter, device)
+
+ nni_port = Port(
+ port_no=info.nni_port,
+ label='NNI facing Ethernet port',
+ type=Port.ETHERNET_NNI,
+ oper_status=OperStatus.ACTIVE
+ )
+ self.nni_port = nni_port
+ yield self.core_proxy.port_created(device.id, nni_port)
+ yield self.core_proxy.port_created(device.id, Port(
+ port_no=1,
+ label='PON port',
+ type=Port.PON_OLT,
+ oper_status=OperStatus.ACTIVE
+ ))
+
+ yield self.core_proxy.device_state_update(device.id,
+ connect_status=ConnectStatus.REACHABLE,
+ oper_status=OperStatus.ACTIVE)
+
+ # register ONUS
+ self.log.info('onu-found', onus=info.onus, len=len(info.onus))
+ for onu in info.onus:
+ vlan_id = onu.uni_port
+ yield self.core_proxy.child_device_detected(
+ parent_device_id=device.id,
+ parent_port_no=1,
+ child_device_type='ponsim_onu',
+ channel_id=vlan_id,
+ )
+
+ self.log.info('starting-frame-grpc-stream')
+ reactor.callInThread(self.rcv_grpc)
+ self.log.info('started-frame-grpc-stream')
+
+ # Start collecting stats from the device after a brief pause
+ self.start_kpi_collection(device.id)
+ except Exception as e:
+ log.exception("Exception-activating", e=e)
+
+ def get_ofp_device_info(self, device):
+ return SwitchCapability(
+ desc=ofp_desc(
+ hw_desc='ponsim pon',
+ sw_desc='ponsim pon',
+ serial_num=device.serial_number,
+ dp_desc='n/a'
+ ),
+ switch_features=ofp_switch_features(
+ n_buffers=256, # TODO fake for now
+ n_tables=2, # TODO ditto
+ capabilities=( # TODO and ditto
+ OFPC_FLOW_STATS
+ | OFPC_TABLE_STATS
+ | OFPC_PORT_STATS
+ | OFPC_GROUP_STATS
+ )
+ )
+ )
+
+ def get_ofp_port_info(self, device, port_no):
+ # Since the adapter created the device port then it has the reference of the port to
+ # return the capability. TODO: Do a lookup on the NNI port number and return the
+ # appropriate attributes
+ self.log.info('get_ofp_port_info', port_no=port_no,
+ info=self.ofp_port_no, device_id=device.id)
+ cap = OFPPF_1GB_FD | OFPPF_FIBER
+ return PortCapability(
+ port=LogicalPort(
+ ofp_port=ofp_port(
+ hw_addr=mac_str_to_tuple(
+ 'AA:BB:CC:DD:EE:%02x' % port_no),
+ config=0,
+ state=OFPPS_LIVE,
+ curr=cap,
+ advertised=cap,
+ peer=cap,
+ curr_speed=OFPPF_1GB_FD,
+ max_speed=OFPPF_1GB_FD
+ ),
+ device_id=device.id,
+ device_port_no=port_no
+ )
+ )
+
+ # TODO - change for core 2.0
+ def reconcile(self, device):
+ self.log.info('reconciling-OLT-device')
+
+ @inlineCallbacks
+ def _rcv_frame(self, frame):
+ pkt = Ether(frame)
+
+ if pkt.haslayer(Dot1Q):
+ outer_shim = pkt.getlayer(Dot1Q)
+
+ if isinstance(outer_shim.payload, Dot1Q):
+ inner_shim = outer_shim.payload
+ cvid = inner_shim.vlan
+ popped_frame = (
+ Ether(src=pkt.src, dst=pkt.dst, type=inner_shim.type) /
+ inner_shim.payload
+ )
+ self.log.info('sending-packet-in',device_id=self.device_id, port=cvid)
+ yield self.core_proxy.send_packet_in(device_id=self.device_id,
+ port=cvid,
+ packet=str(popped_frame))
+ elif pkt.haslayer(Raw):
+ raw_data = json.loads(pkt.getlayer(Raw).load)
+ self.alarms.send_alarm(self, raw_data)
+
+ @inlineCallbacks
+ def rcv_grpc(self):
+ """
+ This call establishes a GRPC stream to receive frames.
+ """
+ yield self.get_channel()
+ stub = PonSimStub(self.channel)
+ # stub = PonSimStub(self.get_channel())
+
+ # Attempt to establish a grpc stream with the remote ponsim service
+ self.frames = stub.ReceiveFrames(Empty())
+
+ self.log.info('start-receiving-grpc-frames')
+
+ try:
+ for frame in self.frames:
+ self.log.info('received-grpc-frame',
+ frame_len=len(frame.payload))
+ yield self._rcv_frame(frame.payload)
+
+ except _Rendezvous, e:
+ log.warn('grpc-connection-lost', message=e.message)
+
+ self.log.info('stopped-receiving-grpc-frames')
+
+ @inlineCallbacks
+ def update_flow_table(self, flows):
+ yield self.get_channel()
+ stub = PonSimStub(self.channel)
+
+ self.log.info('pushing-olt-flow-table')
+ stub.UpdateFlowTable(FlowTable(
+ port=0,
+ flows=flows
+ ))
+ self.log.info('success')
+
+ def remove_from_flow_table(self, flows):
+ self.log.debug('remove-from-flow-table', flows=flows)
+ # TODO: Update PONSIM code to accept incremental flow changes
+ # Once completed, the accepts_add_remove_flow_updates for this
+ # device type can be set to True
+
+ def add_to_flow_table(self, flows):
+ self.log.debug('add-to-flow-table', flows=flows)
+ # TODO: Update PONSIM code to accept incremental flow changes
+ # Once completed, the accepts_add_remove_flow_updates for this
+ # device type can be set to True
+
+ def update_pm_config(self, device, pm_config):
+ log.info("handler-update-pm-config", device=device,
+ pm_config=pm_config)
+ self.pm_metrics.update(pm_config)
+
+ def send_proxied_message(self, proxy_address, msg):
+ self.log.info('sending-proxied-message')
+ if isinstance(msg, FlowTable):
+ stub = PonSimStub(self.get_channel())
+ self.log.info('pushing-onu-flow-table', port=msg.port)
+ res = stub.UpdateFlowTable(msg)
+ self.core_proxy.receive_proxied_message(proxy_address, res)
+
+ @inlineCallbacks
+ def process_inter_adapter_message(self, request):
+ self.log.info('process-inter-adapter-message', msg=request)
+ try:
+ if request.header.type == InterAdapterMessageType.FLOW_REQUEST:
+ f = FlowTable()
+ if request.body:
+ request.body.Unpack(f)
+ stub = PonSimStub(self.channel)
+ self.log.info('pushing-onu-flow-table')
+ res = stub.UpdateFlowTable(f)
+ # Send response back
+ reply = InterAdapterResponseBody()
+ reply.status = True
+ self.log.info('sending-response-back', reply=reply)
+ yield self.adapter_proxy.send_inter_adapter_message(
+ msg=reply,
+ type=InterAdapterMessageType.FLOW_RESPONSE,
+ from_adapter=self.adapter.name,
+ to_adapter=request.header.from_topic,
+ to_device_id=request.header.to_device_id,
+ message_id=request.header.id
+ )
+ elif request.header.type == InterAdapterMessageType.METRICS_REQUEST:
+ m = PonSimMetricsRequest()
+ if request.body:
+ request.body.Unpack(m)
+ stub = PonSimStub(self.channel)
+ self.log.info('proxying onu stats request', port=m.port)
+ res = stub.GetStats(m)
+ # Send response back
+ reply = InterAdapterResponseBody()
+ reply.status = True
+ reply.body.Pack(res)
+ self.log.info('sending-response-back', reply=reply)
+ yield self.adapter_proxy.send_inter_adapter_message(
+ msg=reply,
+ type=InterAdapterMessageType.METRICS_RESPONSE,
+ from_adapter=self.adapter.name,
+ to_adapter=request.header.from_topic,
+ to_device_id=request.header.to_device_id,
+ message_id=request.header.id
+ )
+ except Exception as e:
+ self.log.exception("error-processing-inter-adapter-message", e=e)
+
+ def packet_out(self, egress_port, msg):
+ self.log.info('sending-packet-out', egress_port=egress_port,
+ msg=hexify(msg))
+ try:
+ pkt = Ether(msg)
+ out_pkt = pkt
+ if egress_port != self.nni_port.port_no:
+ # don't do the vlan manipulation for the NNI port, vlans are already correct
+ out_pkt = (
+ Ether(src=pkt.src, dst=pkt.dst) /
+ Dot1Q(vlan=egress_port, type=pkt.type) /
+ pkt.payload
+ )
+
+ # TODO need better way of mapping logical ports to PON ports
+ out_port = self.nni_port.port_no if egress_port == self.nni_port.port_no else 1
+
+ # send over grpc stream
+ stub = PonSimStub(self.channel)
+ frame = PonSimFrame(id=self.device_id, payload=str(out_pkt),
+ out_port=out_port)
+ stub.SendFrame(frame)
+ except Exception as e:
+ self.log.exception("error-processing-packet-out", e=e)
+
+
+ @inlineCallbacks
+ def reboot(self):
+ self.log.info('rebooting', device_id=self.device_id)
+
+ yield self.core_proxy.device_state_update(self.device_id,
+ connect_status=ConnectStatus.UNREACHABLE)
+
+ # Update the child devices connect state to UNREACHABLE
+ yield self.core_proxy.children_state_update(self.device_id,
+ connect_status=ConnectStatus.UNREACHABLE)
+
+ # Sleep 10 secs, simulating a reboot
+ # TODO: send alert and clear alert after the reboot
+ yield asleep(10)
+
+ # Change the connection status back to REACHABLE. With a
+ # real OLT the connection state must be the actual state
+ yield self.core_proxy.device_state_update(self.device_id,
+ connect_status=ConnectStatus.REACHABLE)
+
+ # Update the child devices connect state to REACHABLE
+ yield self.core_proxy.children_state_update(self.device_id,
+ connect_status=ConnectStatus.REACHABLE)
+
+ self.log.info('rebooted', device_id=self.device_id)
+
+ def self_test_device(self, device):
+ """
+ This is called to Self a device based on a NBI call.
+ :param device: A Voltha.Device object.
+ :return: Will return result of self test
+ """
+ log.info('self-test-device', device=device.id)
+ raise NotImplementedError()
+
+ @inlineCallbacks
+ def disable(self):
+ self.log.info('disabling', device_id=self.device_id)
+
+ self.stop_kpi_collection()
+
+ # Update the operational status to UNKNOWN and connection status to UNREACHABLE
+ yield self.core_proxy.device_state_update(self.device_id,
+ oper_status=OperStatus.UNKNOWN,
+ connect_status=ConnectStatus.UNREACHABLE)
+
+ self.close_channel()
+ self.log.info('disabled-grpc-channel')
+
+ self.stop_kpi_collection()
+
+ # TODO:
+ # 1) Remove all flows from the device
+ # 2) Remove the device from ponsim
+
+ self.log.info('disabled', device_id=self.device_id)
+
+ @inlineCallbacks
+ def reenable(self):
+ self.log.info('re-enabling', device_id=self.device_id)
+
+ # Set the ofp_port_no and nni_port in case we bypassed the reconcile
+ # process if the device was in DISABLED state on voltha restart
+ if not self.ofp_port_no and not self.nni_port:
+ yield self.get_channel()
+ stub = PonSimStub(self.channel)
+ info = stub.GetDeviceInfo(Empty())
+ log.info('got-info', info=info)
+ self.ofp_port_no = info.nni_port
+ ports = yield self._get_nni_port()
+ # For ponsim, we are using only 1 NNI port
+ if ports.items:
+ self.nni_port = ports.items[0]
+
+ # Update the state of the NNI port
+ yield self.core_proxy.port_state_update(self.device_id,
+ port_type=Port.ETHERNET_NNI,
+ port_no=self.ofp_port_no,
+ oper_status=OperStatus.ACTIVE)
+
+ # Update the state of the PON port
+ yield self.core_proxy.port_state_update(self.device_id,
+ port_type=Port.PON_OLT,
+ port_no=1,
+ oper_status=OperStatus.ACTIVE)
+
+ # Set the operational state of the device to ACTIVE and connect status to REACHABLE
+ yield self.core_proxy.device_state_update(self.device_id,
+ connect_status=ConnectStatus.REACHABLE,
+ oper_status=OperStatus.ACTIVE)
+
+ # TODO: establish frame grpc-stream
+ # yield reactor.callInThread(self.rcv_grpc)
+
+ self.start_kpi_collection(self.device_id)
+
+ self.log.info('re-enabled', device_id=self.device_id)
+
+ def delete(self):
+ self.log.info('deleting', device_id=self.device_id)
+
+ self.close_channel()
+ self.log.info('disabled-grpc-channel')
+
+ # TODO:
+ # 1) Remove all flows from the device
+ # 2) Remove the device from ponsim
+
+ self.log.info('deleted', device_id=self.device_id)
+
+ def start_kpi_collection(self, device_id):
+
+ kafka_cluster_proxy = get_kafka_proxy()
+
+ def _collect(device_id, prefix):
+
+ try:
+ # Step 1: gather metrics from device
+ port_metrics = \
+ self.pm_metrics.collect_port_metrics(self.channel)
+
+ # Step 2: prepare the KpiEvent for submission
+ # we can time-stamp them here (or could use time derived from OLT
+ ts = arrow.utcnow().timestamp
+ kpi_event = KpiEvent(
+ type=KpiEventType.slice,
+ ts=ts,
+ prefixes={
+ # OLT NNI port
+ prefix + '.nni': MetricValuePairs(
+ metrics=port_metrics['nni']),
+ # OLT PON port
+ prefix + '.pon': MetricValuePairs(
+ metrics=port_metrics['pon'])
+ }
+ )
+
+ # Step 3: submit directly to the kafka bus
+ if kafka_cluster_proxy:
+ if isinstance(kpi_event, Message):
+ kpi_event = dumps(MessageToDict(kpi_event, True, True))
+ kafka_cluster_proxy.send_message("voltha.kpis", kpi_event)
+
+ except Exception as e:
+ log.exception('failed-to-submit-kpis', e=e)
+
+ self.pm_metrics.start_collector(_collect)
+
+ def stop_kpi_collection(self):
+ self.pm_metrics.stop_collector()
diff --git a/python/adapters/openolt/openolt.yml b/python/adapters/openolt/openolt.yml
new file mode 100644
index 0000000..14f63bb
--- /dev/null
+++ b/python/adapters/openolt/openolt.yml
@@ -0,0 +1,67 @@
+---
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+logging:
+ version: 1
+
+ formatters:
+ brief:
+ format: '%(message)s'
+ default:
+ format: '%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(module)s.%(funcName)s %(message)s'
+ datefmt: '%Y%m%dT%H%M%S'
+
+ handlers:
+ console:
+ class : logging.StreamHandler
+ level: DEBUG
+ formatter: default
+ stream: ext://sys.stdout
+ localRotatingFile:
+ class: logging.handlers.RotatingFileHandler
+ filename: openolt.log
+ formatter: default
+ maxBytes: 2097152
+ backupCount: 10
+ level: DEBUG
+ null:
+ class: logging.NullHandler
+
+ loggers:
+ amqp:
+ handlers: [null]
+ propagate: False
+ conf:
+ propagate: False
+ '': # root logger
+ handlers: [console, localRotatingFile]
+ level: DEBUG # this can be bumped up/down by -q and -v command line
+ # options
+ propagate: False
+
+
+kafka-cluster-proxy:
+ event_bus_publisher:
+ topic_mappings:
+ 'model-change-events':
+ kafka_topic: 'voltha.events'
+ filters: [null]
+ 'alarms':
+ kafka_topic: 'voltha.alarms'
+ filters: [null]
+ 'kpis':
+ kafka_topic: 'voltha.kpis'
+ filters: [null]
+
diff --git a/python/adapters/openolt/openolt_alarms.py b/python/adapters/openolt/openolt_alarms.py
new file mode 100644
index 0000000..764a013
--- /dev/null
+++ b/python/adapters/openolt/openolt_alarms.py
@@ -0,0 +1,466 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import arrow
+from voltha.extensions.alarms.adapter_alarms import AdapterAlarms
+from voltha.extensions.alarms.simulator.simulate_alarms import AdapterAlarmSimulator
+from voltha.extensions.alarms.olt.olt_los_alarm import OltLosAlarm
+from voltha.extensions.alarms.onu.onu_dying_gasp_alarm import OnuDyingGaspAlarm
+from voltha.extensions.alarms.onu.onu_los_alarm import OnuLosAlarm
+from voltha.extensions.alarms.onu.onu_lopc_miss_alarm import OnuLopcMissAlarm
+from voltha.extensions.alarms.onu.onu_lopc_mic_error_alarm import OnuLopcMicErrorAlarm
+from voltha.extensions.alarms.onu.onu_lob_alarm import OnuLobAlarm
+
+from voltha.extensions.alarms.onu.onu_startup_alarm import OnuStartupAlarm
+from voltha.extensions.alarms.onu.onu_signal_degrade_alarm import OnuSignalDegradeAlarm
+from voltha.extensions.alarms.onu.onu_signal_fail_alarm import OnuSignalFailAlarm
+from voltha.extensions.alarms.onu.onu_window_drift_alarm import OnuWindowDriftAlarm
+from voltha.extensions.alarms.onu.onu_activation_fail_alarm import OnuActivationFailAlarm
+
+import protos.openolt_pb2 as openolt_pb2
+import voltha.protos.device_pb2 as device_pb2
+
+
+class OpenOltAlarmMgr(object):
+ def __init__(self, log, adapter_agent, device_id, logical_device_id,
+ platform):
+ """
+ 20180711 - Addition of adapter_agent and device_id
+ to facilitate alarm processing and kafka posting
+ :param log:
+ :param adapter_agent:
+ :param device_id:
+ """
+ self.log = log
+ self.adapter_agent = adapter_agent
+ self.device_id = device_id
+ self.logical_device_id = logical_device_id
+ self.platform = platform
+ """
+ The following is added to reduce the continual posting of OLT LOS alarming
+ to Kafka. Set enable_alarm_suppress = true to enable otherwise the
+ current openolt bal will send continuous olt los alarm cleared messages
+ ONU disc raised counter is place holder for a future addition
+ """
+ self.enable_alarm_suppress = True
+ self.alarm_suppress = {"olt_los_clear": 0, "onu_disc_raised": []} # Keep count of alarms to limit.
+ try:
+ self.alarms = AdapterAlarms(self.adapter_agent, self.device_id, self.logical_device_id)
+ self.simulator = AdapterAlarmSimulator(self.alarms)
+ except Exception as initerr:
+ self.log.exception("alarmhandler-init-error", errmsg=initerr.message)
+ raise Exception(initerr)
+
+ def process_alarms(self, alarm_ind):
+ try:
+ self.log.debug('alarm-indication', alarm=alarm_ind, device_id=self.device_id)
+ if alarm_ind.HasField('los_ind'):
+ self.los_indication(alarm_ind.los_ind)
+ elif alarm_ind.HasField('dying_gasp_ind'):
+ self.dying_gasp_indication(alarm_ind.dying_gasp_ind)
+ elif alarm_ind.HasField('onu_alarm_ind'):
+ self.onu_alarm_indication(alarm_ind.onu_alarm_ind)
+ elif alarm_ind.HasField('onu_startup_fail_ind'):
+ self.onu_startup_failure_indication(
+ alarm_ind.onu_startup_fail_ind)
+ elif alarm_ind.HasField('onu_signal_degrade_ind'):
+ self.onu_signal_degrade_indication(
+ alarm_ind.onu_signal_degrade_ind)
+ elif alarm_ind.HasField('onu_drift_of_window_ind'):
+ self.onu_drift_of_window_indication(
+ alarm_ind.onu_drift_of_window_ind)
+ elif alarm_ind.HasField('onu_loss_omci_ind'):
+ self.onu_loss_omci_indication(alarm_ind.onu_loss_omci_ind)
+ elif alarm_ind.HasField('onu_signals_fail_ind'):
+ self.onu_signals_failure_indication(
+ alarm_ind.onu_signals_fail_ind)
+ elif alarm_ind.HasField('onu_tiwi_ind'):
+ self.onu_transmission_interference_warning(
+ alarm_ind.onu_tiwi_ind)
+ elif alarm_ind.HasField('onu_activation_fail_ind'):
+ self.onu_activation_failure_indication(
+ alarm_ind.onu_activation_fail_ind)
+ elif alarm_ind.HasField('onu_processing_error_ind'):
+ self.onu_processing_error_indication(
+ alarm_ind.onu_processing_error_ind)
+ else:
+ self.log.warn('unknown alarm type', alarm=alarm_ind)
+
+ except Exception as e:
+ self.log.error('sorting of alarm went wrong', error=e,
+ alarm=alarm_ind)
+
+ def simulate_alarm(self, alarm):
+ self.simulator.simulate_alarm(alarm)
+
+ def los_indication(self, los_ind):
+
+ try:
+ self.log.debug('los indication received', los_ind=los_ind,
+ int_id=los_ind.intf_id, status=los_ind.status)
+ try:
+ port_type_name = self.platform.intf_id_to_port_type_name(los_ind.intf_id)
+ if los_ind.status == 1 or los_ind.status == "on":
+ # Zero out the suppression counter on OLT_LOS raise
+ self.alarm_suppress['olt_los_clear'] = 0
+ OltLosAlarm(self.alarms, intf_id=los_ind.intf_id, port_type_name=port_type_name).raise_alarm()
+ else:
+ """
+ Check if there has been more that one los clear following a previous los
+ """
+ if self.alarm_suppress['olt_los_clear'] == 0 and self.enable_alarm_suppress:
+ OltLosAlarm(self.alarms, intf_id=los_ind.intf_id, port_type_name=port_type_name).clear_alarm()
+ self.alarm_suppress['olt_los_clear'] += 1
+
+ except Exception as alarm_err:
+ self.log.error('los-indication', errmsg=alarm_err.message)
+ except Exception as e:
+ self.log.error('los-indication', errmsg=e.message)
+
+ def dying_gasp_indication(self, dying_gasp_ind):
+ try:
+ alarm_dgi = dying_gasp_ind
+ onu_id = alarm_dgi.onu_id
+ self.log.debug('openolt-alarmindication-dispatch-dying-gasp', int_id=alarm_dgi.intf_id,
+ onu_id=alarm_dgi.onu_id, status=alarm_dgi.status)
+ try:
+ """
+ Get the specific onu device information for the onu generating the alarm.
+ Extract the id. In the future extract the serial number as well
+ """
+ onu_device_id = "unresolved"
+ onu_serial_number = "unresolved"
+ onu_device = self.resolve_onu_id(onu_id, port_intf_id=alarm_dgi.intf_id)
+ if onu_device != None:
+ onu_device_id = onu_device.id
+ onu_serial_number = onu_device.serial_number
+
+ if dying_gasp_ind.status == 1 or dying_gasp_ind.status == "on":
+ OnuDyingGaspAlarm(self.alarms, dying_gasp_ind.intf_id,
+ onu_device_id).raise_alarm()
+ else:
+ OnuDyingGaspAlarm(self.alarms, dying_gasp_ind.intf_id,
+ onu_device_id).clear_alarm()
+ except Exception as alarm_err:
+ self.log.exception('dying-gasp-indication', errmsg=alarm_err.message)
+
+ except Exception as e:
+ self.log.error('dying_gasp_indication', error=e)
+
+ def onu_alarm_indication(self, onu_alarm_ind):
+ """
+ LOB = Los of burst
+ LOPC = Loss of PLOAM miss channel
+
+ :param onu_alarm_ind: Alarm indication which currently contains
+ onu_id:
+ los_status:
+ lob_status:
+ lopc_miss_status:
+ lopc_mic_error_status:
+ :return:
+ """
+ self.log.info('onu-alarm-indication')
+
+ try:
+ self.log.debug('onu alarm indication received', los_status=onu_alarm_ind.los_status,
+ onu_intf_id=onu_alarm_ind.onu_id,
+ lob_status=onu_alarm_ind.lob_status,
+ lopc_miss_status=onu_alarm_ind.lopc_miss_status,
+ lopc_mic_error_status=onu_alarm_ind.lopc_mic_error_status,
+ intf_id=onu_alarm_ind.intf_id
+ )
+
+ try:
+ """
+ Get the specific onu device information for the onu generating the alarm.
+ Extract the id. In the future extract the serial number as well
+ """
+ onu_device_id = "unresolved"
+ serial_number = "unresolved"
+ onu_device = self.resolve_onu_id(onu_alarm_ind.onu_id, port_intf_id=onu_alarm_ind.intf_id)
+ if onu_device != None:
+ onu_device_id = onu_device.id
+ serial_number = onu_device.serial_number
+
+ if onu_alarm_ind.los_status == 1 or onu_alarm_ind.los_status == "on":
+ OnuLosAlarm(self.alarms, onu_id=onu_device_id, intf_id=onu_alarm_ind.intf_id).raise_alarm()
+ elif onu_alarm_ind.los_status == 0 or onu_alarm_ind.los_status == "off":
+ OnuLosAlarm(self.alarms, onu_id=onu_device_id, intf_id=onu_alarm_ind.intf_id).clear_alarm()
+ else: # No Change
+ pass
+
+ if onu_alarm_ind.lopc_miss_status == 1 or onu_alarm_ind.lopc_miss_status == "on":
+ OnuLopcMissAlarm(self.alarms, onu_id=onu_device_id, intf_id=onu_alarm_ind.intf_id).raise_alarm()
+ elif (onu_alarm_ind.lopc_miss_status == 0 or onu_alarm_ind.lopc_miss_status == "off"):
+ OnuLopcMissAlarm(self.alarms, onu_id=onu_device_id, intf_id=onu_alarm_ind.intf_id).clear_alarm()
+ else: # No Change
+ pass
+
+ if onu_alarm_ind.lopc_mic_error_status == 1 or onu_alarm_ind.lopc_mic_error_status == "on":
+ OnuLopcMicErrorAlarm(self.alarms, onu_id=onu_device_id, intf_id=onu_alarm_ind.intf_id).raise_alarm()
+ elif onu_alarm_ind.lopc_mic_error_status == 0 or onu_alarm_ind.lopc_mic_error_status == "off":
+ OnuLopcMicErrorAlarm(self.alarms, onu_id=onu_device_id, intf_id=onu_alarm_ind.intf_id).clear_alarm()
+ else: # No Change
+ pass
+
+ if onu_alarm_ind.lob_status == 1 or onu_alarm_ind.lob_status == "on":
+ OnuLobAlarm(self.alarms, onu_id=onu_device_id, intf_id=onu_alarm_ind.intf_id).raise_alarm()
+ elif onu_alarm_ind.lob_status == 0 or onu_alarm_ind.lob_status == "off":
+ OnuLobAlarm(self.alarms, onu_id=onu_device_id, intf_id=onu_alarm_ind.intf_id).clear_alarm()
+ else: # No Change
+ pass
+ except Exception as alarm_err:
+ self.log.exception('onu-alarm-indication', errmsg=alarm_err.message)
+
+ except Exception as e:
+ self.log.exception('onu-alarm-indication', errmsg=e.message)
+
+ def onu_startup_failure_indication(self, onu_startup_fail_ind):
+ """
+ Current protobuf indicator:
+ message OnuStartupFailureIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+ }
+
+ :param onu_startup_fail_ind:
+ :return:
+ """
+ try:
+ ind = onu_startup_fail_ind
+ label = "onu-startup-failure-indication"
+ self.log.debug(label + " received", onu_startup_fail_ind=ind, int_id=ind.intf_id, onu_id=ind.onu_id, status=ind.status)
+ try:
+ if ind.status == 1 or ind.status == "on":
+ OnuStartupAlarm(self.alarms, intf_id=ind.intf_id,onu_id=ind.onu_id).raise_alarm()
+ else:
+ OnuStartupAlarm(self.alarms, intf_id=ind.intf_id, onu_id=ind.onu_id).clear_alarm()
+ except Exception as alarm_err:
+ self.log.exception(label, errmsg=alarm_err.message)
+
+ except Exception as e:
+ self.log.exception(label, errmsg=e.message)
+
+ def onu_signal_degrade_indication(self, onu_signal_degrade_ind):
+ """
+ Current protobuf indicator:
+ OnuSignalDegradeIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+ fixed32 inverse_bit_error_rate = 4;
+ }
+ :param onu_signal_degrade_ind:
+ :return:
+ """
+ try:
+ ind = onu_signal_degrade_ind
+ label = "onu-signal-degrade-indication"
+ self.log.debug(label + ' received',
+ onu_startup_fail_ind=ind,
+ int_id=ind.intf_id,
+ onu_id=ind.onu_id,
+ inverse_bit_error_rate=ind.inverse_bit_error_rate,
+ status=ind.status)
+ try:
+ if ind.status == 1 or ind.status == "on":
+ OnuSignalDegradeAlarm(self.alarms, intf_id=ind.intf_id, onu_id=ind.onu_id,
+ inverse_bit_error_rate=ind.inverse_bit_error_rate).raise_alarm()
+ else:
+ OnuSignalDegradeAlarm(self.alarms, intf_id=ind.intf_id, onu_id=ind.onu_id,
+ inverse_bit_error_rate=ind.inverse_bit_error_rate).clear_alarm()
+ except Exception as alarm_err:
+ self.log.exception(label, errmsg=alarm_err.message)
+
+ except Exception as e:
+ self.log.exception(label, errmsg=e.message)
+
+ def onu_drift_of_window_indication(self, onu_drift_of_window_ind):
+ """
+ Current protobuf indicator:
+ OnuDriftOfWindowIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+ fixed32 drift = 4;
+ fixed32 new_eqd = 5;
+ }
+
+ :param onu_drift_of_window_ind:
+ :return:
+ """
+ try:
+ ind = onu_drift_of_window_ind
+ label = "onu-window-drift-indication"
+
+ onu_device_id, onu_serial_number = self.resolve_onudev_id_onudev_serialnum(
+ self.resolve_onu_id(ind.onu_id, port_intf_id=ind.intf_id))
+
+ self.log.debug(label + ' received',
+ onu_drift_of_window_ind=ind,
+ int_id=ind.intf_id,
+ onu_id=ind.onu_id,
+ onu_device_id=onu_device_id,
+ drift=ind.drift,
+ new_eqd=ind.new_eqd,
+ status=ind.status)
+ try:
+ if ind.status == 1 or ind.status == "on":
+ OnuWindowDriftAlarm(self.alarms, intf_id=ind.intf_id,
+ onu_id=onu_device_id,
+ drift=ind.drift,
+ new_eqd=ind.new_eqd).raise_alarm()
+ else:
+ OnuWindowDriftAlarm(self.alarms, intf_id=ind.intf_id,
+ onu_id=onu_device_id,
+ drift=ind.drift,
+ new_eqd=ind.new_eqd).clear_alarm()
+ except Exception as alarm_err:
+ self.log.exception(label, errmsg=alarm_err.message)
+
+ except Exception as e:
+ self.log.exception(label, errmsg=e.message)
+
+ def onu_loss_omci_indication(self, onu_loss_omci_ind):
+ self.log.info('not implemented yet')
+
+ def onu_signals_failure_indication(self, onu_signals_fail_ind):
+ """
+ Current protobuf indicator:
+ OnuSignalsFailureIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+ fixed32 inverse_bit_error_rate = 4;
+ }
+
+ :param onu_signals_fail_ind:
+ :return:
+ """
+ try:
+ ind = onu_signals_fail_ind
+ label = "onu-signal-failure-indication"
+
+ onu_device_id, onu_serial_number = self.resolve_onudev_id_onudev_serialnum(
+ self.resolve_onu_id(ind.onu_id, port_intf_id=ind.intf_id))
+
+ self.log.debug(label + ' received',
+ onu_startup_fail_ind=ind,
+ int_id=ind.intf_id,
+ onu_id=ind.onu_id,
+ onu_device_id=onu_device_id,
+ onu_serial_number=onu_serial_number,
+ inverse_bit_error_rate=ind.inverse_bit_error_rate,
+ status=ind.status)
+ try:
+ if ind.status == 1 or ind.status == "on":
+ OnuSignalFailAlarm(self.alarms, intf_id=ind.intf_id,
+ onu_id=onu_device_id,
+ inverse_bit_error_rate=ind.inverse_bit_error_rate).raise_alarm()
+ else:
+ OnuSignalFailAlarm(self.alarms, intf_id=ind.intf_id,
+ onu_id=onu_device_id,
+ inverse_bit_error_rate=ind.inverse_bit_error_rate).clear_alarm()
+ except Exception as alarm_err:
+ self.log.exception(label, errmsg=alarm_err.message)
+
+ except Exception as e:
+ self.log.exception(label, errmsg=e.message)
+
+
+ def onu_transmission_interference_warning(self, onu_tiwi_ind):
+ self.log.info('not implemented yet')
+
+ def onu_activation_failure_indication(self, onu_activation_fail_ind):
+ """
+
+ No status is currently passed with this alarm. Consequently it will always just raise
+ :param onu_activation_fail_ind:
+ :return:
+ """
+ try:
+ ind = onu_activation_fail_ind
+ label = "onu-activation-failure-indication"
+
+ onu_device_id, onu_serial_number = self.resolve_onudev_id_onudev_serialnum(
+ self.resolve_onu_id(ind.onu_id, port_intf_id=ind.intf_id))
+
+ self.log.debug(label + ' received',
+ onu_startup_fail_ind=ind,
+ int_id=ind.intf_id,
+ onu_id=ind.onu_id,
+ onu_device_id=onu_device_id,
+ onu_serial_number=onu_serial_number)
+ try:
+
+ OnuActivationFailAlarm(self.alarms, intf_id=ind.intf_id,
+ onu_id=onu_device_id).raise_alarm()
+ except Exception as alarm_err:
+ self.log.exception(label, errmsg=alarm_err.message)
+
+ except Exception as e:
+ self.log.exception(label, errmsg=e.message)
+
+ def onu_processing_error_indication(self, onu_processing_error_ind):
+ self.log.info('not implemented yet')
+
+ """
+ Helper Methods
+ """
+
+ def resolve_onudev_id_onudev_serialnum(self,onu_device):
+ """
+ Convenience wrapper to resolve device_id and serial number
+ :param onu_device:
+ :return: tuple: onu_device_id, onu_serial_number
+ """
+ try:
+ onu_device_id = "unresolved"
+ onu_serial_number = "unresolved"
+ if onu_device != None:
+ onu_device_id = onu_device.id
+ onu_serial_number = onu_device.serial_number
+ except Exception as err:
+ self.log.exception("openolt-alarms-resolve-onudev-id ", errmsg=err.message)
+ raise Exception(err)
+ return onu_device_id, onu_serial_number
+
+ def resolve_onu_id(self, onu_id, port_intf_id):
+ """
+ Resolve the onu_device from the intf_id value and port. Uses the adapter agent to
+ resolve this..
+
+ Returns None if not found. Caller will have to test for None and act accordingly.
+ :param onu_id:
+ :param port_intf_id:
+ :return:
+ """
+
+ try:
+ onu_device = None
+ onu_device = self.adapter_agent.get_child_device(
+ self.device_id,
+ parent_port_no=self.platform.intf_id_to_port_no(
+ port_intf_id, device_pb2.Port.PON_OLT),
+ onu_id=onu_id)
+ onu_device_id = onu_device.id
+ except Exception as inner:
+ self.log.exception('resolve-onu-id', errmsg=inner.message)
+
+ return onu_device
+
diff --git a/python/adapters/openolt/openolt_bw.py b/python/adapters/openolt/openolt_bw.py
new file mode 100644
index 0000000..7c70b78
--- /dev/null
+++ b/python/adapters/openolt/openolt_bw.py
@@ -0,0 +1,41 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+DEFAULT_ONU_BW_PROFILE = "default"
+DEFAULT_ONU_PIR = 1000000 # 1Gbps
+
+
+class OpenOltBW(object):
+
+ def __init__(self, log, proxy):
+ self.log = log
+ self.proxy = proxy
+
+ def pir(self, serial_number):
+ bw = 0
+ try:
+ bw = self.proxy.get(
+ '/traffic_descriptor_profiles/{}'.format(serial_number))
+ except KeyError:
+ self.log.debug('bandwidth not configured',
+ serial_number=serial_number)
+ try:
+ bw = self.proxy.get('/traffic_descriptor_profiles/{}' \
+ .format(DEFAULT_ONU_BW_PROFILE))
+ except KeyError:
+ return DEFAULT_ONU_PIR
+
+ return bw.maximum_bandwidth
diff --git a/python/adapters/openolt/openolt_device.py b/python/adapters/openolt/openolt_device.py
new file mode 100644
index 0000000..2779dc6
--- /dev/null
+++ b/python/adapters/openolt/openolt_device.py
@@ -0,0 +1,1074 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import threading
+import binascii
+import grpc
+import socket
+import re
+import structlog
+from twisted.internet import reactor
+from scapy.layers.l2 import Ether, Dot1Q
+from transitions import Machine
+
+#from voltha.adapters.openolt.protos import openolt_pb2_grpc, openolt_pb2
+from python.protos.bbf_fiber_tcont_body_pb2 import TcontsConfigData
+from python.protos.bbf_fiber_gemport_body_pb2 import GemportsConfigData
+
+from python.adapters.extensions.alarms.onu.onu_discovery_alarm import OnuDiscoveryAlarm
+
+from python.common.utils.nethelpers import mac_str_to_tuple
+from python.protos.openflow_13_pb2 import OFPPS_LIVE, OFPPF_FIBER, \
+ OFPPS_LINK_DOWN, OFPPF_1GB_FD, \
+ OFPC_GROUP_STATS, OFPC_PORT_STATS, OFPC_TABLE_STATS, OFPC_FLOW_STATS, \
+ ofp_switch_features, ofp_port, ofp_port_stats, ofp_desc
+from python.common.utils.registry import registry
+from python.protos import openolt_pb2
+from python.protos import third_party
+from python.protos.common_pb2 import AdminStatus, OperStatus, ConnectStatus
+from python.protos.common_pb2 import LogLevel
+from python.protos.device_pb2 import Port, Device
+
+from python.protos.logical_device_pb2 import LogicalDevice, LogicalPort
+
+class OpenoltDevice(object):
+ """
+ OpenoltDevice state machine:
+
+ null ----> init ------> connected -----> up -----> down
+ ^ ^ | ^ | |
+ | | | | | |
+ | +-------------+ +---------+ |
+ | |
+ +-----------------------------------------+
+ """
+ # pylint: disable=too-many-instance-attributes
+ # pylint: disable=R0904
+ states = [
+ 'state_null',
+ 'state_init',
+ 'state_connected',
+ 'state_up',
+ 'state_down']
+
+ transitions = [
+ {'trigger': 'go_state_init',
+ 'source': ['state_null', 'state_connected', 'state_down'],
+ 'dest': 'state_init',
+ 'before': 'do_state_init',
+ 'after': 'post_init'},
+ {'trigger': 'go_state_connected',
+ 'source': 'state_init',
+ 'dest': 'state_connected',
+ 'before': 'do_state_connected'},
+ {'trigger': 'go_state_up',
+ 'source': ['state_connected', 'state_down'],
+ 'dest': 'state_up',
+ 'before': 'do_state_up'},
+ {'trigger': 'go_state_down',
+ 'source': ['state_up'],
+ 'dest': 'state_down',
+ 'before': 'do_state_down',
+ 'after': 'post_down'}]
+
+ def __init__(self, **kwargs):
+ super(OpenoltDevice, self).__init__()
+
+ self.adapter_agent = kwargs['adapter_agent']
+ self.device_num = kwargs['device_num']
+ device = kwargs['device']
+
+ self.platform_class = kwargs['support_classes']['platform']
+ self.resource_mgr_class = kwargs['support_classes']['resource_mgr']
+ self.flow_mgr_class = kwargs['support_classes']['flow_mgr']
+ self.alarm_mgr_class = kwargs['support_classes']['alarm_mgr']
+ self.stats_mgr_class = kwargs['support_classes']['stats_mgr']
+ self.bw_mgr_class = kwargs['support_classes']['bw_mgr']
+
+ is_reconciliation = kwargs.get('reconciliation', False)
+ self.device_id = device.id
+ self.host_and_port = device.host_and_port
+ self.extra_args = device.extra_args
+ self.log = structlog.get_logger(id=self.device_id,
+ ip=self.host_and_port)
+ self.proxy = registry('core').get_proxy('/')
+
+ self.log.info('openolt-device-init')
+
+ # default device id and device serial number. If device_info provides better results, they will be updated
+ self.dpid = kwargs.get('dp_id')
+ self.serial_number = self.host_and_port # FIXME
+
+ # Device already set in the event of reconciliation
+ if not is_reconciliation:
+ self.log.info('updating-device')
+ # It is a new device
+ # Update device
+ device.root = True
+ device.connect_status = ConnectStatus.UNREACHABLE
+ device.oper_status = OperStatus.ACTIVATING
+ self.adapter_agent.update_device(device)
+
+ # If logical device does exist use it, else create one after connecting to device
+ if device.parent_id:
+ # logical device already exists
+ self.logical_device_id = device.parent_id
+ if is_reconciliation:
+ self.adapter_agent.reconcile_logical_device(
+ self.logical_device_id)
+
+ # Initialize the OLT state machine
+ self.machine = Machine(model=self, states=OpenoltDevice.states,
+ transitions=OpenoltDevice.transitions,
+ send_event=True, initial='state_null')
+ self.go_state_init()
+
+ def create_logical_device(self, device_info):
+ dpid = device_info.device_id
+ serial_number = device_info.device_serial_number
+
+ if dpid is None: dpid = self.dpid
+ if serial_number is None: serial_number = self.serial_number
+
+ if dpid == None or dpid == '':
+ uri = self.host_and_port.split(":")[0]
+ try:
+ socket.inet_pton(socket.AF_INET, uri)
+ dpid = '00:00:' + self.ip_hex(uri)
+ except socket.error:
+ # this is not an IP
+ dpid = self.stringToMacAddr(uri)
+
+ if serial_number == None or serial_number == '':
+ serial_number = self.host_and_port
+
+ self.log.info('creating-openolt-logical-device', dp_id=dpid, serial_number=serial_number)
+
+ mfr_desc = device_info.vendor
+ sw_desc = device_info.firmware_version
+ hw_desc = device_info.model
+ if device_info.hardware_version: hw_desc += '-' + device_info.hardware_version
+
+ # Create logical OF device
+ ld = LogicalDevice(
+ root_device_id=self.device_id,
+ switch_features=ofp_switch_features(
+ n_buffers=256, # TODO fake for now
+ n_tables=2, # TODO ditto
+ capabilities=( # TODO and ditto
+ OFPC_FLOW_STATS
+ | OFPC_TABLE_STATS
+ | OFPC_PORT_STATS
+ | OFPC_GROUP_STATS
+ )
+ ),
+ desc=ofp_desc(
+ serial_num=serial_number
+ )
+ )
+ ld_init = self.adapter_agent.create_logical_device(ld,
+ dpid=dpid)
+
+ self.logical_device_id = ld_init.id
+
+ device = self.adapter_agent.get_device(self.device_id)
+ device.serial_number = serial_number
+ self.adapter_agent.update_device(device)
+
+ self.dpid = dpid
+ self.serial_number = serial_number
+
+ self.log.info('created-openolt-logical-device', logical_device_id=ld_init.id)
+
+ def stringToMacAddr(self, uri):
+ regex = re.compile('[^a-zA-Z]')
+ uri = regex.sub('', uri)
+
+ l = len(uri)
+ if l > 6:
+ uri = uri[0:6]
+ else:
+ uri = uri + uri[0:6 - l]
+
+ print uri
+
+ return ":".join([hex(ord(x))[-2:] for x in uri])
+
+ def do_state_init(self, event):
+ # Initialize gRPC
+ self.channel = grpc.insecure_channel(self.host_and_port)
+ self.channel_ready_future = grpc.channel_ready_future(self.channel)
+
+ self.log.info('openolt-device-created', device_id=self.device_id)
+
+ def post_init(self, event):
+ self.log.debug('post_init')
+
+ # We have reached init state, starting the indications thread
+
+ # Catch RuntimeError exception
+ try:
+ # Start indications thread
+ self.indications_thread_handle = threading.Thread(
+ target=self.indications_thread)
+ # Old getter/setter API for daemon; use it directly as a
+ # property instead. The Jinkins error will happon on the reason of
+ # Exception in thread Thread-1 (most likely raised # during
+ # interpreter shutdown)
+ self.indications_thread_handle.setDaemon(True)
+ self.indications_thread_handle.start()
+ except Exception as e:
+ self.log.exception('post_init failed', e=e)
+
+ def do_state_connected(self, event):
+ self.log.debug("do_state_connected")
+
+ device = self.adapter_agent.get_device(self.device_id)
+
+ self.stub = openolt_pb2_grpc.OpenoltStub(self.channel)
+
+ device_info = self.stub.GetDeviceInfo(openolt_pb2.Empty())
+ self.log.info('Device connected', device_info=device_info)
+
+ self.create_logical_device(device_info)
+
+ device.serial_number = self.serial_number
+
+ self.resource_mgr = self.resource_mgr_class(self.device_id,
+ self.host_and_port,
+ self.extra_args,
+ device_info)
+ self.platform = self.platform_class(self.log, self.resource_mgr)
+ self.flow_mgr = self.flow_mgr_class(self.adapter_agent, self.log,
+ self.stub, self.device_id,
+ self.logical_device_id,
+ self.platform, self.resource_mgr)
+
+ self.alarm_mgr = self.alarm_mgr_class(self.log, self.adapter_agent,
+ self.device_id,
+ self.logical_device_id,
+ self.platform)
+ self.stats_mgr = self.stats_mgr_class(self, self.log, self.platform)
+ self.bw_mgr = self.bw_mgr_class(self.log, self.proxy)
+
+ device.vendor = device_info.vendor
+ device.model = device_info.model
+ device.hardware_version = device_info.hardware_version
+ device.firmware_version = device_info.firmware_version
+
+ # TODO: check for uptime and reboot if too long (VOL-1192)
+
+ device.connect_status = ConnectStatus.REACHABLE
+ self.adapter_agent.update_device(device)
+
+ def do_state_up(self, event):
+ self.log.debug("do_state_up")
+
+ device = self.adapter_agent.get_device(self.device_id)
+
+ # Update phys OF device
+ device.parent_id = self.logical_device_id
+ device.oper_status = OperStatus.ACTIVE
+ self.adapter_agent.update_device(device)
+
+ def do_state_down(self, event):
+ self.log.debug("do_state_down")
+ oper_state = OperStatus.UNKNOWN
+ connect_state = ConnectStatus.UNREACHABLE
+
+ # Propagating to the children
+
+ # Children ports
+ child_devices = self.adapter_agent.get_child_devices(self.device_id)
+ for onu_device in child_devices:
+ onu_adapter_agent = \
+ registry('adapter_loader').get_agent(onu_device.adapter)
+ onu_adapter_agent.update_interface(onu_device,
+ {'oper_state': 'down'})
+ self.onu_ports_down(onu_device, oper_state)
+
+ # Children devices
+ self.adapter_agent.update_child_devices_state(
+ self.device_id, oper_status=oper_state,
+ connect_status=connect_state)
+ # Device Ports
+ device_ports = self.adapter_agent.get_ports(self.device_id,
+ Port.ETHERNET_NNI)
+ logical_ports_ids = [port.label for port in device_ports]
+ device_ports += self.adapter_agent.get_ports(self.device_id,
+ Port.PON_OLT)
+
+ for port in device_ports:
+ port.oper_status = oper_state
+ self.adapter_agent.add_port(self.device_id, port)
+
+ # Device logical port
+ for logical_port_id in logical_ports_ids:
+ logical_port = self.adapter_agent.get_logical_port(
+ self.logical_device_id, logical_port_id)
+ logical_port.ofp_port.state = OFPPS_LINK_DOWN
+ self.adapter_agent.update_logical_port(self.logical_device_id,
+ logical_port)
+
+ # Device
+ device = self.adapter_agent.get_device(self.device_id)
+ device.oper_status = oper_state
+ device.connect_status = connect_state
+
+ reactor.callLater(2, self.adapter_agent.update_device, device)
+
+ # def post_up(self, event):
+ # self.log.debug('post-up')
+ # self.flow_mgr.reseed_flows()
+
+ def post_down(self, event):
+ self.log.debug('post_down')
+ self.flow_mgr.reset_flows()
+
+ def indications_thread(self):
+ self.log.debug('starting-indications-thread')
+ self.log.debug('connecting to olt', device_id=self.device_id)
+ self.channel_ready_future.result() # blocking call
+ self.log.info('connected to olt', device_id=self.device_id)
+ self.go_state_connected()
+
+ self.indications = self.stub.EnableIndication(openolt_pb2.Empty())
+
+ while True:
+ try:
+ # get the next indication from olt
+ ind = next(self.indications)
+ except Exception as e:
+ self.log.warn('gRPC connection lost', error=e)
+ reactor.callFromThread(self.go_state_down)
+ reactor.callFromThread(self.go_state_init)
+ break
+ else:
+ self.log.debug("rx indication", indication=ind)
+
+ # indication handlers run in the main event loop
+ if ind.HasField('olt_ind'):
+ reactor.callFromThread(self.olt_indication, ind.olt_ind)
+ elif ind.HasField('intf_ind'):
+ reactor.callFromThread(self.intf_indication, ind.intf_ind)
+ elif ind.HasField('intf_oper_ind'):
+ reactor.callFromThread(self.intf_oper_indication,
+ ind.intf_oper_ind)
+ elif ind.HasField('onu_disc_ind'):
+ reactor.callFromThread(self.onu_discovery_indication,
+ ind.onu_disc_ind)
+ elif ind.HasField('onu_ind'):
+ reactor.callFromThread(self.onu_indication, ind.onu_ind)
+ elif ind.HasField('omci_ind'):
+ reactor.callFromThread(self.omci_indication, ind.omci_ind)
+ elif ind.HasField('pkt_ind'):
+ reactor.callFromThread(self.packet_indication, ind.pkt_ind)
+ elif ind.HasField('port_stats'):
+ reactor.callFromThread(
+ self.stats_mgr.port_statistics_indication,
+ ind.port_stats)
+ elif ind.HasField('flow_stats'):
+ reactor.callFromThread(
+ self.stats_mgr.flow_statistics_indication,
+ ind.flow_stats)
+ elif ind.HasField('alarm_ind'):
+ reactor.callFromThread(self.alarm_mgr.process_alarms,
+ ind.alarm_ind)
+ else:
+ self.log.warn('unknown indication type')
+
+ def olt_indication(self, olt_indication):
+ if olt_indication.oper_state == "up":
+ self.go_state_up()
+ elif olt_indication.oper_state == "down":
+ self.go_state_down()
+
+ def intf_indication(self, intf_indication):
+ self.log.debug("intf indication", intf_id=intf_indication.intf_id,
+ oper_state=intf_indication.oper_state)
+
+ if intf_indication.oper_state == "up":
+ oper_status = OperStatus.ACTIVE
+ else:
+ oper_status = OperStatus.DISCOVERED
+
+ # add_port update the port if it exists
+ self.add_port(intf_indication.intf_id, Port.PON_OLT, oper_status)
+
+ def intf_oper_indication(self, intf_oper_indication):
+ self.log.debug("Received interface oper state change indication",
+ intf_id=intf_oper_indication.intf_id,
+ type=intf_oper_indication.type,
+ oper_state=intf_oper_indication.oper_state)
+
+ if intf_oper_indication.oper_state == "up":
+ oper_state = OperStatus.ACTIVE
+ else:
+ oper_state = OperStatus.DISCOVERED
+
+ if intf_oper_indication.type == "nni":
+
+ # add_(logical_)port update the port if it exists
+ port_no, label = self.add_port(intf_oper_indication.intf_id,
+ Port.ETHERNET_NNI, oper_state)
+ self.log.debug("int_oper_indication", port_no=port_no, label=label)
+ self.add_logical_port(port_no, intf_oper_indication.intf_id,
+ oper_state)
+
+ elif intf_oper_indication.type == "pon":
+ # FIXME - handle PON oper state change
+ pass
+
+ def onu_discovery_indication(self, onu_disc_indication):
+ intf_id = onu_disc_indication.intf_id
+ serial_number = onu_disc_indication.serial_number
+
+ serial_number_str = self.stringify_serial_number(serial_number)
+
+ self.log.debug("onu discovery indication", intf_id=intf_id,
+ serial_number=serial_number_str)
+
+ # Post ONU Discover alarm 20180809_0805
+ try:
+ OnuDiscoveryAlarm(self.alarm_mgr.alarms, pon_id=intf_id,
+ serial_number=serial_number_str).raise_alarm()
+ except Exception as disc_alarm_error:
+ self.log.exception("onu-discovery-alarm-error",
+ errmsg=disc_alarm_error.message)
+ # continue for now.
+
+ onu_device = self.adapter_agent.get_child_device(
+ self.device_id,
+ serial_number=serial_number_str)
+
+ if onu_device is None:
+ try:
+ onu_id = self.resource_mgr.get_onu_id(intf_id)
+ if onu_id is None:
+ raise Exception("onu-id-unavailable")
+
+ self.add_onu_device(
+ intf_id,
+ self.platform.intf_id_to_port_no(intf_id, Port.PON_OLT),
+ onu_id, serial_number)
+ self.activate_onu(intf_id, onu_id, serial_number,
+ serial_number_str)
+ except Exception as e:
+ self.log.exception('onu-activation-failed', e=e)
+
+ else:
+ if onu_device.connect_status != ConnectStatus.REACHABLE:
+ onu_device.connect_status = ConnectStatus.REACHABLE
+ self.adapter_agent.update_device(onu_device)
+
+ onu_id = onu_device.proxy_address.onu_id
+ if onu_device.oper_status == OperStatus.DISCOVERED \
+ or onu_device.oper_status == OperStatus.ACTIVATING:
+ self.log.debug("ignore onu discovery indication, \
+ the onu has been discovered and should be \
+ activating shorlty", intf_id=intf_id,
+ onu_id=onu_id, state=onu_device.oper_status)
+ elif onu_device.oper_status == OperStatus.ACTIVE:
+ self.log.warn("onu discovery indication whereas onu is \
+ supposed to be active",
+ intf_id=intf_id, onu_id=onu_id,
+ state=onu_device.oper_status)
+ elif onu_device.oper_status == OperStatus.UNKNOWN:
+ self.log.info("onu in unknown state, recovering from olt \
+ reboot probably, activate onu", intf_id=intf_id,
+ onu_id=onu_id, serial_number=serial_number_str)
+
+ onu_device.oper_status = OperStatus.DISCOVERED
+ self.adapter_agent.update_device(onu_device)
+ try:
+ self.activate_onu(intf_id, onu_id, serial_number,
+ serial_number_str)
+ except Exception as e:
+ self.log.error('onu-activation-error',
+ serial_number=serial_number_str, error=e)
+ else:
+ self.log.warn('unexpected state', onu_id=onu_id,
+ onu_device_oper_state=onu_device.oper_status)
+
+ def onu_indication(self, onu_indication):
+ self.log.debug("onu indication", intf_id=onu_indication.intf_id,
+ onu_id=onu_indication.onu_id,
+ serial_number=onu_indication.serial_number,
+ oper_state=onu_indication.oper_state,
+ admin_state=onu_indication.admin_state)
+ try:
+ serial_number_str = self.stringify_serial_number(
+ onu_indication.serial_number)
+ except Exception as e:
+ serial_number_str = None
+
+ if serial_number_str is not None:
+ onu_device = self.adapter_agent.get_child_device(
+ self.device_id,
+ serial_number=serial_number_str)
+ else:
+ onu_device = self.adapter_agent.get_child_device(
+ self.device_id,
+ parent_port_no=self.platform.intf_id_to_port_no(
+ onu_indication.intf_id, Port.PON_OLT),
+ onu_id=onu_indication.onu_id)
+
+ if onu_device is None:
+ self.log.error('onu not found', intf_id=onu_indication.intf_id,
+ onu_id=onu_indication.onu_id)
+ return
+
+ if self.platform.intf_id_from_pon_port_no(onu_device.parent_port_no) \
+ != onu_indication.intf_id:
+ self.log.warn('ONU-is-on-a-different-intf-id-now',
+ previous_intf_id=self.platform.intf_id_from_pon_port_no(
+ onu_device.parent_port_no),
+ current_intf_id=onu_indication.intf_id)
+ # FIXME - handle intf_id mismatch (ONU move?)
+
+ if onu_device.proxy_address.onu_id != onu_indication.onu_id:
+ # FIXME - handle onu id mismatch
+ self.log.warn('ONU-id-mismatch, can happen if both voltha and '
+ 'the olt rebooted',
+ expected_onu_id=onu_device.proxy_address.onu_id,
+ received_onu_id=onu_indication.onu_id)
+
+ # Admin state
+ if onu_indication.admin_state == 'down':
+ if onu_indication.oper_state != 'down':
+ self.log.error('ONU-admin-state-down-and-oper-status-not-down',
+ oper_state=onu_indication.oper_state)
+ # Forcing the oper state change code to execute
+ onu_indication.oper_state = 'down'
+
+ # Port and logical port update is taken care of by oper state block
+
+ elif onu_indication.admin_state == 'up':
+ pass
+
+ else:
+ self.log.warn('Invalid-or-not-implemented-admin-state',
+ received_admin_state=onu_indication.admin_state)
+
+ self.log.debug('admin-state-dealt-with')
+
+ onu_adapter_agent = \
+ registry('adapter_loader').get_agent(onu_device.adapter)
+ if onu_adapter_agent is None:
+ self.log.error('onu_adapter_agent-could-not-be-retrieved',
+ onu_device=onu_device)
+ return
+
+ # Operating state
+ if onu_indication.oper_state == 'down':
+
+ if onu_device.connect_status != ConnectStatus.UNREACHABLE:
+ onu_device.connect_status = ConnectStatus.UNREACHABLE
+ self.adapter_agent.update_device(onu_device)
+
+ # Move to discovered state
+ self.log.debug('onu-oper-state-is-down')
+
+ if onu_device.oper_status != OperStatus.DISCOVERED:
+ onu_device.oper_status = OperStatus.DISCOVERED
+ self.adapter_agent.update_device(onu_device)
+ # Set port oper state to Discovered
+ self.onu_ports_down(onu_device, OperStatus.DISCOVERED)
+
+ onu_adapter_agent.update_interface(onu_device,
+ {'oper_state': 'down'})
+
+ elif onu_indication.oper_state == 'up':
+
+ if onu_device.connect_status != ConnectStatus.REACHABLE:
+ onu_device.connect_status = ConnectStatus.REACHABLE
+ self.adapter_agent.update_device(onu_device)
+
+ if onu_device.oper_status != OperStatus.DISCOVERED:
+ self.log.debug("ignore onu indication",
+ intf_id=onu_indication.intf_id,
+ onu_id=onu_indication.onu_id,
+ state=onu_device.oper_status,
+ msg_oper_state=onu_indication.oper_state)
+ return
+
+ # Device was in Discovered state, setting it to active
+
+ # Prepare onu configuration
+
+ onu_adapter_agent.create_interface(onu_device, onu_indication)
+
+ else:
+ self.log.warn('Not-implemented-or-invalid-value-of-oper-state',
+ oper_state=onu_indication.oper_state)
+
+ def onu_ports_down(self, onu_device, oper_state):
+ # Set port oper state to Discovered
+ # add port will update port if it exists
+ # self.adapter_agent.add_port(
+ # self.device_id,
+ # Port(
+ # port_no=uni_no,
+ # label=uni_name,
+ # type=Port.ETHERNET_UNI,
+ # admin_state=onu_device.admin_state,
+ # oper_status=oper_state))
+ # TODO this should be downning ports in onu adatper
+
+ # Disable logical port
+ onu_ports = self.proxy.get('devices/{}/ports'.format(onu_device.id))
+ for onu_port in onu_ports:
+ self.log.debug('onu-ports-down', onu_port=onu_port)
+ onu_port_id = onu_port.label
+ try:
+ onu_logical_port = self.adapter_agent.get_logical_port(
+ logical_device_id=self.logical_device_id, port_id=onu_port_id)
+ onu_logical_port.ofp_port.state = OFPPS_LINK_DOWN
+ self.adapter_agent.update_logical_port(
+ logical_device_id=self.logical_device_id,
+ port=onu_logical_port)
+ self.log.debug('cascading-oper-state-to-port-and-logical-port')
+ except KeyError as e:
+ self.log.error('matching-onu-port-label-invalid',
+ onu_id=onu_device.id, olt_id=self.device_id,
+ onu_ports=onu_ports, onu_port_id=onu_port_id,
+ error=e)
+
+ def omci_indication(self, omci_indication):
+
+ self.log.debug("omci indication", intf_id=omci_indication.intf_id,
+ onu_id=omci_indication.onu_id)
+
+ onu_device = self.adapter_agent.get_child_device(
+ self.device_id, onu_id=omci_indication.onu_id,
+ parent_port_no=self.platform.intf_id_to_port_no(
+ omci_indication.intf_id, Port.PON_OLT), )
+
+ self.adapter_agent.receive_proxied_message(onu_device.proxy_address,
+ omci_indication.pkt)
+
+ def packet_indication(self, pkt_indication):
+
+ self.log.debug("packet indication",
+ intf_type=pkt_indication.intf_type,
+ intf_id=pkt_indication.intf_id,
+ port_no=pkt_indication.port_no,
+ cookie=pkt_indication.cookie,
+ gemport_id=pkt_indication.gemport_id,
+ flow_id=pkt_indication.flow_id)
+
+ if pkt_indication.intf_type == "pon":
+ if pkt_indication.port_no:
+ logical_port_num = pkt_indication.port_no
+ else: # TODO Remove this else block after openolt device has been fully rolled out with cookie protobuf change
+ try:
+ onu_id_uni_id = self.resource_mgr.get_onu_uni_from_ponport_gemport(pkt_indication.intf_id,
+ pkt_indication.gemport_id)
+ onu_id = int(onu_id_uni_id[0])
+ uni_id = int(onu_id_uni_id[1])
+ self.log.debug("packet indication-kv", onu_id=onu_id, uni_id=uni_id)
+ if onu_id is None:
+ raise Exception("onu-id-none")
+ if uni_id is None:
+ raise Exception("uni-id-none")
+ logical_port_num = self.platform.mk_uni_port_num(pkt_indication.intf_id, onu_id, uni_id)
+ except Exception as e:
+ self.log.error("no-onu-reference-for-gem",
+ gemport_id=pkt_indication.gemport_id, e=e)
+ return
+
+
+ elif pkt_indication.intf_type == "nni":
+ logical_port_num = self.platform.intf_id_to_port_no(
+ pkt_indication.intf_id,
+ Port.ETHERNET_NNI)
+
+ pkt = Ether(pkt_indication.pkt)
+
+ self.log.debug("packet indication",
+ logical_device_id=self.logical_device_id,
+ logical_port_no=logical_port_num)
+
+ self.adapter_agent.send_packet_in(
+ logical_device_id=self.logical_device_id,
+ logical_port_no=logical_port_num,
+ packet=str(pkt))
+
+ def packet_out(self, egress_port, msg):
+ pkt = Ether(msg)
+ self.log.debug('packet out', egress_port=egress_port,
+ device_id=self.device_id,
+ logical_device_id=self.logical_device_id,
+ packet=str(pkt).encode("HEX"))
+
+ # Find port type
+ egress_port_type = self.platform.intf_id_to_port_type_name(egress_port)
+ if egress_port_type == Port.ETHERNET_UNI:
+
+ if pkt.haslayer(Dot1Q):
+ outer_shim = pkt.getlayer(Dot1Q)
+ if isinstance(outer_shim.payload, Dot1Q):
+ # If double tag, remove the outer tag
+ payload = (
+ Ether(src=pkt.src, dst=pkt.dst, type=outer_shim.type) /
+ outer_shim.payload
+ )
+ else:
+ payload = pkt
+ else:
+ payload = pkt
+
+ send_pkt = binascii.unhexlify(str(payload).encode("HEX"))
+
+ self.log.debug(
+ 'sending-packet-to-ONU', egress_port=egress_port,
+ intf_id=self.platform.intf_id_from_uni_port_num(egress_port),
+ onu_id=self.platform.onu_id_from_port_num(egress_port),
+ uni_id=self.platform.uni_id_from_port_num(egress_port),
+ port_no=egress_port,
+ packet=str(payload).encode("HEX"))
+
+ onu_pkt = openolt_pb2.OnuPacket(
+ intf_id=self.platform.intf_id_from_uni_port_num(egress_port),
+ onu_id=self.platform.onu_id_from_port_num(egress_port),
+ port_no=egress_port,
+ pkt=send_pkt)
+
+ self.stub.OnuPacketOut(onu_pkt)
+
+ elif egress_port_type == Port.ETHERNET_NNI:
+ self.log.debug('sending-packet-to-uplink', egress_port=egress_port,
+ packet=str(pkt).encode("HEX"))
+
+ send_pkt = binascii.unhexlify(str(pkt).encode("HEX"))
+
+ uplink_pkt = openolt_pb2.UplinkPacket(
+ intf_id=self.platform.intf_id_from_nni_port_num(egress_port),
+ pkt=send_pkt)
+
+ self.stub.UplinkPacketOut(uplink_pkt)
+
+ else:
+ self.log.warn('Packet-out-to-this-interface-type-not-implemented',
+ egress_port=egress_port,
+ port_type=egress_port_type)
+
+ def send_proxied_message(self, proxy_address, msg):
+ onu_device = self.adapter_agent.get_child_device(
+ self.device_id, onu_id=proxy_address.onu_id,
+ parent_port_no=self.platform.intf_id_to_port_no(
+ proxy_address.channel_id, Port.PON_OLT)
+ )
+ if onu_device.connect_status != ConnectStatus.REACHABLE:
+ self.log.debug('ONU is not reachable, cannot send OMCI',
+ serial_number=onu_device.serial_number,
+ intf_id=onu_device.proxy_address.channel_id,
+ onu_id=onu_device.proxy_address.onu_id)
+ return
+ omci = openolt_pb2.OmciMsg(intf_id=proxy_address.channel_id,
+ onu_id=proxy_address.onu_id, pkt=str(msg))
+ self.stub.OmciMsgOut(omci)
+
+ def add_onu_device(self, intf_id, port_no, onu_id, serial_number):
+ self.log.info("Adding ONU", port_no=port_no, onu_id=onu_id,
+ serial_number=serial_number)
+
+ # NOTE - channel_id of onu is set to intf_id
+ proxy_address = Device.ProxyAddress(device_id=self.device_id,
+ channel_id=intf_id, onu_id=onu_id,
+ onu_session_id=onu_id)
+
+ self.log.debug("Adding ONU", proxy_address=proxy_address)
+
+ serial_number_str = self.stringify_serial_number(serial_number)
+
+ self.adapter_agent.add_onu_device(
+ parent_device_id=self.device_id, parent_port_no=port_no,
+ vendor_id=serial_number.vendor_id, proxy_address=proxy_address,
+ root=True, serial_number=serial_number_str,
+ admin_state=AdminState.ENABLED#, **{'vlan':4091} # magic still maps to brcm_openomci_onu.pon_port.BRDCM_DEFAULT_VLAN
+ )
+
+ def port_name(self, port_no, port_type, intf_id=None, serial_number=None):
+ if port_type is Port.ETHERNET_NNI:
+ return "nni-" + str(port_no)
+ elif port_type is Port.PON_OLT:
+ return "pon" + str(intf_id)
+ elif port_type is Port.ETHERNET_UNI:
+ assert False, 'local UNI management not supported'
+
+ def add_logical_port(self, port_no, intf_id, oper_state):
+ self.log.info('adding-logical-port', port_no=port_no)
+
+ label = self.port_name(port_no, Port.ETHERNET_NNI)
+
+ cap = OFPPF_1GB_FD | OFPPF_FIBER
+ curr_speed = OFPPF_1GB_FD
+ max_speed = OFPPF_1GB_FD
+
+ if oper_state == OperStatus.ACTIVE:
+ of_oper_state = OFPPS_LIVE
+ else:
+ of_oper_state = OFPPS_LINK_DOWN
+
+ ofp = ofp_port(
+ port_no=port_no,
+ hw_addr=mac_str_to_tuple(self._get_mac_form_port_no(port_no)),
+ name=label, config=0, state=of_oper_state, curr=cap,
+ advertised=cap, peer=cap, curr_speed=curr_speed,
+ max_speed=max_speed)
+
+ ofp_stats = ofp_port_stats(port_no=port_no)
+
+ logical_port = LogicalPort(
+ id=label, ofp_port=ofp, device_id=self.device_id,
+ device_port_no=port_no, root_port=True,
+ ofp_port_stats=ofp_stats)
+
+ self.adapter_agent.add_logical_port(self.logical_device_id,
+ logical_port)
+
+ def _get_mac_form_port_no(self, port_no):
+ mac = ''
+ for i in range(4):
+ mac = ':%02x' % ((port_no >> (i * 8)) & 0xff) + mac
+ return '00:00' + mac
+
+ def add_port(self, intf_id, port_type, oper_status):
+ port_no = self.platform.intf_id_to_port_no(intf_id, port_type)
+
+ label = self.port_name(port_no, port_type, intf_id)
+
+ self.log.debug('adding-port', port_no=port_no, label=label,
+ port_type=port_type)
+
+ port = Port(port_no=port_no, label=label, type=port_type,
+ admin_state=AdminState.ENABLED, oper_status=oper_status)
+
+ self.adapter_agent.add_port(self.device_id, port)
+
+ return port_no, label
+
+ def delete_logical_port(self, child_device):
+ logical_ports = self.proxy.get('/logical_devices/{}/ports'.format(
+ self.logical_device_id))
+ for logical_port in logical_ports:
+ if logical_port.device_id == child_device.id:
+ self.log.debug('delete-logical-port',
+ onu_device_id=child_device.id,
+ logical_port=logical_port)
+ self.flow_mgr.clear_flows_and_scheduler_for_logical_port(
+ child_device, logical_port)
+ self.adapter_agent.delete_logical_port(
+ self.logical_device_id, logical_port)
+ return
+
+ def delete_port(self, child_serial_number):
+ ports = self.proxy.get('/devices/{}/ports'.format(
+ self.device_id))
+ for port in ports:
+ if port.label == child_serial_number:
+ self.log.debug('delete-port',
+ onu_serial_number=child_serial_number,
+ port=port)
+ self.adapter_agent.delete_port(self.device_id, port)
+ return
+
+ def update_flow_table(self, flows):
+ self.log.debug('No updates here now, all is done in logical flows '
+ 'update')
+
+ def update_logical_flows(self, flows_to_add, flows_to_remove,
+ device_rules_map):
+ if not self.is_state_up():
+ self.log.info('The OLT is not up, we cannot update flows',
+ flows_to_add=[f.id for f in flows_to_add],
+ flows_to_remove=[f.id for f in flows_to_remove])
+ return
+
+ try:
+ self.flow_mgr.update_children_flows(device_rules_map)
+ except Exception as e:
+ self.log.error('Error updating children flows', error=e)
+
+ self.log.debug('logical flows update', flows_to_add=flows_to_add,
+ flows_to_remove=flows_to_remove)
+
+ for flow in flows_to_add:
+
+ try:
+ self.flow_mgr.add_flow(flow)
+ except Exception as e:
+ self.log.error('failed to add flow', flow=flow, e=e)
+
+ for flow in flows_to_remove:
+
+ try:
+ self.flow_mgr.remove_flow(flow)
+ except Exception as e:
+ self.log.error('failed to remove flow', flow=flow, e=e)
+
+ self.flow_mgr.repush_all_different_flows()
+
+ # There has to be a better way to do this
+ def ip_hex(self, ip):
+ octets = ip.split(".")
+ hex_ip = []
+ for octet in octets:
+ octet_hex = hex(int(octet))
+ octet_hex = octet_hex.split('0x')[1]
+ octet_hex = octet_hex.rjust(2, '0')
+ hex_ip.append(octet_hex)
+ return ":".join(hex_ip)
+
+ def stringify_vendor_specific(self, vendor_specific):
+ return ''.join(str(i) for i in [
+ hex(ord(vendor_specific[0]) >> 4 & 0x0f)[2:],
+ hex(ord(vendor_specific[0]) & 0x0f)[2:],
+ hex(ord(vendor_specific[1]) >> 4 & 0x0f)[2:],
+ hex(ord(vendor_specific[1]) & 0x0f)[2:],
+ hex(ord(vendor_specific[2]) >> 4 & 0x0f)[2:],
+ hex(ord(vendor_specific[2]) & 0x0f)[2:],
+ hex(ord(vendor_specific[3]) >> 4 & 0x0f)[2:],
+ hex(ord(vendor_specific[3]) & 0x0f)[2:]])
+
+ def stringify_serial_number(self, serial_number):
+ return ''.join([serial_number.vendor_id,
+ self.stringify_vendor_specific(
+ serial_number.vendor_specific)])
+
+ def destringify_serial_number(self, serial_number_str):
+ serial_number = openolt_pb2.SerialNumber(
+ vendor_id=serial_number_str[:4].encode('utf-8'),
+ vendor_specific=binascii.unhexlify(serial_number_str[4:]))
+ return serial_number
+
+ def disable(self):
+ self.log.debug('sending-deactivate-olt-message',
+ device_id=self.device_id)
+
+ try:
+ # Send grpc call
+ self.stub.DisableOlt(openolt_pb2.Empty())
+ # The resulting indication will bring the OLT down
+ # self.go_state_down()
+ self.log.info('openolt device disabled')
+ except Exception as e:
+ self.log.error('Failure to disable openolt device', error=e)
+
+ def delete(self):
+ self.log.info('deleting-olt', device_id=self.device_id,
+ logical_device_id=self.logical_device_id)
+
+ # Clears up the data from the resource manager KV store
+ # for the device
+ del self.resource_mgr
+
+ try:
+ # Rebooting to reset the state
+ self.reboot()
+ # Removing logical device
+ ld = self.adapter_agent.get_logical_device(self.logical_device_id)
+ self.adapter_agent.delete_logical_device(ld)
+ except Exception as e:
+ self.log.error('Failure to delete openolt device', error=e)
+ raise e
+ else:
+ self.log.info('successfully-deleted-olt', device_id=self.device_id)
+
+ def reenable(self):
+ self.log.debug('reenabling-olt', device_id=self.device_id)
+
+ try:
+ self.stub.ReenableOlt(openolt_pb2.Empty())
+
+ self.log.info('enabling-all-ports', device_id=self.device_id)
+ self.adapter_agent.enable_all_ports(self.device_id)
+ except Exception as e:
+ self.log.error('Failure to reenable openolt device', error=e)
+ else:
+ self.log.info('openolt device reenabled')
+
+ def activate_onu(self, intf_id, onu_id, serial_number,
+ serial_number_str):
+ pir = self.bw_mgr.pir(serial_number_str)
+ self.log.debug("activating-onu", intf_id=intf_id, onu_id=onu_id,
+ serial_number_str=serial_number_str,
+ serial_number=serial_number, pir=pir)
+ onu = openolt_pb2.Onu(intf_id=intf_id, onu_id=onu_id,
+ serial_number=serial_number, pir=pir)
+ self.stub.ActivateOnu(onu)
+ self.log.info('onu-activated', serial_number=serial_number_str)
+
+ def delete_child_device(self, child_device):
+ self.log.debug('sending-deactivate-onu',
+ olt_device_id=self.device_id,
+ onu_device=child_device,
+ onu_serial_number=child_device.serial_number)
+ try:
+ self.adapter_agent.delete_child_device(self.device_id,
+ child_device.id,
+ child_device)
+ except Exception as e:
+ self.log.error('adapter_agent error', error=e)
+ try:
+ self.delete_logical_port(child_device)
+ except Exception as e:
+ self.log.error('logical_port delete error', error=e)
+ try:
+ self.delete_port(child_device.serial_number)
+ except Exception as e:
+ self.log.error('port delete error', error=e)
+ serial_number = self.destringify_serial_number(
+ child_device.serial_number)
+ # TODO FIXME - For each uni.
+ # TODO FIXME - Flows are not deleted
+ uni_id = 0 # FIXME
+ self.flow_mgr.delete_tech_profile_instance(
+ child_device.proxy_address.channel_id,
+ child_device.proxy_address.onu_id,
+ uni_id
+ )
+ pon_intf_id_onu_id = (child_device.proxy_address.channel_id,
+ child_device.proxy_address.onu_id,
+ uni_id)
+ # Free any PON resources that were reserved for the ONU
+ self.resource_mgr.free_pon_resources_for_onu(pon_intf_id_onu_id)
+
+ onu = openolt_pb2.Onu(intf_id=child_device.proxy_address.channel_id,
+ onu_id=child_device.proxy_address.onu_id,
+ serial_number=serial_number)
+ self.stub.DeleteOnu(onu)
+
+ def reboot(self):
+ self.log.debug('rebooting openolt device', device_id=self.device_id)
+ try:
+ self.stub.Reboot(openolt_pb2.Empty())
+ except Exception as e:
+ self.log.error('something went wrong with the reboot', error=e)
+ else:
+ self.log.info('device rebooted')
+
+ def trigger_statistics_collection(self):
+ try:
+ self.stub.CollectStatistics(openolt_pb2.Empty())
+ except Exception as e:
+ self.log.error('Error while triggering statistics collection',
+ error=e)
+ else:
+ self.log.info('statistics requested')
+
+ def simulate_alarm(self, alarm):
+ self.alarm_mgr.simulate_alarm(alarm)
diff --git a/python/adapters/openolt/openolt_flow_mgr.py b/python/adapters/openolt/openolt_flow_mgr.py
new file mode 100644
index 0000000..e298db2
--- /dev/null
+++ b/python/adapters/openolt/openolt_flow_mgr.py
@@ -0,0 +1,1062 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import copy
+from twisted.internet import reactor
+import grpc
+from google.protobuf.json_format import MessageToDict
+import hashlib
+from simplejson import dumps
+
+from voltha.protos.openflow_13_pb2 import OFPXMC_OPENFLOW_BASIC, \
+ ofp_flow_stats, OFPMT_OXM, Flows, FlowGroups, OFPXMT_OFB_IN_PORT, \
+ OFPXMT_OFB_VLAN_VID
+from voltha.protos.device_pb2 import Port
+import voltha.core.flow_decomposer as fd
+from voltha.adapters.openolt.protos import openolt_pb2
+from voltha.registry import registry
+
+from common.tech_profile.tech_profile import DEFAULT_TECH_PROFILE_TABLE_ID
+
+# Flow categories
+HSIA_FLOW = "HSIA_FLOW"
+
+EAP_ETH_TYPE = 0x888e
+LLDP_ETH_TYPE = 0x88cc
+
+IGMP_PROTO = 2
+
+# FIXME - see also BRDCM_DEFAULT_VLAN in broadcom_onu.py
+DEFAULT_MGMT_VLAN = 4091
+
+# Openolt Flow
+UPSTREAM = "upstream"
+DOWNSTREAM = "downstream"
+PACKET_TAG_TYPE = "pkt_tag_type"
+UNTAGGED = "untagged"
+SINGLE_TAG = "single_tag"
+DOUBLE_TAG = "double_tag"
+
+# Classifier
+ETH_TYPE = 'eth_type'
+TPID = 'tpid'
+IP_PROTO = 'ip_proto'
+IN_PORT = 'in_port'
+VLAN_VID = 'vlan_vid'
+VLAN_PCP = 'vlan_pcp'
+UDP_DST = 'udp_dst'
+UDP_SRC = 'udp_src'
+IPV4_DST = 'ipv4_dst'
+IPV4_SRC = 'ipv4_src'
+METADATA = 'metadata'
+OUTPUT = 'output'
+# Action
+POP_VLAN = 'pop_vlan'
+PUSH_VLAN = 'push_vlan'
+TRAP_TO_HOST = 'trap_to_host'
+
+
+class OpenOltFlowMgr(object):
+
+ def __init__(self, adapter_agent, log, stub, device_id, logical_device_id,
+ platform, resource_mgr):
+ self.adapter_agent = adapter_agent
+ self.log = log
+ self.stub = stub
+ self.device_id = device_id
+ self.logical_device_id = logical_device_id
+ self.nni_intf_id = None
+ self.platform = platform
+ self.logical_flows_proxy = registry('core').get_proxy(
+ '/logical_devices/{}/flows'.format(self.logical_device_id))
+ self.flows_proxy = registry('core').get_proxy(
+ '/devices/{}/flows'.format(self.device_id))
+ self.root_proxy = registry('core').get_proxy('/')
+ self.resource_mgr = resource_mgr
+ self.tech_profile = dict()
+ self._populate_tech_profile_per_pon_port()
+ self.retry_add_flow_list = []
+
+ def add_flow(self, flow):
+ self.log.debug('add flow', flow=flow)
+ classifier_info = dict()
+ action_info = dict()
+
+ for field in fd.get_ofb_fields(flow):
+ if field.type == fd.ETH_TYPE:
+ classifier_info[ETH_TYPE] = field.eth_type
+ self.log.debug('field-type-eth-type',
+ eth_type=classifier_info[ETH_TYPE])
+ elif field.type == fd.IP_PROTO:
+ classifier_info[IP_PROTO] = field.ip_proto
+ self.log.debug('field-type-ip-proto',
+ ip_proto=classifier_info[IP_PROTO])
+ elif field.type == fd.IN_PORT:
+ classifier_info[IN_PORT] = field.port
+ self.log.debug('field-type-in-port',
+ in_port=classifier_info[IN_PORT])
+ elif field.type == fd.VLAN_VID:
+ classifier_info[VLAN_VID] = field.vlan_vid & 0xfff
+ self.log.debug('field-type-vlan-vid',
+ vlan=classifier_info[VLAN_VID])
+ elif field.type == fd.VLAN_PCP:
+ classifier_info[VLAN_PCP] = field.vlan_pcp
+ self.log.debug('field-type-vlan-pcp',
+ pcp=classifier_info[VLAN_PCP])
+ elif field.type == fd.UDP_DST:
+ classifier_info[UDP_DST] = field.udp_dst
+ self.log.debug('field-type-udp-dst',
+ udp_dst=classifier_info[UDP_DST])
+ elif field.type == fd.UDP_SRC:
+ classifier_info[UDP_SRC] = field.udp_src
+ self.log.debug('field-type-udp-src',
+ udp_src=classifier_info[UDP_SRC])
+ elif field.type == fd.IPV4_DST:
+ classifier_info[IPV4_DST] = field.ipv4_dst
+ self.log.debug('field-type-ipv4-dst',
+ ipv4_dst=classifier_info[IPV4_DST])
+ elif field.type == fd.IPV4_SRC:
+ classifier_info[IPV4_SRC] = field.ipv4_src
+ self.log.debug('field-type-ipv4-src',
+ ipv4_dst=classifier_info[IPV4_SRC])
+ elif field.type == fd.METADATA:
+ classifier_info[METADATA] = field.table_metadata
+ self.log.debug('field-type-metadata',
+ metadata=classifier_info[METADATA])
+ else:
+ raise NotImplementedError('field.type={}'.format(
+ field.type))
+
+ for action in fd.get_actions(flow):
+ if action.type == fd.OUTPUT:
+ action_info[OUTPUT] = action.output.port
+ self.log.debug('action-type-output',
+ output=action_info[OUTPUT],
+ in_port=classifier_info[IN_PORT])
+ elif action.type == fd.POP_VLAN:
+ if fd.get_goto_table_id(flow) is None:
+ self.log.debug('being taken care of by ONU', flow=flow)
+ return
+ action_info[POP_VLAN] = True
+ self.log.debug('action-type-pop-vlan',
+ in_port=classifier_info[IN_PORT])
+ elif action.type == fd.PUSH_VLAN:
+ action_info[PUSH_VLAN] = True
+ action_info[TPID] = action.push.ethertype
+ self.log.debug('action-type-push-vlan',
+ push_tpid=action_info[TPID], in_port=classifier_info[IN_PORT])
+ if action.push.ethertype != 0x8100:
+ self.log.error('unhandled-tpid',
+ ethertype=action.push.ethertype)
+ elif action.type == fd.SET_FIELD:
+ # action_info['action_type'] = 'set_field'
+ _field = action.set_field.field.ofb_field
+ assert (action.set_field.field.oxm_class ==
+ OFPXMC_OPENFLOW_BASIC)
+ self.log.debug('action-type-set-field',
+ field=_field, in_port=classifier_info[IN_PORT])
+ if _field.type == fd.VLAN_VID:
+ self.log.debug('set-field-type-vlan-vid',
+ vlan_vid=_field.vlan_vid & 0xfff)
+ action_info[VLAN_VID] = (_field.vlan_vid & 0xfff)
+ else:
+ self.log.error('unsupported-action-set-field-type',
+ field_type=_field.type)
+ else:
+ self.log.error('unsupported-action-type',
+ action_type=action.type, in_port=classifier_info[IN_PORT])
+
+ if fd.get_goto_table_id(flow) is not None and POP_VLAN not in action_info:
+ self.log.debug('being taken care of by ONU', flow=flow)
+ return
+
+ if OUTPUT not in action_info and METADATA in classifier_info:
+ # find flow in the next table
+ next_flow = self.find_next_flow(flow)
+ if next_flow is None:
+ return
+ action_info[OUTPUT] = fd.get_out_port(next_flow)
+ for field in fd.get_ofb_fields(next_flow):
+ if field.type == fd.VLAN_VID:
+ classifier_info[METADATA] = field.vlan_vid & 0xfff
+
+ self.log.debug('flow-ports', classifier_inport=classifier_info[IN_PORT], action_output=action_info[OUTPUT])
+ (port_no, intf_id, onu_id, uni_id) = self.platform.extract_access_from_flow(
+ classifier_info[IN_PORT], action_info[OUTPUT])
+
+ self.divide_and_add_flow(intf_id, onu_id, uni_id, port_no, classifier_info,
+ action_info, flow)
+
+ def _is_uni_port(self, port_no):
+ try:
+ port = self.adapter_agent.get_logical_port(self.logical_device_id,
+ 'uni-{}'.format(port_no))
+ if port is not None:
+ return (not port.root_port), port.device_id
+ else:
+ return False, None
+ except Exception as e:
+ self.log.error("error-retrieving-port", e=e)
+ return False, None
+
+ def _clear_flow_id_from_rm(self, flow, flow_id, flow_direction):
+ uni_port_no = None
+ child_device_id = None
+ if flow_direction == UPSTREAM:
+ for field in fd.get_ofb_fields(flow):
+ if field.type == fd.IN_PORT:
+ is_uni, child_device_id = self._is_uni_port(field.port)
+ if is_uni:
+ uni_port_no = field.port
+ elif flow_direction == DOWNSTREAM:
+ for field in fd.get_ofb_fields(flow):
+ if field.type == fd.METADATA:
+ uni_port = field.table_metadata & 0xFFFFFFFF
+ is_uni, child_device_id = self._is_uni_port(uni_port)
+ if is_uni:
+ uni_port_no = field.port
+
+ if uni_port_no is None:
+ for action in fd.get_actions(flow):
+ if action.type == fd.OUTPUT:
+ is_uni, child_device_id = \
+ self._is_uni_port(action.output.port)
+ if is_uni:
+ uni_port_no = action.output.port
+
+ if child_device_id:
+ child_device = self.adapter_agent.get_device(child_device_id)
+ pon_intf = child_device.proxy_address.channel_id
+ onu_id = child_device.proxy_address.onu_id
+ uni_id = self.platform.uni_id_from_port_num(uni_port_no) if uni_port_no is not None else None
+ flows = self.resource_mgr.get_flow_id_info(pon_intf, onu_id, uni_id, flow_id)
+ assert (isinstance(flows, list))
+ self.log.debug("retrieved-flows", flows=flows)
+ for idx in range(len(flows)):
+ if flow_direction == flows[idx]['flow_type']:
+ flows.pop(idx)
+ self.update_flow_info_to_kv_store(pon_intf, onu_id, uni_id, flow_id, flows)
+ if len(flows) > 0:
+ # There are still flows referencing the same flow_id.
+ # So the flow should not be freed yet.
+ # For ex: Case of HSIA where same flow is shared
+ # between DS and US.
+ return
+
+ self.resource_mgr.free_flow_id_for_uni(pon_intf, onu_id, uni_id, flow_id)
+ else:
+ self.log.error("invalid-info", uni_port_no=uni_port_no,
+ child_device_id=child_device_id)
+
+ def retry_add_flow(self, flow):
+ self.log.debug("retry-add-flow")
+ if flow.id in self.retry_add_flow_list:
+ self.retry_add_flow_list.remove(flow.id)
+ self.add_flow(flow)
+
+ def remove_flow(self, flow):
+ self.log.debug('trying to remove flows from logical flow :',
+ logical_flow=flow)
+ device_flows_to_remove = []
+ device_flows = self.flows_proxy.get('/').items
+ for f in device_flows:
+ if f.cookie == flow.id:
+ device_flows_to_remove.append(f)
+
+ for f in device_flows_to_remove:
+ (id, direction) = self.decode_stored_id(f.id)
+ flow_to_remove = openolt_pb2.Flow(flow_id=id, flow_type=direction)
+ try:
+ self.stub.FlowRemove(flow_to_remove)
+ except grpc.RpcError as grpc_e:
+ if grpc_e.code() == grpc.StatusCode.NOT_FOUND:
+ self.log.debug('This flow does not exist on the switch, '
+ 'normal after an OLT reboot',
+ flow=flow_to_remove)
+ else:
+ raise grpc_e
+
+ # once we have successfully deleted the flow on the device
+ # release the flow_id on resource pool and also clear any
+ # data associated with the flow_id on KV store.
+ self._clear_flow_id_from_rm(f, id, direction)
+ self.log.debug('flow removed from device', flow=f,
+ flow_key=flow_to_remove)
+
+ if len(device_flows_to_remove) > 0:
+ new_flows = []
+ flows_ids_to_remove = [f.id for f in device_flows_to_remove]
+ for f in device_flows:
+ if f.id not in flows_ids_to_remove:
+ new_flows.append(f)
+
+ self.flows_proxy.update('/', Flows(items=new_flows))
+ self.log.debug('flows removed from the data store',
+ flow_ids_removed=flows_ids_to_remove,
+ number_of_flows_removed=(len(device_flows) - len(
+ new_flows)), expected_flows_removed=len(
+ device_flows_to_remove))
+ else:
+ self.log.debug('no device flow to remove for this flow (normal '
+ 'for multi table flows)', flow=flow)
+
+ def _get_ofp_port_name(self, intf_id, onu_id, uni_id):
+ parent_port_no = self.platform.intf_id_to_port_no(intf_id, Port.PON_OLT)
+ child_device = self.adapter_agent.get_child_device(self.device_id,
+ parent_port_no=parent_port_no, onu_id=onu_id)
+ if child_device is None:
+ self.log.error("could-not-find-child-device",
+ parent_port_no=intf_id, onu_id=onu_id)
+ return (None, None)
+ ports = self.adapter_agent.get_ports(child_device.id, Port.ETHERNET_UNI)
+ logical_port = self.adapter_agent.get_logical_port(
+ self.logical_device_id, ports[uni_id].label)
+ ofp_port_name = (logical_port.ofp_port.name, logical_port.ofp_port.port_no)
+ return ofp_port_name
+
+ def get_tp_path(self, intf_id, ofp_port_name):
+ # FIXME Should get Table id form the flow, as of now hardcoded to
+ # DEFAULT_TECH_PROFILE_TABLE_ID (64)
+ # 'tp_path' contains the suffix part of the tech_profile_instance path.
+ # The prefix to the 'tp_path' should be set to \
+ # TechProfile.KV_STORE_TECH_PROFILE_PATH_PREFIX by the ONU adapter.
+ return self.tech_profile[intf_id]. \
+ get_tp_path(DEFAULT_TECH_PROFILE_TABLE_ID,
+ ofp_port_name)
+
+ def delete_tech_profile_instance(self, intf_id, onu_id, uni_id):
+ # Remove the TP instance associated with the ONU
+ ofp_port_name = self._get_ofp_port_name(intf_id, onu_id, uni_id)
+ tp_path = self.get_tp_path(intf_id, ofp_port_name)
+ return self.tech_profile[intf_id].delete_tech_profile_instance(tp_path)
+
+ def divide_and_add_flow(self, intf_id, onu_id, uni_id, port_no, classifier,
+ action, flow):
+
+ self.log.debug('sorting flow', intf_id=intf_id, onu_id=onu_id, uni_id=uni_id, port_no=port_no,
+ classifier=classifier, action=action)
+
+ alloc_id, gem_ports = self.create_tcont_gemport(intf_id, onu_id, uni_id,
+ flow.table_id)
+ if alloc_id is None or gem_ports is None:
+ self.log.error("alloc-id-gem-ports-unavailable", alloc_id=alloc_id,
+ gem_ports=gem_ports)
+ return
+
+ self.log.debug('Generated required alloc and gemport ids',
+ alloc_id=alloc_id, gemports=gem_ports)
+
+ # Flows can't be added specific to gemport unless p-bits are received.
+ # Hence adding flows for all gemports
+ for gemport_id in gem_ports:
+ if IP_PROTO in classifier:
+ if classifier[IP_PROTO] == 17:
+ self.log.debug('dhcp flow add')
+ self.add_dhcp_trap(intf_id, onu_id, uni_id, port_no, classifier,
+ action, flow, alloc_id, gemport_id)
+ elif classifier[IP_PROTO] == 2:
+ self.log.warn('igmp flow add ignored, not implemented yet')
+ else:
+ self.log.warn("Invalid-Classifier-to-handle",
+ classifier=classifier,
+ action=action)
+ elif ETH_TYPE in classifier:
+ if classifier[ETH_TYPE] == EAP_ETH_TYPE:
+ self.log.debug('eapol flow add')
+ self.add_eapol_flow(intf_id, onu_id, uni_id, port_no, flow, alloc_id,
+ gemport_id)
+ vlan_id = self.get_subscriber_vlan(fd.get_in_port(flow))
+ if vlan_id is not None:
+ self.add_eapol_flow(
+ intf_id, onu_id, uni_id, port_no, flow, alloc_id, gemport_id,
+ vlan_id=vlan_id)
+ parent_port_no = self.platform.intf_id_to_port_no(intf_id, Port.PON_OLT)
+ onu_device = self.adapter_agent.get_child_device(self.device_id,
+ onu_id=onu_id,
+ parent_port_no=parent_port_no)
+ (ofp_port_name, ofp_port_no) = self._get_ofp_port_name(intf_id, onu_id, uni_id)
+ if ofp_port_name is None:
+ self.log.error("port-name-not-found")
+ return
+
+ tp_path = self.get_tp_path(intf_id, ofp_port_name)
+
+ self.log.debug('Load-tech-profile-request-to-brcm-handler',
+ tp_path=tp_path)
+ msg = {'proxy_address': onu_device.proxy_address, 'uni_id': uni_id,
+ 'event': 'download_tech_profile', 'event_data': tp_path}
+
+ # Send the event message to the ONU adapter
+ self.adapter_agent.publish_inter_adapter_message(onu_device.id,
+ msg)
+
+ if classifier[ETH_TYPE] == LLDP_ETH_TYPE:
+ self.log.debug('lldp flow add')
+ nni_intf_id = self.get_nni_intf_id()
+ self.add_lldp_flow(flow, port_no, nni_intf_id)
+
+ elif PUSH_VLAN in action:
+ self.add_upstream_data_flow(intf_id, onu_id, uni_id, port_no, classifier,
+ action, flow, alloc_id, gemport_id)
+ elif POP_VLAN in action:
+ self.add_downstream_data_flow(intf_id, onu_id, uni_id, port_no, classifier,
+ action, flow, alloc_id, gemport_id)
+ else:
+ self.log.debug('Invalid-flow-type-to-handle',
+ classifier=classifier,
+ action=action, flow=flow)
+
+ def create_tcont_gemport(self, intf_id, onu_id, uni_id, table_id):
+ alloc_id, gem_port_ids = None, None
+ pon_intf_onu_id = (intf_id, onu_id)
+
+ # If we already have allocated alloc_id and gem_ports earlier, render them
+ alloc_id = \
+ self.resource_mgr.get_current_alloc_ids_for_onu(pon_intf_onu_id)
+ gem_port_ids = \
+ self.resource_mgr.get_current_gemport_ids_for_onu(pon_intf_onu_id)
+ if alloc_id is not None and gem_port_ids is not None:
+ return alloc_id, gem_port_ids
+
+ try:
+ (ofp_port_name, ofp_port_no) = self._get_ofp_port_name(intf_id, onu_id, uni_id)
+ if ofp_port_name is None:
+ self.log.error("port-name-not-found")
+ return alloc_id, gem_port_ids
+ # FIXME: If table id is <= 63 using 64 as table id
+ if table_id < DEFAULT_TECH_PROFILE_TABLE_ID:
+ table_id = DEFAULT_TECH_PROFILE_TABLE_ID
+
+ # Check tech profile instance already exists for derived port name
+ tech_profile_instance = self.tech_profile[intf_id]. \
+ get_tech_profile_instance(table_id, ofp_port_name)
+ self.log.debug('Get-tech-profile-instance-status', tech_profile_instance=tech_profile_instance)
+
+ if tech_profile_instance is None:
+ # create tech profile instance
+ tech_profile_instance = self.tech_profile[intf_id]. \
+ create_tech_profile_instance(table_id, ofp_port_name,
+ intf_id)
+ if tech_profile_instance is None:
+ raise Exception('Tech-profile-instance-creation-failed')
+ else:
+ self.log.debug(
+ 'Tech-profile-instance-already-exist-for-given port-name',
+ ofp_port_name=ofp_port_name)
+
+ # upstream scheduler
+ us_scheduler = self.tech_profile[intf_id].get_us_scheduler(
+ tech_profile_instance)
+ # downstream scheduler
+ ds_scheduler = self.tech_profile[intf_id].get_ds_scheduler(
+ tech_profile_instance)
+ # create Tcont
+ tconts = self.tech_profile[intf_id].get_tconts(tech_profile_instance,
+ us_scheduler,
+ ds_scheduler)
+
+ self.stub.CreateTconts(openolt_pb2.Tconts(intf_id=intf_id,
+ onu_id=onu_id,
+ uni_id=uni_id,
+ port_no=ofp_port_no,
+ tconts=tconts))
+
+ # Fetch alloc id and gemports from tech profile instance
+ alloc_id = tech_profile_instance.us_scheduler.alloc_id
+ gem_port_ids = []
+ for i in range(len(
+ tech_profile_instance.upstream_gem_port_attribute_list)):
+ gem_port_ids.append(
+ tech_profile_instance.upstream_gem_port_attribute_list[i].
+ gemport_id)
+ except BaseException as e:
+ self.log.exception(exception=e)
+
+ # Update the allocated alloc_id and gem_port_id for the ONU/UNI to KV store
+ pon_intf_onu_id = (intf_id, onu_id, uni_id)
+ self.resource_mgr.resource_mgrs[intf_id].update_alloc_ids_for_onu(
+ pon_intf_onu_id,
+ list([alloc_id])
+ )
+ self.resource_mgr.resource_mgrs[intf_id].update_gemport_ids_for_onu(
+ pon_intf_onu_id,
+ gem_port_ids
+ )
+
+ self.resource_mgr.update_gemports_ponport_to_onu_map_on_kv_store(
+ gem_port_ids, intf_id, onu_id, uni_id
+ )
+
+ return alloc_id, gem_port_ids
+
+ def add_upstream_data_flow(self, intf_id, onu_id, uni_id, port_no, uplink_classifier,
+ uplink_action, logical_flow, alloc_id,
+ gemport_id):
+
+ uplink_classifier[PACKET_TAG_TYPE] = SINGLE_TAG
+
+ self.add_hsia_flow(intf_id, onu_id, uni_id, port_no, uplink_classifier,
+ uplink_action, UPSTREAM,
+ logical_flow, alloc_id, gemport_id)
+
+ # Secondary EAP on the subscriber vlan
+ (eap_active, eap_logical_flow) = self.is_eap_enabled(intf_id, onu_id, uni_id)
+ if eap_active:
+ self.add_eapol_flow(intf_id, onu_id, uni_id, port_no, eap_logical_flow, alloc_id,
+ gemport_id, vlan_id=uplink_classifier[VLAN_VID])
+
+ def add_downstream_data_flow(self, intf_id, onu_id, uni_id, port_no, downlink_classifier,
+ downlink_action, flow, alloc_id, gemport_id):
+ downlink_classifier[PACKET_TAG_TYPE] = DOUBLE_TAG
+ # Needed ???? It should be already there
+ downlink_action[POP_VLAN] = True
+ downlink_action[VLAN_VID] = downlink_classifier[VLAN_VID]
+
+ self.add_hsia_flow(intf_id, onu_id, uni_id, port_no, downlink_classifier,
+ downlink_action, DOWNSTREAM,
+ flow, alloc_id, gemport_id)
+
+ def add_hsia_flow(self, intf_id, onu_id, uni_id, port_no, classifier, action,
+ direction, logical_flow, alloc_id, gemport_id):
+
+ flow_store_cookie = self._get_flow_store_cookie(classifier,
+ gemport_id)
+
+ # One of the OLT platform (Broadcom BAL) requires that symmetric
+ # flows require the same flow_id to be used across UL and DL.
+ # Since HSIA flow is the only symmetric flow currently, we need to
+ # re-use the flow_id across both direction. The 'flow_category'
+ # takes priority over flow_cookie to find any available HSIA_FLOW
+ # id for the ONU.
+ flow_id = self.resource_mgr.get_flow_id(intf_id, onu_id, uni_id,
+ flow_store_cookie,
+ HSIA_FLOW)
+ if flow_id is None:
+ self.log.error("hsia-flow-unavailable")
+ return
+ flow = openolt_pb2.Flow(
+ access_intf_id=intf_id, onu_id=onu_id, uni_id=uni_id, flow_id=flow_id,
+ flow_type=direction, alloc_id=alloc_id, network_intf_id=self.get_nni_intf_id(),
+ gemport_id=gemport_id,
+ classifier=self.mk_classifier(classifier),
+ action=self.mk_action(action),
+ priority=logical_flow.priority,
+ port_no=port_no,
+ cookie=logical_flow.cookie)
+
+ if self.add_flow_to_device(flow, logical_flow):
+ flow_info = self._get_flow_info_as_json_blob(flow,
+ flow_store_cookie,
+ HSIA_FLOW)
+ self.update_flow_info_to_kv_store(flow.access_intf_id,
+ flow.onu_id, flow.uni_id,
+ flow.flow_id, flow_info)
+
+ def add_dhcp_trap(self, intf_id, onu_id, uni_id, port_no, classifier, action, logical_flow,
+ alloc_id, gemport_id):
+
+ self.log.debug('add dhcp upstream trap', classifier=classifier,
+ intf_id=intf_id, onu_id=onu_id, uni_id=uni_id, action=action)
+
+ action.clear()
+ action[TRAP_TO_HOST] = True
+ classifier[UDP_SRC] = 68
+ classifier[UDP_DST] = 67
+ classifier[PACKET_TAG_TYPE] = SINGLE_TAG
+ classifier.pop(VLAN_VID, None)
+
+ flow_store_cookie = self._get_flow_store_cookie(classifier,
+ gemport_id)
+
+ flow_id = self.resource_mgr.get_flow_id(
+ intf_id, onu_id, uni_id, flow_store_cookie
+ )
+ dhcp_flow = openolt_pb2.Flow(
+ onu_id=onu_id, uni_id=uni_id, flow_id=flow_id, flow_type=UPSTREAM,
+ access_intf_id=intf_id, gemport_id=gemport_id,
+ alloc_id=alloc_id, network_intf_id=self.get_nni_intf_id(),
+ priority=logical_flow.priority,
+ classifier=self.mk_classifier(classifier),
+ action=self.mk_action(action),
+ port_no=port_no,
+ cookie=logical_flow.cookie)
+
+ if self.add_flow_to_device(dhcp_flow, logical_flow):
+ flow_info = self._get_flow_info_as_json_blob(dhcp_flow, flow_store_cookie)
+ self.update_flow_info_to_kv_store(dhcp_flow.access_intf_id,
+ dhcp_flow.onu_id,
+ dhcp_flow.uni_id,
+ dhcp_flow.flow_id,
+ flow_info)
+
+ def add_eapol_flow(self, intf_id, onu_id, uni_id, port_no, logical_flow, alloc_id,
+ gemport_id, vlan_id=DEFAULT_MGMT_VLAN):
+
+ uplink_classifier = dict()
+ uplink_classifier[ETH_TYPE] = EAP_ETH_TYPE
+ uplink_classifier[PACKET_TAG_TYPE] = SINGLE_TAG
+ uplink_classifier[VLAN_VID] = vlan_id
+
+ uplink_action = dict()
+ uplink_action[TRAP_TO_HOST] = True
+
+ flow_store_cookie = self._get_flow_store_cookie(uplink_classifier,
+ gemport_id)
+ # Add Upstream EAPOL Flow.
+ uplink_flow_id = self.resource_mgr.get_flow_id(
+ intf_id, onu_id, uni_id, flow_store_cookie
+ )
+
+ upstream_flow = openolt_pb2.Flow(
+ access_intf_id=intf_id, onu_id=onu_id, uni_id=uni_id, flow_id=uplink_flow_id,
+ flow_type=UPSTREAM, alloc_id=alloc_id, network_intf_id=self.get_nni_intf_id(),
+ gemport_id=gemport_id,
+ classifier=self.mk_classifier(uplink_classifier),
+ action=self.mk_action(uplink_action),
+ priority=logical_flow.priority,
+ port_no=port_no,
+ cookie=logical_flow.cookie)
+
+ logical_flow = copy.deepcopy(logical_flow)
+ logical_flow.match.oxm_fields.extend(fd.mk_oxm_fields([fd.vlan_vid(
+ vlan_id | 0x1000)]))
+ logical_flow.match.type = OFPMT_OXM
+
+ if self.add_flow_to_device(upstream_flow, logical_flow):
+ flow_info = self._get_flow_info_as_json_blob(upstream_flow,
+ flow_store_cookie)
+ self.update_flow_info_to_kv_store(upstream_flow.access_intf_id,
+ upstream_flow.onu_id,
+ upstream_flow.uni_id,
+ upstream_flow.flow_id,
+ flow_info)
+
+ if vlan_id == DEFAULT_MGMT_VLAN:
+ # Add Downstream EAPOL Flow, Only for first EAP flow (BAL
+ # requirement)
+ # On one of the platforms (Broadcom BAL), when same DL classifier
+ # vlan was used across multiple ONUs, eapol flow re-adds after
+ # flow delete (cases of onu reboot/disable) fails.
+ # In order to generate unique vlan, a combination of intf_id
+ # onu_id and uni_id is used.
+ # uni_id defaults to 0, so add 1 to it.
+ special_vlan_downstream_flow = 4090 - intf_id * onu_id * (uni_id+1)
+ # Assert that we do not generate invalid vlans under no condition
+ assert (special_vlan_downstream_flow >= 2, 'invalid-vlan-generated')
+
+ downlink_classifier = dict()
+ downlink_classifier[PACKET_TAG_TYPE] = SINGLE_TAG
+ downlink_classifier[VLAN_VID] = special_vlan_downstream_flow
+
+ downlink_action = dict()
+ downlink_action[PUSH_VLAN] = True
+ downlink_action[VLAN_VID] = vlan_id
+
+
+ flow_store_cookie = self._get_flow_store_cookie(downlink_classifier,
+ gemport_id)
+
+ downlink_flow_id = self.resource_mgr.get_flow_id(
+ intf_id, onu_id, uni_id, flow_store_cookie
+ )
+
+ downstream_flow = openolt_pb2.Flow(
+ access_intf_id=intf_id, onu_id=onu_id, uni_id=uni_id, flow_id=downlink_flow_id,
+ flow_type=DOWNSTREAM, alloc_id=alloc_id, network_intf_id=self.get_nni_intf_id(),
+ gemport_id=gemport_id,
+ classifier=self.mk_classifier(downlink_classifier),
+ action=self.mk_action(downlink_action),
+ priority=logical_flow.priority,
+ port_no=port_no,
+ cookie=logical_flow.cookie)
+
+ downstream_logical_flow = ofp_flow_stats(
+ id=logical_flow.id, cookie=logical_flow.cookie,
+ table_id=logical_flow.table_id, priority=logical_flow.priority,
+ flags=logical_flow.flags)
+
+ downstream_logical_flow.match.oxm_fields.extend(fd.mk_oxm_fields([
+ fd.in_port(fd.get_out_port(logical_flow)),
+ fd.vlan_vid(special_vlan_downstream_flow | 0x1000)]))
+ downstream_logical_flow.match.type = OFPMT_OXM
+
+ downstream_logical_flow.instructions.extend(
+ fd.mk_instructions_from_actions([fd.output(
+ self.platform.mk_uni_port_num(intf_id, onu_id, uni_id))]))
+
+ if self.add_flow_to_device(downstream_flow, downstream_logical_flow):
+ flow_info = self._get_flow_info_as_json_blob(downstream_flow,
+ flow_store_cookie)
+ self.update_flow_info_to_kv_store(downstream_flow.access_intf_id,
+ downstream_flow.onu_id,
+ downstream_flow.uni_id,
+ downstream_flow.flow_id,
+ flow_info)
+
+ def repush_all_different_flows(self):
+ # Check if the device is supposed to have flows, if so add them
+ # Recover static flows after a reboot
+ logical_flows = self.logical_flows_proxy.get('/').items
+ devices_flows = self.flows_proxy.get('/').items
+ logical_flows_ids_provisioned = [f.cookie for f in devices_flows]
+ for logical_flow in logical_flows:
+ try:
+ if logical_flow.id not in logical_flows_ids_provisioned:
+ self.add_flow(logical_flow)
+ except Exception as e:
+ self.log.exception('Problem reading this flow', e=e)
+
+ def reset_flows(self):
+ self.flows_proxy.update('/', Flows())
+
+ """ Add a downstream LLDP trap flow on the NNI interface
+ """
+
+ def add_lldp_flow(self, logical_flow, port_no, network_intf_id=0):
+
+ classifier = dict()
+ classifier[ETH_TYPE] = LLDP_ETH_TYPE
+ classifier[PACKET_TAG_TYPE] = UNTAGGED
+ action = dict()
+ action[TRAP_TO_HOST] = True
+
+ # LLDP flow is installed to trap LLDP packets on the NNI port.
+ # We manage flow_id resource pool on per PON port basis.
+ # Since this situation is tricky, as a hack, we pass the NNI port
+ # index (network_intf_id) as PON port Index for the flow_id resource
+ # pool. Also, there is no ONU Id available for trapping LLDP packets
+ # on NNI port, use onu_id as -1 (invalid)
+ # ****************** CAVEAT *******************
+ # This logic works if the NNI Port Id falls within the same valid
+ # range of PON Port Ids. If this doesn't work for some OLT Vendor
+ # we need to have a re-look at this.
+ # *********************************************
+ onu_id = -1
+ uni_id = -1
+ flow_store_cookie = self._get_flow_store_cookie(classifier)
+ flow_id = self.resource_mgr.get_flow_id(network_intf_id, onu_id, uni_id,
+ flow_store_cookie)
+
+ downstream_flow = openolt_pb2.Flow(
+ access_intf_id=-1, # access_intf_id not required
+ onu_id=onu_id, # onu_id not required
+ uni_id=uni_id, # uni_id not used
+ flow_id=flow_id,
+ flow_type=DOWNSTREAM,
+ network_intf_id=network_intf_id,
+ gemport_id=-1, # gemport_id not required
+ classifier=self.mk_classifier(classifier),
+ action=self.mk_action(action),
+ priority=logical_flow.priority,
+ port_no=port_no,
+ cookie=logical_flow.cookie)
+
+ self.log.debug('add lldp downstream trap', classifier=classifier,
+ action=action, flow=downstream_flow, port_no=port_no)
+ if self.add_flow_to_device(downstream_flow, logical_flow):
+ self.update_flow_info_to_kv_store(network_intf_id, onu_id, uni_id,
+ flow_id, downstream_flow)
+
+ def mk_classifier(self, classifier_info):
+
+ classifier = openolt_pb2.Classifier()
+
+ if ETH_TYPE in classifier_info:
+ classifier.eth_type = classifier_info[ETH_TYPE]
+ if IP_PROTO in classifier_info:
+ classifier.ip_proto = classifier_info[IP_PROTO]
+ if VLAN_VID in classifier_info:
+ classifier.o_vid = classifier_info[VLAN_VID]
+ if METADATA in classifier_info:
+ classifier.i_vid = classifier_info[METADATA]
+ if VLAN_PCP in classifier_info:
+ classifier.o_pbits = classifier_info[VLAN_PCP]
+ if UDP_SRC in classifier_info:
+ classifier.src_port = classifier_info[UDP_SRC]
+ if UDP_DST in classifier_info:
+ classifier.dst_port = classifier_info[UDP_DST]
+ if IPV4_DST in classifier_info:
+ classifier.dst_ip = classifier_info[IPV4_DST]
+ if IPV4_SRC in classifier_info:
+ classifier.src_ip = classifier_info[IPV4_SRC]
+ if PACKET_TAG_TYPE in classifier_info:
+ if classifier_info[PACKET_TAG_TYPE] == SINGLE_TAG:
+ classifier.pkt_tag_type = SINGLE_TAG
+ elif classifier_info[PACKET_TAG_TYPE] == DOUBLE_TAG:
+ classifier.pkt_tag_type = DOUBLE_TAG
+ elif classifier_info[PACKET_TAG_TYPE] == UNTAGGED:
+ classifier.pkt_tag_type = UNTAGGED
+ else:
+ classifier.pkt_tag_type = 'none'
+
+ return classifier
+
+ def mk_action(self, action_info):
+ action = openolt_pb2.Action()
+
+ if POP_VLAN in action_info:
+ action.o_vid = action_info[VLAN_VID]
+ action.cmd.remove_outer_tag = True
+ elif PUSH_VLAN in action_info:
+ action.o_vid = action_info[VLAN_VID]
+ action.cmd.add_outer_tag = True
+ elif TRAP_TO_HOST in action_info:
+ action.cmd.trap_to_host = True
+ else:
+ self.log.info('Invalid-action-field', action_info=action_info)
+ return
+ return action
+
+ def is_eap_enabled(self, intf_id, onu_id, uni_id):
+ flows = self.logical_flows_proxy.get('/').items
+
+ for flow in flows:
+ eap_flow = False
+ eap_intf_id = None
+ eap_onu_id = None
+ eap_uni_id = None
+ for field in fd.get_ofb_fields(flow):
+ if field.type == fd.ETH_TYPE:
+ if field.eth_type == EAP_ETH_TYPE:
+ eap_flow = True
+ if field.type == fd.IN_PORT:
+ eap_intf_id = self.platform.intf_id_from_uni_port_num(
+ field.port)
+ eap_onu_id = self.platform.onu_id_from_port_num(field.port)
+ eap_uni_id = self.platform.uni_id_from_port_num(field.port)
+
+ if eap_flow:
+ self.log.debug('eap flow detected', onu_id=onu_id, uni_id=uni_id,
+ intf_id=intf_id, eap_intf_id=eap_intf_id,
+ eap_onu_id=eap_onu_id,
+ eap_uni_id=eap_uni_id)
+ if eap_flow and intf_id == eap_intf_id and onu_id == eap_onu_id and uni_id == eap_uni_id:
+ return True, flow
+
+ return False, None
+
+ def get_subscriber_vlan(self, port):
+ self.log.debug('looking from subscriber flow for port', port=port)
+
+ flows = self.logical_flows_proxy.get('/').items
+ for flow in flows:
+ in_port = fd.get_in_port(flow)
+ out_port = fd.get_out_port(flow)
+ if in_port == port and out_port is not None and \
+ self.platform.intf_id_to_port_type_name(out_port) \
+ == Port.ETHERNET_NNI:
+ fields = fd.get_ofb_fields(flow)
+ self.log.debug('subscriber flow found', fields=fields)
+ for field in fields:
+ if field.type == OFPXMT_OFB_VLAN_VID:
+ self.log.debug('subscriber vlan found',
+ vlan_id=field.vlan_vid)
+ return field.vlan_vid & 0x0fff
+ self.log.debug('No subscriber flow found', port=port)
+ return None
+
+ def add_flow_to_device(self, flow, logical_flow):
+ self.log.debug('pushing flow to device', flow=flow)
+ try:
+ self.stub.FlowAdd(flow)
+ except grpc.RpcError as grpc_e:
+ if grpc_e.code() == grpc.StatusCode.ALREADY_EXISTS:
+ self.log.warn('flow already exists', e=grpc_e, flow=flow)
+ else:
+ self.log.error('failed to add flow',
+ logical_flow=logical_flow, flow=flow,
+ grpc_error=grpc_e)
+ return False
+ else:
+ self.register_flow(logical_flow, flow)
+ return True
+
+ def update_flow_info_to_kv_store(self, intf_id, onu_id, uni_id, flow_id, flow):
+ self.resource_mgr.update_flow_id_info_for_uni(intf_id, onu_id, uni_id,
+ flow_id, flow)
+
+ def register_flow(self, logical_flow, device_flow):
+ self.log.debug('registering flow in device',
+ logical_flow=logical_flow, device_flow=device_flow)
+ stored_flow = copy.deepcopy(logical_flow)
+ stored_flow.id = self.generate_stored_id(device_flow.flow_id,
+ device_flow.flow_type)
+ self.log.debug('generated device flow id', id=stored_flow.id,
+ flow_id=device_flow.flow_id,
+ direction=device_flow.flow_type)
+ stored_flow.cookie = logical_flow.id
+ flows = self.flows_proxy.get('/')
+ flows.items.extend([stored_flow])
+ self.flows_proxy.update('/', flows)
+
+ def find_next_flow(self, flow):
+ table_id = fd.get_goto_table_id(flow)
+ metadata = 0
+ # Prior to ONOS 1.13.5, Metadata contained the UNI output port number. In
+ # 1.13.5 and later, the lower 32-bits is the output port number and the
+ # upper 32-bits is the inner-vid we are looking for. Use just the lower 32
+ # bits. Allows this code to work with pre- and post-1.13.5 ONOS OltPipeline
+
+ for field in fd.get_ofb_fields(flow):
+ if field.type == fd.METADATA:
+ metadata = field.table_metadata & 0xFFFFFFFF
+ if table_id is None:
+ return None
+ flows = self.logical_flows_proxy.get('/').items
+ next_flows = []
+ for f in flows:
+ if f.table_id == table_id:
+ # FIXME
+ if fd.get_in_port(f) == fd.get_in_port(flow) and \
+ fd.get_out_port(f) == metadata:
+ next_flows.append(f)
+
+ if len(next_flows) == 0:
+ self.log.warning('no next flow found, it may be a timing issue',
+ flow=flow, number_of_flows=len(flows))
+ if flow.id in self.retry_add_flow_list:
+ self.log.debug('flow is already in retry list', flow_id=flow.id)
+ else:
+ self.retry_add_flow_list.append(flow.id)
+ reactor.callLater(5, self.retry_add_flow, flow)
+ return None
+
+ next_flows.sort(key=lambda f: f.priority, reverse=True)
+
+ return next_flows[0]
+
+ def update_children_flows(self, device_rules_map):
+
+ for device_id, (flows, groups) in device_rules_map.iteritems():
+ if device_id != self.device_id:
+ self.root_proxy.update('/devices/{}/flows'.format(device_id),
+ Flows(items=flows.values()))
+ self.root_proxy.update('/devices/{}/flow_groups'.format(
+ device_id), FlowGroups(items=groups.values()))
+
+ def clear_flows_and_scheduler_for_logical_port(self, child_device, logical_port):
+ ofp_port_name = logical_port.ofp_port.name
+ port_no = logical_port.ofp_port.port_no
+ pon_port = child_device.proxy_address.channel_id
+ onu_id = child_device.proxy_address.onu_id
+ uni_id = self.platform.uni_id_from_port_num(logical_port)
+
+ # TODO: The DEFAULT_TECH_PROFILE_ID is assumed. Right way to do,
+ # is probably to maintain a list of Tech-profile table IDs associated
+ # with the UNI logical_port. This way, when the logical port is deleted,
+ # all the associated tech-profile configuration with the UNI logical_port
+ # can be cleared.
+ tech_profile_instance = self.tech_profile[pon_port]. \
+ get_tech_profile_instance(
+ DEFAULT_TECH_PROFILE_TABLE_ID,
+ ofp_port_name)
+ flow_ids = self.resource_mgr.get_current_flow_ids_for_uni(pon_port, onu_id, uni_id)
+ self.log.debug("outstanding-flows-to-be-cleared", flow_ids=flow_ids)
+ for flow_id in flow_ids:
+ flow_infos = self.resource_mgr.get_flow_id_info(pon_port, onu_id, uni_id, flow_id)
+ for flow_info in flow_infos:
+ direction = flow_info['flow_type']
+ flow_to_remove = openolt_pb2.Flow(flow_id=flow_id,
+ flow_type=direction)
+ try:
+ self.stub.FlowRemove(flow_to_remove)
+ except grpc.RpcError as grpc_e:
+ if grpc_e.code() == grpc.StatusCode.NOT_FOUND:
+ self.log.debug('This flow does not exist on the switch, '
+ 'normal after an OLT reboot',
+ flow=flow_to_remove)
+ else:
+ raise grpc_e
+
+ self.resource_mgr.free_flow_id_for_uni(pon_port, onu_id, uni_id, flow_id)
+
+ try:
+ tconts = self.tech_profile[pon_port].get_tconts(tech_profile_instance)
+ self.stub.RemoveTconts(openolt_pb2.Tconts(intf_id=pon_port,
+ onu_id=onu_id,
+ uni_id=uni_id,
+ port_no=port_no,
+ tconts=tconts))
+ except grpc.RpcError as grpc_e:
+ self.log.error('error-removing-tcont-scheduler-queues',
+ err=grpc_e)
+
+ def generate_stored_id(self, flow_id, direction):
+ if direction == UPSTREAM:
+ self.log.debug('upstream flow, shifting id')
+ return 0x1 << 15 | flow_id
+ elif direction == DOWNSTREAM:
+ self.log.debug('downstream flow, not shifting id')
+ return flow_id
+ else:
+ self.log.warn('Unrecognized direction', direction=direction)
+ return flow_id
+
+ def decode_stored_id(self, id):
+ if id >> 15 == 0x1:
+ return id & 0x7fff, UPSTREAM
+ else:
+ return id, DOWNSTREAM
+
+ def _populate_tech_profile_per_pon_port(self):
+ for arange in self.resource_mgr.device_info.ranges:
+ for intf_id in arange.intf_ids:
+ self.tech_profile[intf_id] = \
+ self.resource_mgr.resource_mgrs[intf_id].tech_profile
+
+ # Make sure we have as many tech_profiles as there are pon ports on
+ # the device
+ assert len(self.tech_profile) == self.resource_mgr.device_info.pon_ports
+
+ def _get_flow_info_as_json_blob(self, flow, flow_store_cookie,
+ flow_category=None):
+ json_blob = MessageToDict(message=flow,
+ preserving_proto_field_name=True)
+ self.log.debug("flow-info", json_blob=json_blob)
+ json_blob['flow_store_cookie'] = flow_store_cookie
+ if flow_category is not None:
+ json_blob['flow_category'] = flow_category
+ flow_info = self.resource_mgr.get_flow_id_info(flow.access_intf_id,
+ flow.onu_id, flow.uni_id, flow.flow_id)
+
+ if flow_info is None:
+ flow_info = list()
+ flow_info.append(json_blob)
+ else:
+ assert (isinstance(flow_info, list))
+ flow_info.append(json_blob)
+
+ return flow_info
+
+ @staticmethod
+ def _get_flow_store_cookie(classifier, gem_port=None):
+ assert isinstance(classifier, dict)
+ # We need unique flows per gem_port
+ if gem_port is not None:
+ to_hash = dumps(classifier, sort_keys=True) + str(gem_port)
+ else:
+ to_hash = dumps(classifier, sort_keys=True)
+ return hashlib.md5(to_hash).hexdigest()[:12]
+
+ def get_nni_intf_id(self):
+ if self.nni_intf_id is not None:
+ return self.nni_intf_id
+
+ port_list = self.adapter_agent.get_ports(self.device_id, Port.ETHERNET_NNI)
+ logical_port = self.adapter_agent.get_logical_port(self.logical_device_id,
+ port_list[0].label)
+ self.nni_intf_id = self.platform.intf_id_from_nni_port_num(logical_port.ofp_port.port_no)
+ self.log.debug("nni-intf-d ", nni_intf_id=self.nni_intf_id)
+ return self.nni_intf_id
diff --git a/python/adapters/openolt/openolt_platform.py b/python/adapters/openolt/openolt_platform.py
new file mode 100644
index 0000000..a44eafc
--- /dev/null
+++ b/python/adapters/openolt/openolt_platform.py
@@ -0,0 +1,164 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from voltha.protos.device_pb2 import Port
+import voltha.protos.device_pb2 as dev_pb2
+
+"""
+Encoding of identifiers
+=======================
+
+Flow id
+
+ Identifies a flow within a single OLT
+ Flow Id is unique per OLT
+ Multiple GEM ports can map to same flow id
+
+ 13 11 4 0
+ +--------+--------------+------+
+ | pon id | onu id | Flow |
+ | | | idx |
+ +--------+--------------+------+
+
+ 14 bits = 16384 flows (per OLT).
+
+ pon id = 4 bits = 16 PON ports
+ onu id = 7 bits = 128 ONUss per PON port
+ Flow index = 3 bits = 4 bi-directional flows per ONU
+ = 8 uni-directional flows per ONU
+
+
+Logical (OF) UNI port number
+
+ OpenFlow port number corresponding to PON UNI
+
+ 15 11 4 0
+ +--+--------+--------------+------+
+ |0 | pon id | onu id | 0 |
+ +--+--------+--------------+------+
+
+ pon id = 4 bits = 16 PON ports
+ onu id = 7 bits = 128 ONUs per PON port
+
+Logical (OF) NNI port number
+
+ OpenFlow port number corresponding to PON UNI
+
+ 16 0
+ +--+----------------------------+
+ |1 | intf_id |
+ +--+----------------------------+
+
+ No overlap with UNI port number space
+
+
+PON OLT (OF) port number
+
+ OpenFlow port number corresponding to PON OLT ports
+
+ 31 28 0
+ +--------+------------------------~~~------+
+ | 0x2 | pon intf id |
+ +--------+------------------------~~~------+
+
+"""
+
+class OpenOltPlatform(object):
+ MAX_PONS_PER_OLT = 16
+ MAX_ONUS_PER_PON = 32
+ MAX_UNIS_PER_ONU = 16
+
+ def __init__(self, log, resource_mgr):
+ self.log = log
+ self.resource_mgr = resource_mgr
+
+ def mk_uni_port_num(self, intf_id, onu_id, uni_id):
+ assert intf_id < OpenOltPlatform.MAX_PONS_PER_OLT
+ assert onu_id < OpenOltPlatform.MAX_ONUS_PER_PON
+ assert uni_id < OpenOltPlatform.MAX_UNIS_PER_ONU
+ self.resource_mgr.assert_uni_id_limit(intf_id, onu_id, uni_id)
+ return intf_id << 11 | onu_id << 4 | uni_id
+
+ #def mk_flow_id(self, intf_id, onu_id, idx):
+ # return intf_id << 9 | onu_id << 4 | idx
+
+ def uni_id_from_port_num(self, port_num):
+ return port_num & 0xF
+
+ def onu_id_from_port_num(self, port_num):
+ return (port_num >> 4) & 0x7F
+
+
+ def intf_id_from_uni_port_num(self, port_num):
+ return (port_num >> 11) & 0xF
+
+
+ def intf_id_from_pon_port_no(self, port_no):
+ return port_no & 0xF
+
+
+ def intf_id_to_port_no(self, intf_id, intf_type):
+ if intf_type is Port.ETHERNET_NNI:
+ return (0x1 << 16) | intf_id
+ elif intf_type is Port.PON_OLT:
+ return 0x2 << 28 | intf_id
+ else:
+ raise Exception('Invalid port type')
+
+
+ def intf_id_from_nni_port_num(self, port_num):
+ return port_num & 0xFFFF
+
+
+ def intf_id_to_port_type_name(self, intf_id):
+ if (2 << 28 ^ intf_id) < 16:
+ return Port.PON_OLT
+ elif intf_id & (0x1 << 16) == (0x1 << 16):
+ return Port.ETHERNET_NNI
+ else:
+ return Port.ETHERNET_UNI
+
+ def port_type_name_by_port_index(self, port_index):
+ try:
+ return dev_pb2._PORT_PORTTYPE.values_by_number[port_index].name
+ except Exception as err:
+ raise Exception(err)
+
+ def extract_access_from_flow(self, in_port, out_port):
+ if self.is_upstream(out_port):
+ return (in_port,
+ self.intf_id_from_uni_port_num(in_port),
+ self.onu_id_from_port_num(in_port),
+ self.uni_id_from_port_num(in_port))
+ else:
+ return (out_port,
+ self.intf_id_from_uni_port_num(out_port),
+ self.onu_id_from_port_num(out_port),
+ self.uni_id_from_port_num(out_port))
+
+ def is_upstream(self, out_port):
+
+ if out_port in [0xfffd, 0xfffffffd]:
+ # To Controller
+ return True
+ if (out_port & (0x1 << 16)) == (0x1 << 16):
+ # NNI interface
+ return True
+
+ return False
+ #
+ #def max_onus_per_pon(self):
+ # return OpenOltPlatform.MAX_ONUS_PER_PON
diff --git a/python/adapters/openolt/openolt_resource_manager.py b/python/adapters/openolt/openolt_resource_manager.py
new file mode 100644
index 0000000..760471d
--- /dev/null
+++ b/python/adapters/openolt/openolt_resource_manager.py
@@ -0,0 +1,435 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import structlog
+
+from common.pon_resource_manager.resource_manager import PONResourceManager
+from voltha.registry import registry
+from voltha.core.config.config_backend import ConsulStore
+from voltha.core.config.config_backend import EtcdStore
+from voltha.adapters.openolt.openolt_flow_mgr import *
+
+from voltha.adapters.openolt.protos import openolt_pb2
+from voltha.adapters.openolt.openolt_platform import OpenOltPlatform
+
+
+class OpenOltResourceMgr(object):
+ BASE_PATH_KV_STORE = "service/voltha/openolt/{}" # service/voltha/openolt/<device_id>
+
+ def __init__(self, device_id, host_and_port, extra_args, device_info):
+ self.log = structlog.get_logger(id=device_id,
+ ip=host_and_port)
+ self.device_id = device_id
+ self.host_and_port = host_and_port
+ self.extra_args = extra_args
+ self.device_info = device_info
+ self.args = registry('main').get_args()
+
+ # KV store's IP Address and PORT
+ if self.args.backend == 'etcd':
+ host, port = self.args.etcd.split(':', 1)
+ self.kv_store = EtcdStore(host, port,
+ OpenOltResourceMgr.BASE_PATH_KV_STORE.format(device_id))
+ elif self.args.backend == 'consul':
+ host, port = self.args.consul.split(':', 1)
+ self.kv_store = ConsulStore(host, port,
+ OpenOltResourceMgr.BASE_PATH_KV_STORE.format(device_id))
+ else:
+ self.log.error('Invalid-backend')
+ raise Exception("Invalid-backend-for-kv-store")
+
+ ranges = dict()
+ resource_mgrs_by_tech = dict()
+ self.resource_mgrs = dict()
+
+ # If a legacy driver returns protobuf without any ranges,s synthesize one from
+ # the legacy global per-device informaiton. This, in theory, is temporary until
+ # the legacy drivers are upgrade to support pool ranges.
+ if len(self.device_info.ranges) == 0:
+ arange = self.device_info.ranges.add()
+ arange.technology = self.device_info.technology
+ arange.intf_ids.extend(range(0, device_info.pon_ports))
+
+ pool = arange.pools.add()
+ pool.type = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.ONU_ID
+ pool.start = self.device_info.onu_id_start
+ pool.end = self.device_info.onu_id_end
+ pool.sharing = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.DEDICATED_PER_INTF
+
+ pool = arange.pools.add()
+ pool.type = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.ALLOC_ID
+ pool.start = self.device_info.alloc_id_start
+ pool.end = self.device_info.alloc_id_end
+ pool.sharing = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_ALL_TECH
+
+ pool = arange.pools.add()
+ pool.type = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.GEMPORT_ID
+ pool.start = self.device_info.gemport_id_start
+ pool.end = self.device_info.gemport_id_end
+ pool.sharing = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_ALL_TECH
+
+ pool = arange.pools.add()
+ pool.type = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.FLOW_ID
+ pool.start = self.device_info.flow_id_start
+ pool.end = self.device_info.flow_id_end
+ pool.sharing = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_ALL_TECH
+
+ # Create a separate Resource Manager instance for each range. This assumes that
+ # each technology is represented by only a single range
+ global_resource_mgr = None
+ for arange in self.device_info.ranges:
+ technology = arange.technology
+ self.log.info("device-info", technology=technology)
+ ranges[technology] = arange
+ extra_args = self.extra_args + ' ' + PONResourceManager.OLT_MODEL_ARG + ' {}'.format(self.device_info.model)
+ resource_mgr = PONResourceManager(technology,
+ extra_args, self.device_id, self.args.backend, host, port)
+ resource_mgrs_by_tech[technology] = resource_mgr
+ if global_resource_mgr is None:
+ global_resource_mgr = resource_mgr
+ for intf_id in arange.intf_ids:
+ self.resource_mgrs[intf_id] = resource_mgrs_by_tech[technology]
+ self.initialize_device_resource_range_and_pool(resource_mgr, global_resource_mgr, arange)
+
+ # After we have initialized resource ranges, initialize the
+ # resource pools accordingly.
+ for technology, resource_mgr in resource_mgrs_by_tech.iteritems():
+ resource_mgr.init_device_resource_pool()
+
+ def __del__(self):
+ self.log.info("clearing-device-resource-pool")
+ for key, resource_mgr in self.resource_mgrs.iteritems():
+ resource_mgr.clear_device_resource_pool()
+
+ def assert_pon_id_limit(self, pon_intf_id):
+ assert pon_intf_id in self.resource_mgrs
+
+ def assert_onu_id_limit(self, pon_intf_id, onu_id):
+ self.assert_pon_id_limit(pon_intf_id)
+ self.resource_mgrs[pon_intf_id].assert_resource_limits(onu_id, PONResourceManager.ONU_ID)
+
+ @property
+ def max_uni_id_per_onu(self):
+ return 0 #OpenOltPlatform.MAX_UNIS_PER_ONU-1, zero-based indexing Uncomment or override to make default multi-uni
+
+ def assert_uni_id_limit(self, pon_intf_id, onu_id, uni_id):
+ self.assert_onu_id_limit(pon_intf_id, onu_id)
+ self.resource_mgrs[pon_intf_id].assert_resource_limits(uni_id, PONResourceManager.UNI_ID)
+
+ def get_onu_id(self, pon_intf_id):
+ onu_id = self.resource_mgrs[pon_intf_id].get_resource_id(
+ pon_intf_id, PONResourceManager.ONU_ID, 1)
+
+ if onu_id is not None:
+ pon_intf_onu_id = (pon_intf_id, onu_id)
+ self.resource_mgrs[pon_intf_id].init_resource_map(
+ pon_intf_onu_id)
+
+ return onu_id
+
+ def get_flow_id(self, pon_intf_id, onu_id, uni_id, flow_store_cookie,
+ flow_category=None):
+ pon_intf_onu_id = (pon_intf_id, onu_id, uni_id)
+ try:
+ flow_ids = self.resource_mgrs[pon_intf_id]. \
+ get_current_flow_ids_for_onu(pon_intf_onu_id)
+ if flow_ids is not None:
+ for flow_id in flow_ids:
+ flows = self.get_flow_id_info(pon_intf_id, onu_id, uni_id, flow_id)
+ assert (isinstance(flows, list))
+ for flow in flows:
+
+ if flow_category is not None and \
+ 'flow_category' in flow and \
+ flow['flow_category'] == flow_category:
+ return flow_id
+ if flow['flow_store_cookie'] == flow_store_cookie:
+ return flow_id
+ except Exception as e:
+ self.log.error("error-retrieving-flow-info", e=e)
+
+ flow_id = self.resource_mgrs[pon_intf_id].get_resource_id(
+ pon_intf_onu_id[0], PONResourceManager.FLOW_ID)
+ if flow_id is not None:
+ self.resource_mgrs[pon_intf_id].update_flow_id_for_onu(
+ pon_intf_onu_id, flow_id
+ )
+
+ return flow_id
+
+ def get_flow_id_info(self, pon_intf_id, onu_id, uni_id, flow_id):
+ pon_intf_onu_id = (pon_intf_id, onu_id, uni_id)
+ return self.resource_mgrs[pon_intf_id].get_flow_id_info(pon_intf_onu_id, flow_id)
+
+ def get_current_flow_ids_for_uni(self, pon_intf_id, onu_id, uni_id):
+ pon_intf_onu_id = (pon_intf_id, onu_id, uni_id)
+ return self.resource_mgrs[pon_intf_id].get_current_flow_ids_for_onu(pon_intf_onu_id)
+
+ 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_mgrs[pon_intf_id].update_flow_id_info_for_onu(
+ pon_intf_onu_id, flow_id, flow_data)
+
+ def get_alloc_id(self, pon_intf_onu_id):
+ # Derive the pon_intf from the pon_intf_onu_id tuple
+ pon_intf = pon_intf_onu_id[0]
+ alloc_id_list = self.resource_mgrs[pon_intf].get_current_alloc_ids_for_onu(
+ pon_intf_onu_id)
+
+ if alloc_id_list and len(alloc_id_list) > 0:
+ # Since we support only one alloc_id for the ONU at the moment,
+ # return the first alloc_id in the list, if available, for that
+ # ONU.
+ return alloc_id_list[0]
+
+ alloc_id = self.resource_mgrs[pon_intf].get_resource_id(
+ pon_intf_id=pon_intf,
+ resource_type=PONResourceManager.ALLOC_ID,
+ num_of_id=1
+ )
+ if alloc_id is None:
+ self.log.error("no-alloc-id-available")
+ return None
+
+ # update the resource map on KV store with the list of alloc_id
+ # allocated for the pon_intf_onu_id tuple
+ self.resource_mgrs[pon_intf].update_alloc_ids_for_onu(pon_intf_onu_id,
+ list(alloc_id))
+
+ return alloc_id
+
+ def get_current_gemport_ids_for_onu(self, pon_intf_onu_id):
+ pon_intf_id = pon_intf_onu_id[0]
+ return self.resource_mgrs[pon_intf_id].get_current_gemport_ids_for_onu(pon_intf_onu_id)
+
+ def get_current_alloc_ids_for_onu(self, pon_intf_onu_id):
+ pon_intf_id = pon_intf_onu_id[0]
+ alloc_ids = self.resource_mgrs[pon_intf_id].get_current_alloc_ids_for_onu(pon_intf_onu_id)
+ if alloc_ids is None:
+ return None
+ # We support only one tcont at the moment
+ return alloc_ids[0]
+
+ def update_gemports_ponport_to_onu_map_on_kv_store(self, gemport_list, pon_port, onu_id, uni_id):
+ for gemport in gemport_list:
+ pon_intf_gemport = (pon_port, gemport)
+ # This information is used when packet_indication is received and
+ # we need to derive the ONU Id for which the packet arrived based
+ # on the pon_intf and gemport available in the packet_indication
+ self.kv_store[str(pon_intf_gemport)] = ' '.join(map(str, (onu_id, uni_id)))
+
+ def get_onu_uni_from_ponport_gemport(self, pon_port, gemport):
+ pon_intf_gemport = (pon_port, gemport)
+ return tuple(map(int, self.kv_store[str(pon_intf_gemport)].split(' ')))
+
+ def get_gemport_id(self, pon_intf_onu_id, num_of_id=1):
+ # Derive the pon_intf and onu_id from the pon_intf_onu_id tuple
+ pon_intf = pon_intf_onu_id[0]
+ onu_id = pon_intf_onu_id[1]
+ uni_id = pon_intf_onu_id[2]
+ assert False, 'unused function'
+
+ gemport_id_list = self.resource_mgrs[pon_intf].get_current_gemport_ids_for_onu(
+ pon_intf_onu_id)
+ if gemport_id_list and len(gemport_id_list) > 0:
+ return gemport_id_list
+
+ gemport_id_list = self.resource_mgrs[pon_intf].get_resource_id(
+ pon_intf_id=pon_intf,
+ resource_type=PONResourceManager.GEMPORT_ID,
+ num_of_id=num_of_id
+ )
+
+ if gemport_id_list and len(gemport_id_list) == 0:
+ self.log.error("no-gemport-id-available")
+ return None
+
+ # update the resource map on KV store with the list of gemport_id
+ # allocated for the pon_intf_onu_id tuple
+ self.resource_mgrs[pon_intf].update_gemport_ids_for_onu(pon_intf_onu_id,
+ gemport_id_list)
+
+ self.update_gemports_ponport_to_onu_map_on_kv_store(gemport_id_list,
+ pon_intf, onu_id, uni_id)
+ return gemport_id_list
+
+ def free_onu_id(self, pon_intf_id, onu_id):
+ _ = self.resource_mgrs[pon_intf_id].free_resource_id(
+ pon_intf_id, PONResourceManager.ONU_ID, onu_id)
+
+ pon_intf_onu_id = (pon_intf_id, onu_id)
+ self.resource_mgrs[pon_intf_id].remove_resource_map(
+ pon_intf_onu_id)
+
+ def free_flow_id_for_uni(self, pon_intf_id, onu_id, uni_id, flow_id):
+ self.resource_mgrs[pon_intf_id].free_resource_id(
+ pon_intf_id, PONResourceManager.FLOW_ID, flow_id)
+ pon_intf_onu_id = (pon_intf_id, onu_id, uni_id)
+ self.resource_mgrs[pon_intf_id].update_flow_id_for_onu(pon_intf_onu_id,
+ flow_id, False)
+ self.resource_mgrs[pon_intf_id].remove_flow_id_info(pon_intf_onu_id,
+ flow_id)
+
+ def free_pon_resources_for_onu(self, pon_intf_id_onu_id):
+
+ pon_intf_id = pon_intf_id_onu_id[0]
+ onu_id = pon_intf_id_onu_id[1]
+ alloc_ids = \
+ self.resource_mgrs[pon_intf_id].get_current_alloc_ids_for_onu(pon_intf_id_onu_id)
+ self.resource_mgrs[pon_intf_id].free_resource_id(pon_intf_id,
+ PONResourceManager.ALLOC_ID,
+ alloc_ids)
+
+ gemport_ids = \
+ self.resource_mgrs[pon_intf_id].get_current_gemport_ids_for_onu(pon_intf_id_onu_id)
+ self.resource_mgrs[pon_intf_id].free_resource_id(pon_intf_id,
+ PONResourceManager.GEMPORT_ID,
+ gemport_ids)
+
+ flow_ids = \
+ self.resource_mgrs[pon_intf_id].get_current_flow_ids_for_onu(pon_intf_id_onu_id)
+ self.resource_mgrs[pon_intf_id].free_resource_id(pon_intf_id,
+ PONResourceManager.FLOW_ID,
+ flow_ids)
+
+ self.resource_mgrs[pon_intf_id].free_resource_id(pon_intf_id,
+ PONResourceManager.ONU_ID,
+ onu_id)
+
+ # Clear resource map associated with (pon_intf_id, gemport_id) tuple.
+ self.resource_mgrs[pon_intf_id].remove_resource_map(pon_intf_id_onu_id)
+
+ # Clear the ONU Id associated with the (pon_intf_id, gemport_id) tuple.
+ for gemport_id in gemport_ids:
+ del self.kv_store[str((pon_intf_id, gemport_id))]
+
+ def initialize_device_resource_range_and_pool(self, resource_mgr, global_resource_mgr, arange):
+ self.log.info("resource-range-pool-init", technology=resource_mgr.technology)
+
+ # first load from KV profiles
+ status = resource_mgr.init_resource_ranges_from_kv_store()
+ if not status:
+ self.log.info("failed-to-load-resource-range-from-kv-store", technology=resource_mgr.technology)
+
+ # Then apply device specific information. If KV doesn't exist
+ # or is broader than the device, the device's informationw ill
+ # dictate the range limits
+ self.log.info("using-device-info-to-init-pon-resource-ranges", technology=resource_mgr.technology)
+
+ onu_id_start = self.device_info.onu_id_start
+ onu_id_end = self.device_info.onu_id_end
+ onu_id_shared = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.DEDICATED_PER_INTF
+ onu_id_shared_pool_id = None
+ alloc_id_start = self.device_info.alloc_id_start
+ alloc_id_end = self.device_info.alloc_id_end
+ alloc_id_shared = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_ALL_TECH # TODO EdgeCore/BAL limitation
+ alloc_id_shared_pool_id = None
+ gemport_id_start = self.device_info.gemport_id_start
+ gemport_id_end = self.device_info.gemport_id_end
+ gemport_id_shared = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_ALL_TECH # TODO EdgeCore/BAL limitation
+ gemport_id_shared_pool_id = None
+ flow_id_start = self.device_info.flow_id_start
+ flow_id_end = self.device_info.flow_id_end
+ flow_id_shared = openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_ALL_TECH # TODO EdgeCore/BAL limitation
+ flow_id_shared_pool_id = None
+
+ global_pool_id = 0
+ for first_intf_pool_id in arange.intf_ids:
+ break
+
+ for pool in arange.pools:
+ shared_pool_id = global_pool_id if pool.sharing == openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_ALL_TECH else \
+ first_intf_pool_id if pool.sharing == openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_SAME_TECH else \
+ None
+
+ if pool.type == openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.ONU_ID:
+ onu_id_start = pool.start
+ onu_id_end = pool.end
+ onu_id_shared = pool.sharing
+ onu_id_shared_pool_id = shared_pool_id
+ elif pool.type == openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.ALLOC_ID:
+ alloc_id_start = pool.start
+ alloc_id_end = pool.end
+ alloc_id_shared = pool.sharing
+ alloc_id_shared_pool_id = shared_pool_id
+ elif pool.type == openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.GEMPORT_ID:
+ gemport_id_start = pool.start
+ gemport_id_end = pool.end
+ gemport_id_shared = pool.sharing
+ gemport_id_shared_pool_id = shared_pool_id
+ elif pool.type == openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.FLOW_ID:
+ flow_id_start = pool.start
+ flow_id_end = pool.end
+ flow_id_shared = pool.sharing
+ flow_id_shared_pool_id = shared_pool_id
+
+ self.log.info("device-info-init", technology=arange.technology,
+ onu_id_start=onu_id_start, onu_id_end=onu_id_end, onu_id_shared_pool_id=onu_id_shared_pool_id,
+ alloc_id_start=alloc_id_start, alloc_id_end=alloc_id_end,
+ alloc_id_shared_pool_id=alloc_id_shared_pool_id,
+ gemport_id_start=gemport_id_start, gemport_id_end=gemport_id_end,
+ gemport_id_shared_pool_id=gemport_id_shared_pool_id,
+ flow_id_start_idx=flow_id_start,
+ flow_id_end_idx=flow_id_end,
+ flow_id_shared_pool_id=flow_id_shared_pool_id,
+ intf_ids=arange.intf_ids,
+ uni_id_start_idx=0,
+ uni_id_end_idx=self.max_uni_id_per_onu)
+
+ resource_mgr.init_default_pon_resource_ranges(
+ onu_id_start_idx=onu_id_start,
+ onu_id_end_idx=onu_id_end,
+ onu_id_shared_pool_id=onu_id_shared_pool_id,
+ alloc_id_start_idx=alloc_id_start,
+ alloc_id_end_idx=alloc_id_end,
+ alloc_id_shared_pool_id=alloc_id_shared_pool_id,
+ gemport_id_start_idx=gemport_id_start,
+ gemport_id_end_idx=gemport_id_end,
+ gemport_id_shared_pool_id=gemport_id_shared_pool_id,
+ flow_id_start_idx=flow_id_start,
+ flow_id_end_idx=flow_id_end,
+ flow_id_shared_pool_id=flow_id_shared_pool_id,
+ uni_id_start_idx=0, uni_id_end_idx=self.max_uni_id_per_onu,
+ num_of_pon_ports=self.device_info.pon_ports,
+ intf_ids=arange.intf_ids
+ )
+
+ # For global sharing, make sure to refresh both local and global resource manager instances' range
+ if global_resource_mgr is not self:
+ if onu_id_shared == openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_ALL_TECH:
+ global_resource_mgr.update_ranges(onu_id_start_idx=onu_id_start, onu_id_end_idx=onu_id_end)
+ resource_mgr.update_ranges(onu_id_start_idx=onu_id_start, onu_id_end_idx=onu_id_end,
+ onu_id_shared_resource_mgr=global_resource_mgr)
+
+ if alloc_id_shared == openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_ALL_TECH:
+ global_resource_mgr.update_ranges(alloc_id_start_idx=alloc_id_start, alloc_id_end_idx=alloc_id_end)
+ resource_mgr.update_ranges(alloc_id_start_idx=alloc_id_start, alloc_id_end_idx=alloc_id_end,
+ alloc_id_shared_resource_mgr=global_resource_mgr)
+
+ if gemport_id_shared == openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_ALL_TECH:
+ global_resource_mgr.update_ranges(gemport_id_start_idx=gemport_id_start,
+ gemport_id_end_idx=gemport_id_end)
+ resource_mgr.update_ranges(gemport_id_start_idx=gemport_id_start, gemport_id_end_idx=gemport_id_end,
+ gemport_id_shared_resource_mgr=global_resource_mgr)
+
+ if flow_id_shared == openolt_pb2.DeviceInfo.DeviceResourceRanges.Pool.SHARED_BY_ALL_INTF_ALL_TECH:
+ global_resource_mgr.update_ranges(flow_id_start_idx=flow_id_start,
+ flow_id_end_idx=flow_id_end)
+ resource_mgr.update_ranges(flow_id_start_idx=flow_id_start, flow_id_end_idx=flow_id_end,
+ flow_id_shared_resource_mgr=global_resource_mgr)
+
+ # Make sure loaded range fits the platform bit encoding ranges
+ resource_mgr.update_ranges(uni_id_start_idx=0, uni_id_end_idx=OpenOltPlatform.MAX_UNIS_PER_ONU-1)
diff --git a/python/adapters/openolt/openolt_statistics.py b/python/adapters/openolt/openolt_statistics.py
new file mode 100644
index 0000000..bfdb61c
--- /dev/null
+++ b/python/adapters/openolt/openolt_statistics.py
@@ -0,0 +1,606 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# from voltha.protos.events_pb2 import KpiEvent, MetricValuePairs
+# from voltha.protos.events_pb2 import KpiEventType
+
+# from voltha.adapters.openolt.nni_port import NniPort
+# from voltha.adapters.openolt.pon_port import PonPort
+# from voltha.protos.device_pb2 import Port
+
+from twisted.internet import reactor, defer
+from voltha.extensions.kpi.olt.olt_pm_metrics import OltPmMetrics
+from voltha.protos.device_pb2 import PmConfig, PmConfigs, PmGroupConfig, Port
+
+
+class OpenOltStatisticsMgr(object):
+ def __init__(self, openolt_device, log, platform, **kargs):
+
+ """
+ kargs are used to pass debugging flags at this time.
+ :param openolt_device:
+ :param log:
+ :param kargs:
+ """
+ self.device = openolt_device
+ self.log = log
+ self.platform = platform
+ # Northbound and Southbound ports
+ # added to initialize the pm_metrics
+ self.northbound_ports = self.init_ports(type="nni")
+ self.southbound_ports = self.init_ports(type='pon')
+
+ self.pm_metrics = None
+ # The following can be used to allow a standalone test routine to start
+ # the metrics independently
+ self.metrics_init = kargs.pop("metrics_init", True)
+ if self.metrics_init == True:
+ self.init_pm_metrics()
+
+ def init_pm_metrics(self):
+ # Setup PM configuration for this device
+ if self.pm_metrics is None:
+ try:
+ self.device.reason = 'setting up Performance Monitoring configuration'
+ kwargs = {
+ 'nni-ports': self.northbound_ports.values(),
+ 'pon-ports': self.southbound_ports.values()
+ }
+ self.pm_metrics = OltPmMetrics(self.device.adapter_agent, self.device.device_id,
+ self.device.logical_device_id,
+ grouped=True, freq_override=False,
+ **kwargs)
+ """
+ override the default naming structures in the OltPmMetrics class.
+ This is being done until the protos can be modified in the BAL driver
+
+ """
+ self.pm_metrics.nni_pm_names = (self.get_openolt_port_pm_names())['nni_pm_names']
+ self.pm_metrics.nni_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in self.pm_metrics.nni_pm_names}
+
+ self.pm_metrics.pon_pm_names = (self.get_openolt_port_pm_names())['pon_pm_names']
+ self.pm_metrics.pon_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
+ for (m, t) in self.pm_metrics.pon_pm_names}
+ pm_config = self.pm_metrics.make_proto()
+ self.log.info("initial-pm-config", pm_config=pm_config)
+ self.device.adapter_agent.update_device_pm_config(pm_config, init=True)
+ # Start collecting stats from the device after a brief pause
+ reactor.callLater(10, self.pm_metrics.start_collector)
+ except Exception as e:
+ self.log.exception('pm-setup', e=e)
+
+ def port_statistics_indication(self, port_stats):
+ # self.log.info('port-stats-collected', stats=port_stats)
+ self.ports_statistics_kpis(port_stats)
+ #FIXME: etcd problem, do not update objects for now
+
+ #
+ #
+ # #FIXME : only the first uplink is a logical port
+ # if platform.intf_id_to_port_type_name(port_stats.intf_id) ==
+ # Port.ETHERNET_NNI:
+ # # ONOS update
+ # self.update_logical_port_stats(port_stats)
+ # # update port object stats
+ # port = self.device.adapter_agent.get_port(self.device.device_id,
+ # port_no=port_stats.intf_id)
+ #
+ # if port is None:
+ # self.log.warn('port associated with this stats does not exist')
+ # return
+ #
+ # port.rx_packets = port_stats.rx_packets
+ # port.rx_bytes = port_stats.rx_bytes
+ # port.rx_errors = port_stats.rx_error_packets
+ # port.tx_packets = port_stats.tx_packets
+ # port.tx_bytes = port_stats.tx_bytes
+ # port.tx_errors = port_stats.tx_error_packets
+ #
+ # # Add port does an update if port exists
+ # self.device.adapter_agent.add_port(self.device.device_id, port)
+
+ def flow_statistics_indication(self, flow_stats):
+ self.log.info('flow-stats-collected', stats=flow_stats)
+ # TODO: send to kafka ?
+ # FIXME: etcd problem, do not update objects for now
+ # # UNTESTED : the openolt driver does not yet provide flow stats
+ # self.device.adapter_agent.update_flow_stats(
+ # self.device.logical_device_id,
+ # flow_id=flow_stats.flow_id, packet_count=flow_stats.tx_packets,
+ # byte_count=flow_stats.tx_bytes)
+
+ def ports_statistics_kpis(self, port_stats):
+ """
+ map the port stats values into a dictionary
+ Create a kpoEvent and publish to Kafka
+
+ :param port_stats:
+ :return:
+ """
+
+ try:
+ intf_id = port_stats.intf_id
+
+ if self.platform.intf_id_to_port_no(0, Port.ETHERNET_NNI) < intf_id < \
+ self.platform.intf_id_to_port_no(4, Port.ETHERNET_NNI) :
+ """
+ for this release we are only interested in the first NNI for
+ Northbound.
+ we are not using the other 3
+ """
+ return
+ else:
+
+ pm_data = {}
+ pm_data["rx_bytes"] = port_stats.rx_bytes
+ pm_data["rx_packets"] = port_stats.rx_packets
+ pm_data["rx_ucast_packets"] = port_stats.rx_ucast_packets
+ pm_data["rx_mcast_packets"] = port_stats.rx_mcast_packets
+ pm_data["rx_bcast_packets"] = port_stats.rx_bcast_packets
+ pm_data["rx_error_packets"] = port_stats.rx_error_packets
+ pm_data["tx_bytes"] = port_stats.tx_bytes
+ pm_data["tx_packets"] = port_stats.tx_packets
+ pm_data["tx_ucast_packets"] = port_stats.tx_ucast_packets
+ pm_data["tx_mcast_packets"] = port_stats.tx_mcast_packets
+ pm_data["tx_bcast_packets"] = port_stats.tx_bcast_packets
+ pm_data["tx_error_packets"] = port_stats.tx_error_packets
+ pm_data["rx_crc_errors"] = port_stats.rx_crc_errors
+ pm_data["bip_errors"] = port_stats.bip_errors
+
+ pm_data["intf_id"] = intf_id
+
+ """
+ Based upon the intf_id map to an nni port or a pon port
+ the intf_id is the key to the north or south bound collections
+
+ Based upon the intf_id the port object (nni_port or pon_port) will
+ have its data attr. updated by the current dataset collected.
+
+ For prefixing the rule is currently to use the port number and not the intf_id
+
+ """
+ #FIXME : Just use first NNI for now
+ if intf_id == self.platform.intf_id_to_port_no(0,
+ Port.ETHERNET_NNI):
+ #NNI port (just the first one)
+ self.update_port_object_kpi_data(
+ port_object=self.northbound_ports[port_stats.intf_id], datadict=pm_data)
+ else:
+ #PON ports
+ self.update_port_object_kpi_data(
+ port_object=self.southbound_ports[port_stats.intf_id],datadict=pm_data)
+ except Exception as err:
+ self.log.exception("Error publishing kpi statistics. ", errmessage=err)
+
+ def update_logical_port_stats(self, port_stats):
+ try:
+ label = 'nni-{}'.format(port_stats.intf_id)
+ logical_port = self.device.adapter_agent.get_logical_port(
+ self.device.logical_device_id, label)
+ except KeyError as e:
+ self.log.warn('logical port was not found, it may not have been '
+ 'created yet', exception=e)
+ return
+
+ if logical_port is None:
+ self.log.error('logical-port-is-None',
+ logical_device_id=self.device.logical_device_id, label=label,
+ port_stats=port_stats)
+ return
+
+ logical_port.ofp_port_stats.rx_packets = port_stats.rx_packets
+ logical_port.ofp_port_stats.rx_bytes = port_stats.rx_bytes
+ logical_port.ofp_port_stats.tx_packets = port_stats.tx_packets
+ logical_port.ofp_port_stats.tx_bytes = port_stats.tx_bytes
+ logical_port.ofp_port_stats.rx_errors = port_stats.rx_error_packets
+ logical_port.ofp_port_stats.tx_errors = port_stats.tx_error_packets
+ logical_port.ofp_port_stats.rx_crc_err = port_stats.rx_crc_errors
+
+ self.log.debug('after-stats-update', port=logical_port)
+
+ self.device.adapter_agent.update_logical_port(
+ self.device.logical_device_id, logical_port)
+
+ """
+ The following 4 methods customer naming, the generation of the port objects, building of those
+ objects and populating new data. The pm metrics operate on the value that are contained in the Port objects.
+ This class updates those port objects with the current data from the grpc indication and
+ post the data on a fixed interval.
+
+ """
+ def get_openolt_port_pm_names(self):
+ """
+ This collects a dictionary of the custom port names
+ used by the openolt.
+
+ Some of these are the same as the pm names used by the olt_pm_metrics class
+ if the set is the same then there is no need to call this method. However, when
+ custom names are used in the protos then the specific names should be pushed into
+ the olt_pm_metrics class.
+
+ :return:
+ """
+ nni_pm_names = {
+ ('intf_id', PmConfig.CONTEXT), # Physical device interface ID/Port number
+
+ ('admin_state', PmConfig.STATE),
+ ('oper_status', PmConfig.STATE),
+ ('port_no', PmConfig.GAUGE),
+ ('rx_bytes', PmConfig.COUNTER),
+ ('rx_packets', PmConfig.COUNTER),
+ ('rx_ucast_packets', PmConfig.COUNTER),
+ ('rx_mcast_packets', PmConfig.COUNTER),
+ ('rx_bcast_packets', PmConfig.COUNTER),
+ ('rx_error_packets', PmConfig.COUNTER),
+ ('tx_bytes', PmConfig.COUNTER),
+ ('tx_packets', PmConfig.COUNTER),
+ ('tx_ucast_packets', PmConfig.COUNTER),
+ ('tx_mcast_packets', PmConfig.COUNTER),
+ ('tx_bcast_packets', PmConfig.COUNTER),
+ ('tx_error_packets', PmConfig.COUNTER)
+ }
+ nni_pm_names_from_kpi_extension = {
+ ('intf_id', PmConfig.CONTEXT), # Physical device interface ID/Port number
+
+ ('admin_state', PmConfig.STATE),
+ ('oper_status', PmConfig.STATE),
+
+ ('rx_bytes', PmConfig.COUNTER),
+ ('rx_packets', PmConfig.COUNTER),
+ ('rx_ucast_packets', PmConfig.COUNTER),
+ ('rx_mcast_packets', PmConfig.COUNTER),
+ ('rx_bcast_packets', PmConfig.COUNTER),
+ ('rx_error_packets', PmConfig.COUNTER),
+
+ ('tx_bytes', PmConfig.COUNTER),
+ ('tx_packets', PmConfig.COUNTER),
+ ('tx_ucast_packets', PmConfig.COUNTER),
+ ('tx_mcast_packets', PmConfig.COUNTER),
+ ('tx_bcast_packets', PmConfig.COUNTER),
+ ('tx_error_packets', PmConfig.COUNTER),
+ ('rx_crc_errors', PmConfig.COUNTER),
+ ('bip_errors', PmConfig.COUNTER),
+ }
+
+ # pon_names uses same structure as nmi_names with the addition of pon_id to context
+ pon_pm_names = {
+ ('pon_id', PmConfig.CONTEXT), # PON ID (0..n)
+ ('port_no', PmConfig.CONTEXT),
+
+ ('admin_state', PmConfig.STATE),
+ ('oper_status', PmConfig.STATE),
+ ('rx_bytes', PmConfig.COUNTER),
+ ('rx_packets', PmConfig.COUNTER),
+ ('rx_ucast_packets', PmConfig.COUNTER),
+ ('rx_mcast_packets', PmConfig.COUNTER),
+ ('rx_bcast_packets', PmConfig.COUNTER),
+ ('rx_error_packets', PmConfig.COUNTER),
+ ('tx_bytes', PmConfig.COUNTER),
+ ('tx_packets', PmConfig.COUNTER),
+ ('tx_ucast_packets', PmConfig.COUNTER),
+ ('tx_mcast_packets', PmConfig.COUNTER),
+ ('tx_bcast_packets', PmConfig.COUNTER),
+ ('tx_error_packets', PmConfig.COUNTER)
+ }
+ pon_pm_names_from_kpi_extension = {
+ ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
+ ('pon_id', PmConfig.CONTEXT), # PON ID (0..n)
+
+ ('admin_state', PmConfig.STATE),
+ ('oper_status', PmConfig.STATE),
+ ('rx_packets', PmConfig.COUNTER),
+ ('rx_bytes', PmConfig.COUNTER),
+ ('tx_packets', PmConfig.COUNTER),
+ ('tx_bytes', PmConfig.COUNTER),
+ ('tx_bip_errors', PmConfig.COUNTER),
+ ('in_service_onus', PmConfig.GAUGE),
+ ('closest_onu_distance', PmConfig.GAUGE)
+ }
+ onu_pm_names = {
+ ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
+ ('pon_id', PmConfig.CONTEXT),
+ ('onu_id', PmConfig.CONTEXT),
+
+ ('fiber_length', PmConfig.GAUGE),
+ ('equalization_delay', PmConfig.GAUGE),
+ ('rssi', PmConfig.GAUGE),
+ }
+ gem_pm_names = {
+ ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
+ ('pon_id', PmConfig.CONTEXT),
+ ('onu_id', PmConfig.CONTEXT),
+ ('gem_id', PmConfig.CONTEXT),
+
+ ('alloc_id', PmConfig.GAUGE),
+ ('rx_packets', PmConfig.COUNTER),
+ ('rx_bytes', PmConfig.COUNTER),
+ ('tx_packets', PmConfig.COUNTER),
+ ('tx_bytes', PmConfig.COUNTER),
+ }
+ # Build a dict for the names. The caller will index to the correct values
+ names_dict = {"nni_pm_names": nni_pm_names,
+ "pon_pm_names": pon_pm_names,
+ "pon_pm_names_orig": pon_pm_names_from_kpi_extension,
+ "onu_pm_names": onu_pm_names,
+ "gem_pm_names": gem_pm_names,
+
+ }
+
+ return names_dict
+
+ def init_ports(self, device_id=12345, type="nni", log=None):
+ """
+ This method collects the port objects: nni and pon that are updated with the
+ current data from the OLT
+
+ Both the northbound (nni) and southbound ports are indexed by the interface id (intf_id)
+ and NOT the port number. When the port object is instantiated it will contain the intf_id and
+ port_no values
+
+ :param type:
+ :param device_id:
+ :param log:
+ :return:
+ """
+ try:
+ if type == "nni":
+ nni_ports = {}
+ for i in range(0, 1):
+ nni_port = self.build_port_object(i, type='nni')
+ nni_ports[nni_port.intf_id] = nni_port
+ return nni_ports
+ elif type == "pon":
+ pon_ports = {}
+ for i in range(0, 16):
+ pon_port = self.build_port_object(i, type="pon")
+ pon_ports[pon_port.intf_id] = pon_port
+ return pon_ports
+ else:
+ self.log.exception("Unmapped port type requested = " , type=type)
+ raise Exception("Unmapped port type requested = " + type)
+
+ except Exception as err:
+ raise Exception(err)
+
+ def build_port_object(self, port_num, type="nni"):
+ """
+ Seperate method to allow for updating north and southbound ports
+ newly discovered ports and devices
+
+ :param port_num:
+ :param type:
+ :return:
+ """
+ try:
+ """
+ This builds a port object which is added to the
+ appropriate northbound or southbound values
+ """
+ if type == "nni":
+ kwargs = {
+ 'port_no': port_num,
+ 'intf_id': self.platform.intf_id_to_port_no(port_num,
+ Port.ETHERNET_NNI),
+ "device_id": self.device.device_id
+ }
+ nni_port = NniPort
+ port = nni_port( **kwargs)
+ return port
+ elif type == "pon":
+ # PON ports require a different configuration
+ # intf_id and pon_id are currently equal.
+ kwargs = {
+ 'port_no': port_num,
+ 'intf_id': self.platform.intf_id_to_port_no(port_num,
+ Port.PON_OLT),
+ 'pon-id': self.platform.intf_id_to_port_no(port_num,
+ Port.PON_OLT),
+ "device_id": self.device.device_id
+ }
+ pon_port = PonPort
+ port = pon_port(**kwargs)
+ return port
+
+ else:
+ self.log.exception("Unknown port type")
+ raise Exception("Unknown port type")
+
+ except Exception as err:
+ self.log.exception("Unknown port type", error=err)
+ raise Exception(err)
+
+ def update_port_object_kpi_data(self, port_object, datadict={}):
+ """
+ This method takes the formatted data the is marshalled from
+ the initicator collector and updates the corresponding property by
+ attr get and set.
+
+ :param port: The port class to be updated
+ :param datadict:
+ :return:
+ """
+
+ try:
+ cur_attr = ""
+ if isinstance(port_object, NniPort):
+ for k, v in datadict.items():
+ cur_attr = k
+ if hasattr(port_object, k):
+ setattr(port_object, k, v)
+ elif isinstance(port_object, PonPort):
+ for k, v in datadict.items():
+ cur_attr = k
+ if hasattr(port_object, k):
+ setattr(port_object, k, v)
+ else:
+ raise Exception("Must be either PON or NNI port.")
+ return
+ except Exception as err:
+ self.log.exception("Caught error updating port data: ", cur_attr=cur_attr, errormsg=err.message)
+ raise Exception(err)
+
+
+class PonPort(object):
+ """
+ This is a highly reduced version taken from the adtran pon_port.
+ TODO: Extend for use in the openolt adapter set.
+ """
+ MAX_ONUS_SUPPORTED = 256
+ DEFAULT_ENABLED = False
+ MAX_DEPLOYMENT_RANGE = 25000 # Meters (OLT-PB maximum)
+
+ _MCAST_ONU_ID = 253
+ _MCAST_ALLOC_BASE = 0x500
+
+ _SUPPORTED_ACTIVATION_METHODS = ['autodiscovery'] # , 'autoactivate']
+ _SUPPORTED_AUTHENTICATION_METHODS = ['serial-number']
+
+ def __init__(self, **kwargs):
+ assert 'pon-id' in kwargs, 'PON ID not found'
+
+ self._pon_id = kwargs['pon-id']
+ self._device_id = kwargs['device_id']
+ self._intf_id = kwargs['intf_id']
+ self._port_no = kwargs['port_no']
+ self._port_id = 0
+ # self._name = 'xpon 0/{}'.format(self._pon_id+1)
+ self._label = 'pon-{}'.format(self._pon_id)
+
+ self._onus = {} # serial_number-base64 -> ONU (allowed list)
+ self._onu_by_id = {} # onu-id -> ONU
+
+ """
+ Statistics taken from nni_port
+ self.intf_id = 0 #handled by getter
+ self.port_no = 0 #handled by getter
+ self.port_id = 0 #handled by getter
+
+ Note: In the current implementation of the kpis coming from the BAL the stats are the
+ samne model for NNI and PON.
+
+ TODO: Integrate additional kpis for the PON and other southbound port objecgts.
+
+ """
+
+ self.rx_bytes = 0
+ self.rx_packets = 0
+ self.rx_mcast_packets = 0
+ self.rx_bcast_packets = 0
+ self.rx_error_packets = 0
+ self.tx_bytes = 0
+ self.tx_packets = 0
+ self.tx_ucast_packets = 0
+ self.tx_mcast_packets = 0
+ self.tx_bcast_packets = 0
+ self.tx_error_packets = 0
+ return
+
+ def __str__(self):
+ return "PonPort-{}: Admin: {}, Oper: {}, OLT: {}".format(self._label,
+ self._admin_state,
+ self._oper_status,
+ self.olt)
+
+ @property
+ def intf_id(self):
+ return self._intf_id
+
+ @intf_id.setter
+ def intf_id(self, value):
+ self._intf_id = value
+
+ @property
+ def pon_id(self):
+ return self._pon_id
+
+ @pon_id.setter
+ def pon_id(self, value):
+ self._pon_id = value
+
+ @property
+ def port_no(self):
+ return self._port_no
+
+ @port_no.setter
+ def port_no(self, value):
+ self._port_no = value
+
+ @property
+ def port_id(self):
+ return self._port_id
+
+ @intf_id.setter
+ def port_id(self, value):
+ self._port_id = value
+
+ @property
+ def onus(self):
+ """
+ Get a set of all ONUs. While the set is immutable, do not use this method
+ to get a collection that you will iterate through that my yield the CPU
+ such as inline callback. ONUs may be deleted at any time and they will
+ set some references to other objects to NULL during the 'delete' call.
+ Instead, get a list of ONU-IDs and iterate on these and call the 'onu'
+ method below (which will return 'None' if the ONU has been deleted.
+
+ :return: (frozenset) collection of ONU objects on this PON
+ """
+ return frozenset(self._onus.values())
+
+ @property
+ def onu_ids(self):
+ return frozenset(self._onu_by_id.keys())
+
+ def onu(self, onu_id):
+ return self._onu_by_id.get(onu_id)
+
+
+class NniPort(object):
+ """
+ Northbound network port, often Ethernet-based
+
+ This is a highly reduced version taken from the adtran nni_port code set
+ TODO: add functions to allow for port specific values and operations
+
+ """
+ def __init__(self, **kwargs):
+ # TODO: Extend for use in the openolt adapter set.
+ self.port_no = kwargs.get('port_no')
+ self._port_no = self.port_no
+ self._name = kwargs.get('name', 'nni-{}'.format(self._port_no))
+ self._logical_port = None
+
+ # Statistics
+ self.intf_id = kwargs.pop('intf_id', None)
+ self.port_no = 0
+ self.rx_bytes = 0
+ self.rx_packets = 0
+ self.rx_mcast_packets = 0
+ self.rx_bcast_packets = 0
+ self.rx_error_packets = 0
+ self.tx_bytes = 0
+ self.tx_packets = 0
+ self.tx_ucast_packets = 0
+ self.tx_mcast_packets = 0
+ self.tx_bcast_packets = 0
+ self.tx_error_packets = 0
+ return
+
+ def __str__(self):
+ return "NniPort-{}: Admin: {}, Oper: {}, parent: {}".format(self._port_no,
+ self._admin_state,
+ self._oper_status,
+ self._parent)
diff --git a/python/common/__init__.py b/python/common/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/common/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/common/event_bus.py b/python/common/event_bus.py
new file mode 100644
index 0000000..e717c16
--- /dev/null
+++ b/python/common/event_bus.py
@@ -0,0 +1,194 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+A simple internal pub/sub event bus with topics and filter-based registration.
+"""
+import re
+
+import structlog
+
+
+log = structlog.get_logger()
+
+
+class _Subscription(object):
+
+ __slots__ = ('bus', 'predicate', 'callback', 'topic')
+ def __init__(self, bus, predicate, callback, topic=None):
+ self.bus = bus
+ self.predicate = predicate
+ self.callback = callback
+ self.topic = topic
+
+
+class EventBus(object):
+
+ def __init__(self):
+ self.subscriptions = {} # topic -> list of _Subscription objects
+ # topic None holds regexp based topic subs.
+ self.subs_topic_map = {} # to aid fast lookup when unsubscribing
+
+ def list_subscribers(self, topic=None):
+ if topic is None:
+ return sum(self.subscriptions.itervalues(), [])
+ else:
+ if topic in self.subscriptions:
+ return self.subscriptions[topic]
+ else:
+ return []
+
+ @staticmethod
+ def _get_topic_key(topic):
+ if isinstance(topic, str):
+ return topic
+ elif hasattr(topic, 'match'):
+ return None
+ else:
+ raise AttributeError('topic not a string nor a compiled regex')
+
+ def subscribe(self, topic, callback, predicate=None):
+ """
+ Subscribe to given topic with predicate and register the callback
+ :param topic: String topic (explicit) or regexp based topic filter.
+ :param callback: Callback method with signature def func(topic, msg)
+ :param predicate: Optional method/function signature def predicate(msg)
+ :return: Subscription object which can be used to unsubscribe
+ """
+ subscription = _Subscription(self, predicate, callback, topic)
+ topic_key = self._get_topic_key(topic)
+ self.subscriptions.setdefault(topic_key, []).append(subscription)
+ self.subs_topic_map[subscription] = topic_key
+ return subscription
+
+ def unsubscribe(self, subscription):
+ """
+ Remove given subscription
+ :param subscription: subscription object as was returned by subscribe
+ :return: None
+ """
+ topic_key = self.subs_topic_map[subscription]
+ self.subscriptions[topic_key].remove(subscription)
+
+ def publish(self, topic, msg):
+ """
+ Publish given message to all subscribers registered with topic taking
+ the predicate functions into account.
+ :param topic: String topic
+ :param msg: Arbitrary python data as message
+ :return: None
+ """
+ from copy import copy
+
+ def passes(msg, predicate):
+ try:
+ return predicate(msg)
+ except Exception, e:
+ return False # failed predicate function treated as no match
+
+ # lookup subscribers with explicit topic subscriptions
+ subscribers = self.subscriptions.get(topic, [])
+
+ # add matching regexp topic subscribers
+ subscribers.extend(s for s in self.subscriptions.get(None, [])
+ if s.topic.match(topic))
+
+ # iterate over a shallow-copy of subscribers
+ for candidate in copy(subscribers):
+ predicate = candidate.predicate
+ if predicate is None or passes(msg, predicate):
+ try:
+ candidate.callback(topic, msg)
+ except Exception, e:
+ log.exception('callback-failed', e=repr(e), topic=topic)
+
+
+
+default_bus = EventBus()
+
+
+class EventBusClient(object):
+ """
+ Primary interface to the EventBus. Usage:
+
+ Publish:
+ >>> events = EventBusClient()
+ >>> msg = dict(a=1, b='foo')
+ >>> events.publish('a.topic', msg)
+
+ Subscribe to get all messages on specific topic:
+ >>> def got_event(topic, msg):
+ >>> print topic, ':', msg
+ >>> events = EventBusClient()
+ >>> events.subscribe('a.topic', got_event)
+
+ Subscribe to get messages matching predicate on specific topic:
+ >>> def got_event(topic, msg):
+ >>> print topic, ':', msg
+ >>> events = EventBusClient()
+ >>> events.subscribe('a.topic', got_event, lambda msg: msg.len() < 100)
+
+ Use a DeferredQueue to buffer incoming messages
+ >>> queue = DeferredQueue()
+ >>> events = EventBusClient()
+ >>> events.subscribe('a.topic', lambda _, msg: queue.put(msg))
+
+ """
+ def __init__(self, bus=None):
+ """
+ Obtain a client interface for the pub/sub event bus.
+ :param bus: An optional specific event bus. Inteded for mainly test
+ use. If not provided, the process default bus will be used, which is
+ the preferred use (a process shall not need more than one bus).
+ """
+ self.bus = bus or default_bus
+
+ def publish(self, topic, msg):
+ """
+ Publish given msg to given topic.
+ :param topic: String topic
+ :param msg: Arbitrary python data as message
+ :return: None
+ """
+ self.bus.publish(topic, msg)
+
+ def subscribe(self, topic, callback, predicate=None):
+ """
+ Subscribe to given topic with predicate and register the callback
+ :param topic: String topic (explicit) or regexp based topic filter.
+ :param callback: Callback method with signature def func(topic, msg)
+ :param predicate: Optional method/function with signature
+ def predicate(msg)
+ :return: Subscription object which can be used to unsubscribe
+ """
+ return self.bus.subscribe(topic, callback, predicate)
+
+ def unsubscribe(self, subscription):
+ """
+ Remove given subscription
+ :param subscription: subscription object as was returned by subscribe
+ :return: None
+ """
+ return self.bus.unsubscribe(subscription)
+
+ def list_subscribers(self, topic=None):
+ """
+ Return list of subscribers. If topci is provided, it is filtered for
+ those subscribing to the topic.
+ :param topic: Optional topic
+ :return: List of subscriptions
+ """
+ return self.bus.list_subscribers(topic)
diff --git a/python/common/manhole.py b/python/common/manhole.py
new file mode 100644
index 0000000..c00c900
--- /dev/null
+++ b/python/common/manhole.py
@@ -0,0 +1,129 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import os
+import rlcompleter
+from pprint import pprint
+
+import structlog
+from twisted.conch import manhole_ssh
+from twisted.conch.manhole import ColoredManhole
+from twisted.conch.ssh import keys
+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
+from twisted.cred.portal import Portal
+from twisted.internet import reactor
+
+log = structlog.get_logger()
+
+
+MANHOLE_SERVER_RSA_PRIVATE = './manhole_rsa_key'
+MANHOLE_SERVER_RSA_PUBLIC = './manhole_rsa_key.pub'
+
+
+def get_rsa_keys():
+ if not (os.path.exists(MANHOLE_SERVER_RSA_PUBLIC) and \
+ os.path.exists(MANHOLE_SERVER_RSA_PRIVATE)):
+ # generate a RSA keypair
+ log.info('generate-rsa-keypair')
+ from Crypto.PublicKey import RSA
+ rsa_key = RSA.generate(1024)
+ public_key_str = rsa_key.publickey().exportKey(format='OpenSSH')
+ private_key_str = rsa_key.exportKey()
+
+ # save keys for next time
+ file(MANHOLE_SERVER_RSA_PUBLIC, 'w+b').write(public_key_str)
+ file(MANHOLE_SERVER_RSA_PRIVATE, 'w+b').write(private_key_str)
+ log.debug('saved-rsa-keypair', public=MANHOLE_SERVER_RSA_PUBLIC,
+ private=MANHOLE_SERVER_RSA_PRIVATE)
+ else:
+ public_key_str = file(MANHOLE_SERVER_RSA_PUBLIC).read()
+ private_key_str = file(MANHOLE_SERVER_RSA_PRIVATE).read()
+ return public_key_str, private_key_str
+
+
+class ManholeWithCompleter(ColoredManhole):
+
+ def __init__(self, namespace):
+ namespace['manhole'] = self
+ super(ManholeWithCompleter, self).__init__(namespace)
+ self.last_tab = None
+ self.completer = rlcompleter.Completer(self.namespace)
+
+ def handle_TAB(self):
+ if self.last_tab != self.lineBuffer:
+ self.last_tab = self.lineBuffer
+ return
+
+ buffer = ''.join(self.lineBuffer)
+ completions = []
+ maxlen = 3
+ for c in xrange(1000):
+ candidate = self.completer.complete(buffer, c)
+ if not candidate:
+ break
+
+ if len(candidate) > maxlen:
+ maxlen = len(candidate)
+
+ completions.append(candidate)
+
+ if len(completions) == 1:
+ rest = completions[0][len(buffer):]
+ self.terminal.write(rest)
+ self.lineBufferIndex += len(rest)
+ self.lineBuffer.extend(rest)
+
+ elif len(completions):
+ maxlen += 3
+ numcols = self.width / maxlen
+ self.terminal.nextLine()
+ for idx, candidate in enumerate(completions):
+ self.terminal.write('%%-%ss' % maxlen % candidate)
+ if not ((idx + 1) % numcols):
+ self.terminal.nextLine()
+ self.terminal.nextLine()
+ self.drawInputLine()
+
+
+class Manhole(object):
+
+ def __init__(self, port, pws, **kw):
+ kw.update(globals())
+ kw['pp'] = pprint
+
+ realm = manhole_ssh.TerminalRealm()
+ manhole = ManholeWithCompleter(kw)
+
+ def windowChanged(_, win_size):
+ manhole.terminalSize(*reversed(win_size[:2]))
+
+ realm.sessionFactory.windowChanged = windowChanged
+ realm.chainedProtocolFactory.protocolFactory = lambda _: manhole
+ portal = Portal(realm)
+ portal.registerChecker(InMemoryUsernamePasswordDatabaseDontUse(**pws))
+ factory = manhole_ssh.ConchFactory(portal)
+ public_key_str, private_key_str = get_rsa_keys()
+ factory.publicKeys = {
+ 'ssh-rsa': keys.Key.fromString(public_key_str)
+ }
+ factory.privateKeys = {
+ 'ssh-rsa': keys.Key.fromString(private_key_str)
+ }
+ reactor.listenTCP(port, factory, interface='localhost')
+
+
+if __name__ == '__main__':
+ Manhole(12222, dict(admin='admin'))
+ reactor.run()
diff --git a/python/common/openflow/__init__.py b/python/common/openflow/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/common/openflow/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/common/openflow/utils.py b/python/common/openflow/utils.py
new file mode 100644
index 0000000..456ae06
--- /dev/null
+++ b/python/common/openflow/utils.py
@@ -0,0 +1,558 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import structlog
+
+from python.protos import openflow_13_pb2 as ofp
+from hashlib import md5
+
+log = structlog.get_logger()
+
+# aliases
+ofb_field = ofp.ofp_oxm_ofb_field
+action = ofp.ofp_action
+
+# OFPAT_* shortcuts
+OUTPUT = ofp.OFPAT_OUTPUT
+COPY_TTL_OUT = ofp.OFPAT_COPY_TTL_OUT
+COPY_TTL_IN = ofp.OFPAT_COPY_TTL_IN
+SET_MPLS_TTL = ofp.OFPAT_SET_MPLS_TTL
+DEC_MPLS_TTL = ofp.OFPAT_DEC_MPLS_TTL
+PUSH_VLAN = ofp.OFPAT_PUSH_VLAN
+POP_VLAN = ofp.OFPAT_POP_VLAN
+PUSH_MPLS = ofp.OFPAT_PUSH_MPLS
+POP_MPLS = ofp.OFPAT_POP_MPLS
+SET_QUEUE = ofp.OFPAT_SET_QUEUE
+GROUP = ofp.OFPAT_GROUP
+SET_NW_TTL = ofp.OFPAT_SET_NW_TTL
+NW_TTL = ofp.OFPAT_DEC_NW_TTL
+SET_FIELD = ofp.OFPAT_SET_FIELD
+PUSH_PBB = ofp.OFPAT_PUSH_PBB
+POP_PBB = ofp.OFPAT_POP_PBB
+EXPERIMENTER = ofp.OFPAT_EXPERIMENTER
+
+# OFPXMT_OFB_* shortcuts (incomplete)
+IN_PORT = ofp.OFPXMT_OFB_IN_PORT
+IN_PHY_PORT = ofp.OFPXMT_OFB_IN_PHY_PORT
+METADATA = ofp.OFPXMT_OFB_METADATA
+ETH_DST = ofp.OFPXMT_OFB_ETH_DST
+ETH_SRC = ofp.OFPXMT_OFB_ETH_SRC
+ETH_TYPE = ofp.OFPXMT_OFB_ETH_TYPE
+VLAN_VID = ofp.OFPXMT_OFB_VLAN_VID
+VLAN_PCP = ofp.OFPXMT_OFB_VLAN_PCP
+IP_DSCP = ofp.OFPXMT_OFB_IP_DSCP
+IP_ECN = ofp.OFPXMT_OFB_IP_ECN
+IP_PROTO = ofp.OFPXMT_OFB_IP_PROTO
+IPV4_SRC = ofp.OFPXMT_OFB_IPV4_SRC
+IPV4_DST = ofp.OFPXMT_OFB_IPV4_DST
+TCP_SRC = ofp.OFPXMT_OFB_TCP_SRC
+TCP_DST = ofp.OFPXMT_OFB_TCP_DST
+UDP_SRC = ofp.OFPXMT_OFB_UDP_SRC
+UDP_DST = ofp.OFPXMT_OFB_UDP_DST
+SCTP_SRC = ofp.OFPXMT_OFB_SCTP_SRC
+SCTP_DST = ofp.OFPXMT_OFB_SCTP_DST
+ICMPV4_TYPE = ofp.OFPXMT_OFB_ICMPV4_TYPE
+ICMPV4_CODE = ofp.OFPXMT_OFB_ICMPV4_CODE
+ARP_OP = ofp.OFPXMT_OFB_ARP_OP
+ARP_SPA = ofp.OFPXMT_OFB_ARP_SPA
+ARP_TPA = ofp.OFPXMT_OFB_ARP_TPA
+ARP_SHA = ofp.OFPXMT_OFB_ARP_SHA
+ARP_THA = ofp.OFPXMT_OFB_ARP_THA
+IPV6_SRC = ofp.OFPXMT_OFB_IPV6_SRC
+IPV6_DST = ofp.OFPXMT_OFB_IPV6_DST
+IPV6_FLABEL = ofp.OFPXMT_OFB_IPV6_FLABEL
+ICMPV6_TYPE = ofp.OFPXMT_OFB_ICMPV6_TYPE
+ICMPV6_CODE = ofp.OFPXMT_OFB_ICMPV6_CODE
+IPV6_ND_TARGET = ofp.OFPXMT_OFB_IPV6_ND_TARGET
+OFB_IPV6_ND_SLL = ofp.OFPXMT_OFB_IPV6_ND_SLL
+IPV6_ND_TLL = ofp.OFPXMT_OFB_IPV6_ND_TLL
+MPLS_LABEL = ofp.OFPXMT_OFB_MPLS_LABEL
+MPLS_TC = ofp.OFPXMT_OFB_MPLS_TC
+MPLS_BOS = ofp.OFPXMT_OFB_MPLS_BOS
+PBB_ISID = ofp.OFPXMT_OFB_PBB_ISID
+TUNNEL_ID = ofp.OFPXMT_OFB_TUNNEL_ID
+IPV6_EXTHDR = ofp.OFPXMT_OFB_IPV6_EXTHDR
+
+
+# ofp_action_* shortcuts
+
+def output(port, max_len=ofp.OFPCML_MAX):
+ return action(
+ type=OUTPUT,
+ output=ofp.ofp_action_output(port=port, max_len=max_len)
+ )
+
+
+def mpls_ttl(ttl):
+ return action(
+ type=SET_MPLS_TTL,
+ mpls_ttl=ofp.ofp_action_mpls_ttl(mpls_ttl=ttl)
+ )
+
+
+def push_vlan(eth_type):
+ return action(
+ type=PUSH_VLAN,
+ push=ofp.ofp_action_push(ethertype=eth_type)
+ )
+
+
+def pop_vlan():
+ return action(
+ type=POP_VLAN
+ )
+
+
+def pop_mpls(eth_type):
+ return action(
+ type=POP_MPLS,
+ pop_mpls=ofp.ofp_action_pop_mpls(ethertype=eth_type)
+ )
+
+
+def group(group_id):
+ return action(
+ type=GROUP,
+ group=ofp.ofp_action_group(group_id=group_id)
+ )
+
+
+def nw_ttl(nw_ttl):
+ return action(
+ type=NW_TTL,
+ nw_ttl=ofp.ofp_action_nw_ttl(nw_ttl=nw_ttl)
+ )
+
+
+def set_field(field):
+ return action(
+ type=SET_FIELD,
+ set_field=ofp.ofp_action_set_field(
+ field=ofp.ofp_oxm_field(
+ oxm_class=ofp.OFPXMC_OPENFLOW_BASIC,
+ ofb_field=field))
+ )
+
+
+def experimenter(experimenter, data):
+ return action(
+ type=EXPERIMENTER,
+ experimenter=ofp.ofp_action_experimenter(
+ experimenter=experimenter, data=data)
+ )
+
+
+# ofb_field generators (incomplete set)
+
+def in_port(_in_port):
+ return ofb_field(type=IN_PORT, port=_in_port)
+
+
+def in_phy_port(_in_phy_port):
+ return ofb_field(type=IN_PHY_PORT, port=_in_phy_port)
+
+
+def metadata(_table_metadata):
+ return ofb_field(type=METADATA, table_metadata=_table_metadata)
+
+
+def eth_dst(_eth_dst):
+ return ofb_field(type=ETH_DST, table_metadata=_eth_dst)
+
+
+def eth_src(_eth_src):
+ return ofb_field(type=ETH_SRC, table_metadata=_eth_src)
+
+
+def eth_type(_eth_type):
+ return ofb_field(type=ETH_TYPE, eth_type=_eth_type)
+
+
+def vlan_vid(_vlan_vid):
+ return ofb_field(type=VLAN_VID, vlan_vid=_vlan_vid)
+
+
+def vlan_pcp(_vlan_pcp):
+ return ofb_field(type=VLAN_PCP, vlan_pcp=_vlan_pcp)
+
+
+def ip_dscp(_ip_dscp):
+ return ofb_field(type=IP_DSCP, ip_dscp=_ip_dscp)
+
+
+def ip_ecn(_ip_ecn):
+ return ofb_field(type=IP_ECN, ip_ecn=_ip_ecn)
+
+
+def ip_proto(_ip_proto):
+ return ofb_field(type=IP_PROTO, ip_proto=_ip_proto)
+
+
+def ipv4_src(_ipv4_src):
+ return ofb_field(type=IPV4_SRC, ipv4_src=_ipv4_src)
+
+
+def ipv4_dst(_ipv4_dst):
+ return ofb_field(type=IPV4_DST, ipv4_dst=_ipv4_dst)
+
+
+def tcp_src(_tcp_src):
+ return ofb_field(type=TCP_SRC, tcp_src=_tcp_src)
+
+
+def tcp_dst(_tcp_dst):
+ return ofb_field(type=TCP_DST, tcp_dst=_tcp_dst)
+
+
+def udp_src(_udp_src):
+ return ofb_field(type=UDP_SRC, udp_src=_udp_src)
+
+
+def udp_dst(_udp_dst):
+ return ofb_field(type=UDP_DST, udp_dst=_udp_dst)
+
+
+def sctp_src(_sctp_src):
+ return ofb_field(type=SCTP_SRC, sctp_src=_sctp_src)
+
+
+def sctp_dst(_sctp_dst):
+ return ofb_field(type=SCTP_DST, sctp_dst=_sctp_dst)
+
+
+def icmpv4_type(_icmpv4_type):
+ return ofb_field(type=ICMPV4_TYPE, icmpv4_type=_icmpv4_type)
+
+
+def icmpv4_code(_icmpv4_code):
+ return ofb_field(type=ICMPV4_CODE, icmpv4_code=_icmpv4_code)
+
+
+def arp_op(_arp_op):
+ return ofb_field(type=ARP_OP, arp_op=_arp_op)
+
+
+def arp_spa(_arp_spa):
+ return ofb_field(type=ARP_SPA, arp_spa=_arp_spa)
+
+
+def arp_tpa(_arp_tpa):
+ return ofb_field(type=ARP_TPA, arp_tpa=_arp_tpa)
+
+
+def arp_sha(_arp_sha):
+ return ofb_field(type=ARP_SHA, arp_sha=_arp_sha)
+
+
+def arp_tha(_arp_tha):
+ return ofb_field(type=ARP_THA, arp_tha=_arp_tha)
+
+
+def ipv6_src(_ipv6_src):
+ return ofb_field(type=IPV6_SRC, arp_tha=_ipv6_src)
+
+
+def ipv6_dst(_ipv6_dst):
+ return ofb_field(type=IPV6_DST, arp_tha=_ipv6_dst)
+
+
+def ipv6_flabel(_ipv6_flabel):
+ return ofb_field(type=IPV6_FLABEL, arp_tha=_ipv6_flabel)
+
+
+def ipmpv6_type(_icmpv6_type):
+ return ofb_field(type=ICMPV6_TYPE, arp_tha=_icmpv6_type)
+
+
+def icmpv6_code(_icmpv6_code):
+ return ofb_field(type=ICMPV6_CODE, arp_tha=_icmpv6_code)
+
+
+def ipv6_nd_target(_ipv6_nd_target):
+ return ofb_field(type=IPV6_ND_TARGET, arp_tha=_ipv6_nd_target)
+
+
+def ofb_ipv6_nd_sll(_ofb_ipv6_nd_sll):
+ return ofb_field(type=OFB_IPV6_ND_SLL, arp_tha=_ofb_ipv6_nd_sll)
+
+
+def ipv6_nd_tll(_ipv6_nd_tll):
+ return ofb_field(type=IPV6_ND_TLL, arp_tha=_ipv6_nd_tll)
+
+
+def mpls_label(_mpls_label):
+ return ofb_field(type=MPLS_LABEL, arp_tha=_mpls_label)
+
+
+def mpls_tc(_mpls_tc):
+ return ofb_field(type=MPLS_TC, arp_tha=_mpls_tc)
+
+
+def mpls_bos(_mpls_bos):
+ return ofb_field(type=MPLS_BOS, arp_tha=_mpls_bos)
+
+
+def pbb_isid(_pbb_isid):
+ return ofb_field(type=PBB_ISID, arp_tha=_pbb_isid)
+
+
+def tunnel_id(_tunnel_id):
+ return ofb_field(type=TUNNEL_ID, arp_tha=_tunnel_id)
+
+
+def ipv6_exthdr(_ipv6_exthdr):
+ return ofb_field(type=IPV6_EXTHDR, arp_tha=_ipv6_exthdr)
+
+
+# frequently used extractors:
+
+def get_actions(flow):
+ """Extract list of ofp_action objects from flow spec object"""
+ assert isinstance(flow, ofp.ofp_flow_stats)
+ # we have the following hard assumptions for now
+ for instruction in flow.instructions:
+ if instruction.type == ofp.OFPIT_APPLY_ACTIONS:
+ return instruction.actions.actions
+
+
+def get_ofb_fields(flow):
+ assert isinstance(flow, ofp.ofp_flow_stats)
+ assert flow.match.type == ofp.OFPMT_OXM
+ ofb_fields = []
+ for field in flow.match.oxm_fields:
+ assert field.oxm_class == ofp.OFPXMC_OPENFLOW_BASIC
+ ofb_fields.append(field.ofb_field)
+ return ofb_fields
+
+
+def get_out_port(flow):
+ for action in get_actions(flow):
+ if action.type == OUTPUT:
+ return action.output.port
+ return None
+
+
+def get_in_port(flow):
+ for field in get_ofb_fields(flow):
+ if field.type == IN_PORT:
+ return field.port
+ return None
+
+
+def get_goto_table_id(flow):
+ for instruction in flow.instructions:
+ if instruction.type == ofp.OFPIT_GOTO_TABLE:
+ return instruction.goto_table.table_id
+ return None
+
+
+def get_metadata(flow):
+ ''' legacy get method (only want lower 32 bits '''
+ for field in get_ofb_fields(flow):
+ if field.type == METADATA:
+ return field.table_metadata & 0xffffffff
+ return None
+
+
+def get_metadata_64_bit(flow):
+ for field in get_ofb_fields(flow):
+ if field.type == METADATA:
+ return field.table_metadata
+ return None
+
+
+def get_port_number_from_metadata(flow):
+ """
+ The port number (UNI on ONU) is in the lower 32-bits of metadata and
+ the inner_tag is in the upper 32-bits
+
+ This is set in the ONOS OltPipeline as a metadata field
+ """
+ md = get_metadata_64_bit(flow)
+
+ if md is None:
+ return None
+
+ if md <= 0xffffffff:
+ log.warn('onos-upgrade-suggested',
+ netadata=md,
+ message='Legacy MetaData detected form OltPipeline')
+ return md
+
+ return md & 0xffffffff
+
+
+def get_inner_tag_from_metadata(flow):
+ """
+ The port number (UNI on ONU) is in the lower 32-bits of metadata and
+ the inner_tag is in the upper 32-bits
+
+ This is set in the ONOS OltPipeline as a metadata field
+ """
+ md = get_metadata_64_bit(flow)
+
+ if md is None:
+ return None
+
+ if md <= 0xffffffff:
+ log.warn('onos-upgrade-suggested',
+ netadata=md,
+ message='Legacy MetaData detected form OltPipeline')
+ return md
+
+ return (md >> 32) & 0xffffffff
+
+
+# test and extract next table and group information
+def has_next_table(flow):
+ return get_goto_table_id(flow) is not None
+
+
+def get_group(flow):
+ for action in get_actions(flow):
+ if action.type == GROUP:
+ return action.group.group_id
+ return None
+
+
+def has_group(flow):
+ return get_group(flow) is not None
+
+
+def mk_oxm_fields(match_fields):
+ oxm_fields = [
+ ofp.ofp_oxm_field(
+ oxm_class=ofp.OFPXMC_OPENFLOW_BASIC,
+ ofb_field=field
+ ) for field in match_fields
+ ]
+
+ return oxm_fields
+
+
+def mk_instructions_from_actions(actions):
+ instructions_action = ofp.ofp_instruction_actions()
+ instructions_action.actions.extend(actions)
+ instruction = ofp.ofp_instruction(type=ofp.OFPIT_APPLY_ACTIONS,
+ actions=instructions_action)
+ return [instruction]
+
+
+def mk_simple_flow_mod(match_fields, actions, command=ofp.OFPFC_ADD,
+ next_table_id=None, **kw):
+ """
+ Convenience function to generare ofp_flow_mod message with OXM BASIC match
+ composed from the match_fields, and single APPLY_ACTIONS instruction with
+ a list if ofp_action objects.
+ :param match_fields: list(ofp_oxm_ofb_field)
+ :param actions: list(ofp_action)
+ :param command: one of OFPFC_*
+ :param kw: additional keyword-based params to ofp_flow_mod
+ :return: initialized ofp_flow_mod object
+ """
+ instructions = [
+ ofp.ofp_instruction(
+ type=ofp.OFPIT_APPLY_ACTIONS,
+ actions=ofp.ofp_instruction_actions(actions=actions)
+ )
+ ]
+ if next_table_id is not None:
+ instructions.append(ofp.ofp_instruction(
+ type=ofp.OFPIT_GOTO_TABLE,
+ goto_table=ofp.ofp_instruction_goto_table(table_id=next_table_id)
+ ))
+
+ return ofp.ofp_flow_mod(
+ command=command,
+ match=ofp.ofp_match(
+ type=ofp.OFPMT_OXM,
+ oxm_fields=[
+ ofp.ofp_oxm_field(
+ oxm_class=ofp.OFPXMC_OPENFLOW_BASIC,
+ ofb_field=field
+ ) for field in match_fields
+ ]
+ ),
+ instructions=instructions,
+ **kw
+ )
+
+
+def mk_multicast_group_mod(group_id, buckets, command=ofp.OFPGC_ADD):
+ group = ofp.ofp_group_mod(
+ command=command,
+ type=ofp.OFPGT_ALL,
+ group_id=group_id,
+ buckets=buckets
+ )
+ return group
+
+
+def hash_flow_stats(flow):
+ """
+ Return unique 64-bit integer hash for flow covering the following
+ attributes: 'table_id', 'priority', 'flags', 'cookie', 'match', '_instruction_string'
+ """
+ _instruction_string = ""
+ for _instruction in flow.instructions:
+ _instruction_string += _instruction.SerializeToString()
+
+ hex = md5('{},{},{},{},{},{}'.format(
+ flow.table_id,
+ flow.priority,
+ flow.flags,
+ flow.cookie,
+ flow.match.SerializeToString(),
+ _instruction_string
+ )).hexdigest()
+ return int(hex[:16], 16)
+
+
+def flow_stats_entry_from_flow_mod_message(mod):
+ flow = ofp.ofp_flow_stats(
+ table_id=mod.table_id,
+ priority=mod.priority,
+ idle_timeout=mod.idle_timeout,
+ hard_timeout=mod.hard_timeout,
+ flags=mod.flags,
+ cookie=mod.cookie,
+ match=mod.match,
+ instructions=mod.instructions
+ )
+ flow.id = hash_flow_stats(flow)
+ return flow
+
+
+def group_entry_from_group_mod(mod):
+ group = ofp.ofp_group_entry(
+ desc=ofp.ofp_group_desc(
+ type=mod.type,
+ group_id=mod.group_id,
+ buckets=mod.buckets
+ ),
+ stats=ofp.ofp_group_stats(
+ group_id=mod.group_id
+ # TODO do we need to instantiate bucket bins?
+ )
+ )
+ return group
+
+
+def mk_flow_stat(**kw):
+ return flow_stats_entry_from_flow_mod_message(mk_simple_flow_mod(**kw))
+
+
+def mk_group_stat(**kw):
+ return group_entry_from_group_mod(mk_multicast_group_mod(**kw))
diff --git a/python/common/structlog_setup.py b/python/common/structlog_setup.py
new file mode 100644
index 0000000..cbbda89
--- /dev/null
+++ b/python/common/structlog_setup.py
@@ -0,0 +1,132 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Setting up proper logging for Voltha"""
+
+import logging
+import logging.config
+from collections import OrderedDict
+
+import structlog
+from structlog.stdlib import BoundLogger, INFO
+
+try:
+ from thread import get_ident as _get_ident
+except ImportError:
+ from dummy_thread import get_ident as _get_ident
+
+
+class StructuredLogRenderer(object):
+ def __call__(self, logger, name, event_dict):
+ # in order to keep structured log data in event_dict to be forwarded as
+ # is, we need to pass it into the logger framework as the first
+ # positional argument.
+ args = (event_dict,)
+ kwargs = {}
+ return args, kwargs
+
+
+class PlainRenderedOrderedDict(OrderedDict):
+ """Our special version of OrderedDict that renders into string as a dict,
+ to make the log stream output cleaner.
+ """
+ def __repr__(self, _repr_running={}):
+ 'od.__repr__() <==> repr(od)'
+ call_key = id(self), _get_ident()
+ if call_key in _repr_running:
+ return '...'
+ _repr_running[call_key] = 1
+ try:
+ if not self:
+ return '{}'
+ return '{%s}' % ", ".join("%s: %s" % (k, v)
+ for k, v in self.items())
+ finally:
+ del _repr_running[call_key]
+
+
+def setup_logging(log_config, instance_id, verbosity_adjust=0):
+ """
+ Set up logging such that:
+ - The primary logging entry method is structlog
+ (see http://structlog.readthedocs.io/en/stable/index.html)
+ - By default, the logging backend is Python standard lib logger
+ """
+
+ def add_exc_info_flag_for_exception(_, name, event_dict):
+ if name == 'exception':
+ event_dict['exc_info'] = True
+ return event_dict
+
+ def add_instance_id(_, __, event_dict):
+ event_dict['instance_id'] = instance_id
+ return event_dict
+
+ # Configure standard logging
+ logging.config.dictConfig(log_config)
+ logging.root.level -= 10 * verbosity_adjust
+
+ processors = [
+ add_exc_info_flag_for_exception,
+ structlog.processors.StackInfoRenderer(),
+ structlog.processors.format_exc_info,
+ add_instance_id,
+ StructuredLogRenderer(),
+ ]
+ structlog.configure(logger_factory=structlog.stdlib.LoggerFactory(),
+ context_class=PlainRenderedOrderedDict,
+ wrapper_class=BoundLogger,
+ processors=processors)
+
+ # Mark first line of log
+ log = structlog.get_logger()
+ log.info("first-line")
+ return log
+
+
+def update_logging(instance_id, vcore_id):
+ """
+ Add the vcore id to the structured logger
+ :param vcore_id: The assigned vcore id
+ :return: structure logger
+ """
+ def add_exc_info_flag_for_exception(_, name, event_dict):
+ if name == 'exception':
+ event_dict['exc_info'] = True
+ return event_dict
+
+ def add_instance_id(_, __, event_dict):
+ event_dict['instance_id'] = instance_id
+ return event_dict
+
+ def add_vcore_id(_, __, event_dict):
+ event_dict['vcore_id'] = vcore_id
+ return event_dict
+
+ processors = [
+ add_exc_info_flag_for_exception,
+ structlog.processors.StackInfoRenderer(),
+ structlog.processors.format_exc_info,
+ add_instance_id,
+ add_vcore_id,
+ StructuredLogRenderer(),
+ ]
+ structlog.configure(processors=processors)
+
+ # Mark first line of log
+ log = structlog.get_logger()
+ log.info("updated-logger")
+ return log
diff --git a/python/common/tech_profile/README.md b/python/common/tech_profile/README.md
new file mode 100644
index 0000000..12610a7
--- /dev/null
+++ b/python/common/tech_profile/README.md
@@ -0,0 +1,347 @@
+# Technology Profile Management
+## Overview
+Technology profiles that are utilized by VOLTHA are stored in a prescribed structure in VOLTHA's key/value store, which is currently etcd. The key structure used to access technology profiles is /voltha/technology_profiles/<TECHNOLOGY>/<TID>; where TID is the numeric ID of the technology profile and TECHNOLOGY specifies the technology being utilized by the adapter, e.g. xgspon. While the TID key is a directory, the TECHNOLOGY key should be set to the JSON data that represents the technology profile values.
+
+
+
+`NOTE`: The content of a technology profile represents a contract between the technology profile definition and all adapters that consume that technology profile. The structure and content of the profiles are outside the scope of Technology Profile Management. Technology profile management only specifies the key/value structure in which profiles are stored.
+
+### Example:
+```sh
+/xgspon/64 {
+ "name": "4QueueHybridProfileMap1",
+ "profile_type": "XPON",
+ "version": 1,
+ "num_gem_ports": 4,
+ "instance_control": {
+ "onu": "multi-instance",
+ "uni": "single-instance",
+ "max_gem_payload_size": "auto"
+ },
+ "us_scheduler": {
+ "additional_bw": "auto",
+ "direction": "UPSTREAM",
+ "priority": 0,
+ "weight": 0,
+ "q_sched_policy": "hybrid"
+ },
+ "ds_scheduler": {
+ "additional_bw": "auto",
+ "direction": "DOWNSTREAM",
+ "priority": 0,
+ "weight": 0,
+ "q_sched_policy": "hybrid"
+ },
+ "upstream_gem_port_attribute_list": [
+ {
+ "pbit_map": "0b00000101",
+ "aes_encryption": "True",
+ "scheduling_policy": "WRR",
+ "priority_q": 4,
+ "weight": 25,
+ "discard_policy": "TailDrop",
+ "max_q_size": "auto",
+ "discard_config": {
+ "max_threshold": 0,
+ "min_threshold": 0,
+ "max_probability": 0
+ }
+ },
+ {
+ "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
+ }
+ },
+ {
+ "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
+ }
+ },
+ {
+ "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
+ }
+ }
+ ],
+ "downstream_gem_port_attribute_list": [
+ {
+ "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
+ }
+ },
+ {
+ "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
+ }
+ },
+ {
+ "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
+ }
+ },
+ {
+ "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
+ }
+ }
+ ]
+}
+```
+
+## Creating Technology Profiles
+Technology profiles are a simple JSON object. This JSON object can be created using a variety of tools such as Vim, Emacs, or various IDEs. JQ can be a useful tool for validating a JSON object. Once a file is created with the JSON object it can be stored in VOLTHA key/value store using the standard etcd command line tool etcdctl or using an HTTP POST operation using Curl.
+
+Assuming you are in a standard VOLTHA deployment within a Kubernetes cluster you can access the etcd key/value store using kubectl via the PODs named etcd-cluster-0000, etcd-cluster-0001, or etcd-cluster-0002. For the examples in this document etcd-cluster-0000 will be used, but it really shouldn't matter which is used.
+
+
+
+Assuming the Technology template is stored in a local file 4QueueHybridProfileMap1.json the following commands could be used to `store` or `update` the technical template into the proper location in the etcd key/value store:
+```sh
+# Store a Technology template using etcdctl
+jq -c . 4QueueHybridProfileMap1.json | kubectl exec -i etcd-cluster-0000 -- etcdctl set /xgspon/64
+
+# Store a Technology template using curl
+curl -sSL -XPUT http://10.233.53.161:2379/v2/keys/xgspon/64 -d value="$(jq -c . 4QueueHybridProfileMap1.json)"
+```
+
+In the examples above, the command jq is used. This command can be installed using standard package management tools on most Linux systems. In the examples the "-c" option is used to compress the JSON. Using this tool is not necessary, and if you choose not to use the tool, you can replace "jq -c ," in the above examples with the "cat" command. More on jq can be found at https://stedolan.github.io/jq/.
+
+
+
+## Listing Technical Profiles for a given Technology
+While both curl and etcdctl (via kubectl) can be used to list or view the available Technology profiles, etcdctl is easier, and thus will be used in the examples. For listing Technology profiles etcdctl ls is used. In can be used in conjunction with the -r option to recursively list profiles.
+```sh
+# List all the Technology profiles for a Technology
+kubectl exec -i etcd-cluster-0000 -- etcdctl ls /xgspon
+
+# Example output
+/xgspon/64
+/xgspon/65
+```
+
+A specified Technology profile can be viewed with the etcdctl get command. (Again, jq is used for presentation purposes, and is not required)
+```sh
+# Display a specified Technology profile, using jq to pretty print
+kubectl exec -i etcd-cluster-0000 -- etcdctl get /xgspon/64 | jq .
+
+# Example outpout
+{
+ "name": "4QueueHybridProfileMap1",
+ "profile_type": "XPON",
+ "version": 1,
+ "num_gem_ports": 4,
+ "instance_control": {
+ "onu": "multi-instance",
+ "uni": "single-instance",
+ "max_gem_payload_size": "auto"
+ },
+ "us_scheduler": {
+ "additional_bw": "auto",
+ "direction": "UPSTREAM",
+ "priority": 0,
+ "weight": 0,
+ "q_sched_policy": "hybrid"
+ },
+ "ds_scheduler": {
+ "additional_bw": "auto",
+ "direction": "DOWNSTREAM",
+ "priority": 0,
+ "weight": 0,
+ "q_sched_policy": "hybrid"
+ },
+ "upstream_gem_port_attribute_list": [
+ {
+ "pbit_map": "0b00000101",
+ "aes_encryption": "True",
+ "scheduling_policy": "WRR",
+ "priority_q": 4,
+ "weight": 25,
+ "discard_policy": "TailDrop",
+ "max_q_size": "auto",
+ "discard_config": {
+ "max_threshold": 0,
+ "min_threshold": 0,
+ "max_probability": 0
+ }
+ },
+ {
+ "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
+ }
+ },
+ {
+ "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
+ }
+ },
+ {
+ "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
+ }
+ }
+ ],
+ "downstream_gem_port_attribute_list": [
+ {
+ "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
+ }
+ },
+ {
+ "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
+ }
+ },
+ {
+ "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
+ }
+ },
+ {
+ "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
+ }
+ }
+ ]
+}
+```
+
+## Deleting Technology Profiles
+A technology profile or a technology profile tree can be removed using etcdctl rm.
+
+```sh
+# Remove a specific technology profile
+kubectl exec -i etcd-cluster-0000 -- etcdctl rm /xgspon/64
+
+# Remove all technology profiles associated with Technology xgspon and ID 64(including the profile ID key)
+kubectl exec -i etcd-cluster-0000 -- etcdctl rm --dir -r /xgspon/64
+```
+
+## Reference
+https://wiki.opencord.org/display/CORD/Technology+Profile+Management
+
diff --git a/python/common/tech_profile/__init__.py b/python/common/tech_profile/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/common/tech_profile/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/common/tech_profile/tech_profile.py b/python/common/tech_profile/tech_profile.py
new file mode 100644
index 0000000..abea364
--- /dev/null
+++ b/python/common/tech_profile/tech_profile.py
@@ -0,0 +1,583 @@
+#
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import json
+import ast
+from collections import namedtuple
+import structlog
+from enum import Enum
+
+from voltha.core.config.config_backend import ConsulStore
+from voltha.core.config.config_backend import EtcdStore
+from voltha.registry import registry
+from voltha.adapters.openolt.protos import openolt_pb2
+
+# logger
+log = structlog.get_logger()
+
+DEFAULT_TECH_PROFILE_TABLE_ID = 64
+
+# Enums used while creating TechProfileInstance
+Direction = Enum('Direction', ['UPSTREAM', 'DOWNSTREAM', 'BIDIRECTIONAL'],
+ start=0)
+SchedulingPolicy = Enum('SchedulingPolicy',
+ ['WRR', 'StrictPriority', 'Hybrid'], start=0)
+AdditionalBW = Enum('AdditionalBW', ['None', 'NA', 'BestEffort', 'Auto'],
+ start=0)
+DiscardPolicy = Enum('DiscardPolicy',
+ ['TailDrop', 'WTailDrop', 'RED', 'WRED'], start=0)
+InferredAdditionBWIndication = Enum('InferredAdditionBWIndication',
+ ['None', 'NoneAssured', 'BestEffort'],
+ start=0)
+
+
+class InstanceControl(object):
+ # Default value constants
+ ONU_DEFAULT_INSTANCE = 'multi-instance'
+ UNI_DEFAULT_INSTANCE = 'single-instance'
+ DEFAULT_NUM_GEM_PORTS = 1
+ DEFAULT_GEM_PAYLOAD_SIZE = 'auto'
+
+ def __init__(self, onu=ONU_DEFAULT_INSTANCE,
+ uni=UNI_DEFAULT_INSTANCE,
+ num_gem_ports=DEFAULT_NUM_GEM_PORTS,
+ max_gem_payload_size=DEFAULT_GEM_PAYLOAD_SIZE):
+ self.onu = onu
+ self.uni = uni
+ self.num_gem_ports = num_gem_ports
+ self.max_gem_payload_size = max_gem_payload_size
+
+
+class Scheduler(object):
+ # Default value constants
+ DEFAULT_ADDITIONAL_BW = 'auto'
+ DEFAULT_PRIORITY = 0
+ DEFAULT_WEIGHT = 0
+ DEFAULT_Q_SCHED_POLICY = 'hybrid'
+
+ def __init__(self, direction, additional_bw=DEFAULT_ADDITIONAL_BW,
+ priority=DEFAULT_PRIORITY,
+ weight=DEFAULT_WEIGHT,
+ q_sched_policy=DEFAULT_Q_SCHED_POLICY):
+ self.direction = direction
+ self.additional_bw = additional_bw
+ self.priority = priority
+ self.weight = weight
+ self.q_sched_policy = q_sched_policy
+
+
+class GemPortAttribute(object):
+ # Default value constants
+ DEFAULT_AES_ENCRYPTION = 'True'
+ DEFAULT_PRIORITY_Q = 0
+ DEFAULT_WEIGHT = 0
+ DEFAULT_MAX_Q_SIZE = 'auto'
+ DEFAULT_DISCARD_POLICY = DiscardPolicy.TailDrop.name
+
+ def __init__(self, pbit_map, discard_config,
+ aes_encryption=DEFAULT_AES_ENCRYPTION,
+ scheduling_policy=SchedulingPolicy.WRR.name,
+ priority_q=DEFAULT_PRIORITY_Q,
+ weight=DEFAULT_WEIGHT,
+ max_q_size=DEFAULT_MAX_Q_SIZE,
+ discard_policy=DiscardPolicy.TailDrop.name):
+ self.max_q_size = max_q_size
+ self.pbit_map = pbit_map
+ self.aes_encryption = aes_encryption
+ self.scheduling_policy = scheduling_policy
+ self.priority_q = priority_q
+ self.weight = weight
+ self.discard_policy = discard_policy
+ self.discard_config = discard_config
+
+
+class DiscardConfig(object):
+ # Default value constants
+ DEFAULT_MIN_THRESHOLD = 0
+ DEFAULT_MAX_THRESHOLD = 0
+ DEFAULT_MAX_PROBABILITY = 0
+
+ def __init__(self, min_threshold=DEFAULT_MIN_THRESHOLD,
+ max_threshold=DEFAULT_MAX_THRESHOLD,
+ max_probability=DEFAULT_MAX_PROBABILITY):
+ self.min_threshold = min_threshold
+ self.max_threshold = max_threshold
+ self.max_probability = max_probability
+
+
+class TechProfile(object):
+ # Constants used in default tech profile
+ DEFAULT_TECH_PROFILE_NAME = 'Default_1tcont_1gem_Profile'
+ DEFAULT_VERSION = 1.0
+ DEFAULT_GEMPORTS_COUNT = 1
+ pbits = ['0b11111111']
+
+ # Tech profile path prefix in kv store
+ KV_STORE_TECH_PROFILE_PATH_PREFIX = 'service/voltha/technology_profiles'
+
+ # Tech profile path in kv store
+ TECH_PROFILE_PATH = '{}/{}' # <technology>/<table_id>
+
+ # Tech profile instance path in kv store
+ # Format: <technology>/<table_id>/<uni_port_name>
+ TECH_PROFILE_INSTANCE_PATH = '{}/{}/{}'
+
+ # Tech-Profile JSON String Keys
+ NAME = 'name'
+ PROFILE_TYPE = 'profile_type'
+ VERSION = 'version'
+ NUM_GEM_PORTS = 'num_gem_ports'
+ INSTANCE_CONTROL = 'instance_control'
+ US_SCHEDULER = 'us_scheduler'
+ DS_SCHEDULER = 'ds_scheduler'
+ UPSTREAM_GEM_PORT_ATTRIBUTE_LIST = 'upstream_gem_port_attribute_list'
+ DOWNSTREAM_GEM_PORT_ATTRIBUTE_LIST = 'downstream_gem_port_attribute_list'
+ ONU = 'onu'
+ UNI = 'uni'
+ MAX_GEM_PAYLOAD_SIZE = 'max_gem_payload_size'
+ DIRECTION = 'direction'
+ ADDITIONAL_BW = 'additional_bw'
+ PRIORITY = 'priority'
+ Q_SCHED_POLICY = 'q_sched_policy'
+ WEIGHT = 'weight'
+ PBIT_MAP = 'pbit_map'
+ DISCARD_CONFIG = 'discard_config'
+ MAX_THRESHOLD = 'max_threshold'
+ MIN_THRESHOLD = 'min_threshold'
+ MAX_PROBABILITY = 'max_probability'
+ DISCARD_POLICY = 'discard_policy'
+ PRIORITY_Q = 'priority_q'
+ SCHEDULING_POLICY = 'scheduling_policy'
+ MAX_Q_SIZE = 'max_q_size'
+ AES_ENCRYPTION = 'aes_encryption'
+
+ def __init__(self, resource_mgr):
+ try:
+ self.args = registry('main').get_args()
+ self.resource_mgr = resource_mgr
+
+ if self.args.backend == 'etcd':
+ # KV store's IP Address and PORT
+ host, port = self.args.etcd.split(':', 1)
+ self._kv_store = EtcdStore(
+ host, port, TechProfile.
+ KV_STORE_TECH_PROFILE_PATH_PREFIX)
+ elif self.args.backend == 'consul':
+ # KV store's IP Address and PORT
+ host, port = self.args.consul.split(':', 1)
+ self._kv_store = ConsulStore(
+ host, port, TechProfile.
+ KV_STORE_TECH_PROFILE_PATH_PREFIX)
+
+ # self.tech_profile_instance_store = dict()
+ except Exception as e:
+ log.exception("exception-in-init")
+ raise Exception(e)
+
+ class DefaultTechProfile(object):
+ def __init__(self, name, **kwargs):
+ self.name = name
+ self.profile_type = kwargs[TechProfile.PROFILE_TYPE]
+ self.version = kwargs[TechProfile.VERSION]
+ self.num_gem_ports = kwargs[TechProfile.NUM_GEM_PORTS]
+ self.instance_control = kwargs[TechProfile.INSTANCE_CONTROL]
+ self.us_scheduler = kwargs[TechProfile.US_SCHEDULER]
+ self.ds_scheduler = kwargs[TechProfile.DS_SCHEDULER]
+ self.upstream_gem_port_attribute_list = kwargs[
+ TechProfile.UPSTREAM_GEM_PORT_ATTRIBUTE_LIST]
+ self.downstream_gem_port_attribute_list = kwargs[
+ TechProfile.DOWNSTREAM_GEM_PORT_ATTRIBUTE_LIST]
+
+ def to_json(self):
+ return json.dumps(self, default=lambda o: o.__dict__,
+ indent=4)
+
+ def get_tp_path(self, table_id, uni_port_name):
+ return TechProfile.TECH_PROFILE_INSTANCE_PATH.format(
+ self.resource_mgr.technology, table_id, uni_port_name)
+
+ def create_tech_profile_instance(self, table_id, uni_port_name, intf_id):
+ tech_profile_instance = None
+ try:
+ # Get tech profile from kv store
+ tech_profile = self._get_tech_profile_from_kv_store(table_id)
+ path = self.get_tp_path(table_id, uni_port_name)
+
+ if tech_profile is not None:
+ tech_profile = self._get_tech_profile(tech_profile)
+ log.debug(
+ "Created-tech-profile-instance-with-values-from-kvstore")
+ else:
+ tech_profile = self._default_tech_profile()
+ log.debug(
+ "Created-tech-profile-instance-with-default-values")
+
+ tech_profile_instance = TechProfileInstance(
+ uni_port_name, tech_profile, self.resource_mgr, intf_id)
+ self._add_tech_profile_instance(path,
+ tech_profile_instance.to_json())
+ except Exception as e:
+ log.exception("Create-tech-profile-instance-failed", exception=e)
+
+ return tech_profile_instance
+
+ def get_tech_profile_instance(self, table_id, uni_port_name):
+ # path to fetch tech profile instance json from kv store
+ path = TechProfile.TECH_PROFILE_INSTANCE_PATH.format(
+ self.resource_mgr.technology, table_id, uni_port_name)
+
+ try:
+ tech_profile_instance = self._kv_store[path]
+ log.debug("Tech-profile-instance-present-in-kvstore", path=path,
+ tech_profile_instance=tech_profile_instance)
+
+ # Parse JSON into an object with attributes corresponding to dict keys.
+ tech_profile_instance = json.loads(tech_profile_instance,
+ object_hook=lambda d:
+ namedtuple('tech_profile_instance',
+ d.keys())(*d.values()))
+ log.debug("Tech-profile-instance-after-json-to-object-conversion", path=path,
+ tech_profile_instance=tech_profile_instance)
+ return tech_profile_instance
+ except BaseException as e:
+ log.debug("Tech-profile-instance-not-present-in-kvstore",
+ path=path, tech_profile_instance=None, exception=e)
+ return None
+
+ def delete_tech_profile_instance(self, tp_path):
+
+ try:
+ del self._kv_store[tp_path]
+ log.debug("Delete-tech-profile-instance-success", path=tp_path)
+ return True
+ except Exception as e:
+ log.debug("Delete-tech-profile-instance-failed", path=tp_path,
+ exception=e)
+ return False
+
+ def _get_tech_profile_from_kv_store(self, table_id):
+ """
+ Get tech profile from kv store.
+
+ :param table_id: reference to get tech profile
+ :return: tech profile if present in kv store else None
+ """
+ # get tech profile from kv store
+ path = TechProfile.TECH_PROFILE_PATH.format(self.resource_mgr.technology,
+ table_id)
+ try:
+ tech_profile = self._kv_store[path]
+ if tech_profile != '':
+ log.debug("Get-tech-profile-success", tech_profile=tech_profile)
+ return json.loads(tech_profile)
+ # return ast.literal_eval(tech_profile)
+ except KeyError as e:
+ log.info("Get-tech-profile-failed", exception=e)
+ return None
+
+ def _default_tech_profile(self):
+ # Default tech profile
+ upstream_gem_port_attribute_list = list()
+ downstream_gem_port_attribute_list = list()
+ for pbit in TechProfile.pbits:
+ upstream_gem_port_attribute_list.append(
+ GemPortAttribute(pbit_map=pbit,
+ discard_config=DiscardConfig()))
+ downstream_gem_port_attribute_list.append(
+ GemPortAttribute(pbit_map=pbit,
+ discard_config=DiscardConfig()))
+
+ return TechProfile.DefaultTechProfile(
+ TechProfile.DEFAULT_TECH_PROFILE_NAME,
+ profile_type=self.resource_mgr.technology,
+ version=TechProfile.DEFAULT_VERSION,
+ num_gem_ports=TechProfile.DEFAULT_GEMPORTS_COUNT,
+ instance_control=InstanceControl(),
+ us_scheduler=Scheduler(direction=Direction.UPSTREAM.name),
+ ds_scheduler=Scheduler(direction=Direction.DOWNSTREAM.name),
+ upstream_gem_port_attribute_list=upstream_gem_port_attribute_list,
+ downstream_gem_port_attribute_list=
+ downstream_gem_port_attribute_list)
+
+ @staticmethod
+ def _get_tech_profile(tech_profile):
+ # Tech profile fetched from kv store
+ instance_control = tech_profile[TechProfile.INSTANCE_CONTROL]
+ instance_control = InstanceControl(
+ onu=instance_control[TechProfile.ONU],
+ uni=instance_control[TechProfile.UNI],
+ max_gem_payload_size=instance_control[
+ TechProfile.MAX_GEM_PAYLOAD_SIZE])
+
+ us_scheduler = tech_profile[TechProfile.US_SCHEDULER]
+ us_scheduler = Scheduler(direction=us_scheduler[TechProfile.DIRECTION],
+ additional_bw=us_scheduler[
+ TechProfile.ADDITIONAL_BW],
+ priority=us_scheduler[TechProfile.PRIORITY],
+ weight=us_scheduler[TechProfile.WEIGHT],
+ q_sched_policy=us_scheduler[
+ TechProfile.Q_SCHED_POLICY])
+ ds_scheduler = tech_profile[TechProfile.DS_SCHEDULER]
+ ds_scheduler = Scheduler(direction=ds_scheduler[TechProfile.DIRECTION],
+ additional_bw=ds_scheduler[
+ TechProfile.ADDITIONAL_BW],
+ priority=ds_scheduler[TechProfile.PRIORITY],
+ weight=ds_scheduler[TechProfile.WEIGHT],
+ q_sched_policy=ds_scheduler[
+ TechProfile.Q_SCHED_POLICY])
+
+ upstream_gem_port_attribute_list = list()
+ downstream_gem_port_attribute_list = list()
+ us_gemport_attr_list = tech_profile[
+ TechProfile.UPSTREAM_GEM_PORT_ATTRIBUTE_LIST]
+ for i in range(len(us_gemport_attr_list)):
+ upstream_gem_port_attribute_list.append(
+ GemPortAttribute(pbit_map=us_gemport_attr_list[i][TechProfile.PBIT_MAP],
+ discard_config=DiscardConfig(
+ max_threshold=
+ us_gemport_attr_list[i][TechProfile.DISCARD_CONFIG][
+ TechProfile.MAX_THRESHOLD],
+ min_threshold=
+ us_gemport_attr_list[i][TechProfile.DISCARD_CONFIG][
+ TechProfile.MIN_THRESHOLD],
+ max_probability=
+ us_gemport_attr_list[i][TechProfile.DISCARD_CONFIG][
+ TechProfile.MAX_PROBABILITY]),
+ discard_policy=us_gemport_attr_list[i][
+ TechProfile.DISCARD_POLICY],
+ priority_q=us_gemport_attr_list[i][
+ TechProfile.PRIORITY_Q],
+ weight=us_gemport_attr_list[i][TechProfile.WEIGHT],
+ scheduling_policy=us_gemport_attr_list[i][
+ TechProfile.SCHEDULING_POLICY],
+ max_q_size=us_gemport_attr_list[i][
+ TechProfile.MAX_Q_SIZE],
+ aes_encryption=us_gemport_attr_list[i][
+ TechProfile.AES_ENCRYPTION]))
+
+ ds_gemport_attr_list = tech_profile[
+ TechProfile.DOWNSTREAM_GEM_PORT_ATTRIBUTE_LIST]
+ for i in range(len(ds_gemport_attr_list)):
+ downstream_gem_port_attribute_list.append(
+ GemPortAttribute(pbit_map=ds_gemport_attr_list[i][TechProfile.PBIT_MAP],
+ discard_config=DiscardConfig(
+ max_threshold=
+ ds_gemport_attr_list[i][TechProfile.DISCARD_CONFIG][
+ TechProfile.MAX_THRESHOLD],
+ min_threshold=
+ ds_gemport_attr_list[i][TechProfile.DISCARD_CONFIG][
+ TechProfile.MIN_THRESHOLD],
+ max_probability=
+ ds_gemport_attr_list[i][TechProfile.DISCARD_CONFIG][
+ TechProfile.MAX_PROBABILITY]),
+ discard_policy=ds_gemport_attr_list[i][
+ TechProfile.DISCARD_POLICY],
+ priority_q=ds_gemport_attr_list[i][
+ TechProfile.PRIORITY_Q],
+ weight=ds_gemport_attr_list[i][TechProfile.WEIGHT],
+ scheduling_policy=ds_gemport_attr_list[i][
+ TechProfile.SCHEDULING_POLICY],
+ max_q_size=ds_gemport_attr_list[i][
+ TechProfile.MAX_Q_SIZE],
+ aes_encryption=ds_gemport_attr_list[i][
+ TechProfile.AES_ENCRYPTION]))
+
+ return TechProfile.DefaultTechProfile(
+ tech_profile[TechProfile.NAME],
+ profile_type=tech_profile[TechProfile.PROFILE_TYPE],
+ version=tech_profile[TechProfile.VERSION],
+ num_gem_ports=tech_profile[TechProfile.NUM_GEM_PORTS],
+ instance_control=instance_control,
+ us_scheduler=us_scheduler,
+ ds_scheduler=ds_scheduler,
+ upstream_gem_port_attribute_list=upstream_gem_port_attribute_list,
+ downstream_gem_port_attribute_list=
+ downstream_gem_port_attribute_list)
+
+ def _add_tech_profile_instance(self, path, tech_profile_instance):
+ """
+ Add tech profile to kv store.
+
+ :param path: path to add tech profile
+ :param tech_profile_instance: tech profile instance need to be added
+ """
+ try:
+ self._kv_store[path] = str(tech_profile_instance)
+ log.debug("Add-tech-profile-instance-success", path=path,
+ tech_profile_instance=tech_profile_instance)
+ return True
+ except BaseException as e:
+ log.exception("Add-tech-profile-instance-failed", path=path,
+ tech_profile_instance=tech_profile_instance,
+ exception=e)
+ return False
+
+ @staticmethod
+ def get_us_scheduler(tech_profile_instance):
+ # upstream scheduler
+ us_scheduler = openolt_pb2.Scheduler(
+ direction=TechProfile.get_parameter(
+ 'direction', tech_profile_instance.us_scheduler.
+ direction),
+ additional_bw=TechProfile.get_parameter(
+ 'additional_bw', tech_profile_instance.
+ us_scheduler.additional_bw),
+ priority=tech_profile_instance.us_scheduler.priority,
+ weight=tech_profile_instance.us_scheduler.weight,
+ sched_policy=TechProfile.get_parameter(
+ 'sched_policy', tech_profile_instance.
+ us_scheduler.q_sched_policy))
+
+ return us_scheduler
+
+ @staticmethod
+ def get_ds_scheduler(tech_profile_instance):
+ ds_scheduler = openolt_pb2.Scheduler(
+ direction=TechProfile.get_parameter(
+ 'direction', tech_profile_instance.ds_scheduler.
+ direction),
+ additional_bw=TechProfile.get_parameter(
+ 'additional_bw', tech_profile_instance.
+ ds_scheduler.additional_bw),
+ priority=tech_profile_instance.ds_scheduler.priority,
+ weight=tech_profile_instance.ds_scheduler.weight,
+ sched_policy=TechProfile.get_parameter(
+ 'sched_policy', tech_profile_instance.ds_scheduler.
+ q_sched_policy))
+
+ return ds_scheduler
+
+ @staticmethod
+ def get_tconts(tech_profile_instance, us_scheduler=None, ds_scheduler=None):
+ if us_scheduler is None:
+ us_scheduler = TechProfile.get_us_scheduler(tech_profile_instance)
+ if ds_scheduler is None:
+ ds_scheduler = TechProfile.get_ds_scheduler(tech_profile_instance)
+
+ tconts = [openolt_pb2.Tcont(direction=TechProfile.get_parameter(
+ 'direction',
+ tech_profile_instance.
+ us_scheduler.direction),
+ alloc_id=tech_profile_instance.
+ us_scheduler.alloc_id,
+ scheduler=us_scheduler),
+ openolt_pb2.Tcont(direction=TechProfile.get_parameter(
+ 'direction',
+ tech_profile_instance.
+ ds_scheduler.direction),
+ alloc_id=tech_profile_instance.
+ ds_scheduler.alloc_id,
+ scheduler=ds_scheduler)]
+
+ return tconts
+
+ @staticmethod
+ def get_parameter(param_type, param_value):
+ parameter = None
+ try:
+ if param_type == 'direction':
+ for direction in openolt_pb2.Direction.keys():
+ if param_value == direction:
+ parameter = direction
+ elif param_type == 'discard_policy':
+ for discard_policy in openolt_pb2.DiscardPolicy.keys():
+ if param_value == discard_policy:
+ parameter = discard_policy
+ elif param_type == 'sched_policy':
+ for sched_policy in openolt_pb2.SchedulingPolicy.keys():
+ if param_value == sched_policy:
+ parameter = sched_policy
+ elif param_type == 'additional_bw':
+ for bw_component in openolt_pb2.AdditionalBW.keys():
+ if param_value == bw_component:
+ parameter = bw_component
+ except BaseException as e:
+ log.exception(exception=e)
+ return parameter
+
+
+class TechProfileInstance(object):
+ def __init__(self, subscriber_identifier, tech_profile, resource_mgr,
+ intf_id, num_of_tconts=1):
+ if tech_profile is not None:
+ self.subscriber_identifier = subscriber_identifier
+ self.num_of_tconts = num_of_tconts
+ self.num_of_gem_ports = tech_profile.num_gem_ports
+ self.name = tech_profile.name
+ self.profile_type = tech_profile.profile_type
+ self.version = tech_profile.version
+ self.instance_control = tech_profile.instance_control
+
+ # TODO: Fixed num_of_tconts to 1 per TP Instance.
+ # This may change in future
+ assert (num_of_tconts == 1)
+ # Get alloc id and gemport id using resource manager
+ alloc_id = resource_mgr.get_resource_id(intf_id,
+ 'ALLOC_ID',
+ num_of_tconts)
+ gem_ports = resource_mgr.get_resource_id(intf_id,
+ 'GEMPORT_ID',
+ self.num_of_gem_ports)
+
+ gemport_list = list()
+ if isinstance(gem_ports, int):
+ gemport_list.append(gem_ports)
+ elif isinstance(gem_ports, list):
+ for gem in gem_ports:
+ gemport_list.append(gem)
+ else:
+ raise Exception("invalid-type")
+
+ self.us_scheduler = TechProfileInstance.IScheduler(
+ alloc_id, tech_profile.us_scheduler)
+ self.ds_scheduler = TechProfileInstance.IScheduler(
+ alloc_id, tech_profile.ds_scheduler)
+
+ self.upstream_gem_port_attribute_list = list()
+ self.downstream_gem_port_attribute_list = list()
+ for i in range(self.num_of_gem_ports):
+ self.upstream_gem_port_attribute_list.append(
+ TechProfileInstance.IGemPortAttribute(
+ gemport_list[i],
+ tech_profile.upstream_gem_port_attribute_list[
+ i]))
+ self.downstream_gem_port_attribute_list.append(
+ TechProfileInstance.IGemPortAttribute(
+ gemport_list[i],
+ tech_profile.downstream_gem_port_attribute_list[
+ i]))
+
+ class IScheduler(Scheduler):
+ def __init__(self, alloc_id, scheduler):
+ super(TechProfileInstance.IScheduler, self).__init__(
+ scheduler.direction, scheduler.additional_bw,
+ scheduler.priority,
+ scheduler.weight, scheduler.q_sched_policy)
+ self.alloc_id = alloc_id
+
+ class IGemPortAttribute(GemPortAttribute):
+ def __init__(self, gemport_id, gem_port_attribute):
+ super(TechProfileInstance.IGemPortAttribute, self).__init__(
+ gem_port_attribute.pbit_map, gem_port_attribute.discard_config,
+ gem_port_attribute.aes_encryption,
+ gem_port_attribute.scheduling_policy,
+ gem_port_attribute.priority_q, gem_port_attribute.weight,
+ gem_port_attribute.max_q_size,
+ gem_port_attribute.discard_policy)
+ self.gemport_id = gemport_id
+
+ def to_json(self):
+ return json.dumps(self, default=lambda o: o.__dict__,
+ indent=4)
diff --git a/python/common/utils/__init__.py b/python/common/utils/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/common/utils/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/common/utils/asleep.py b/python/common/utils/asleep.py
new file mode 100644
index 0000000..10d1ce3
--- /dev/null
+++ b/python/common/utils/asleep.py
@@ -0,0 +1,31 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+""" Async sleep (asleep) method and other twisted goodies """
+
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred
+
+
+def asleep(dt):
+ """
+ Async (event driven) wait for given time period (in seconds)
+ :param dt: Delay in seconds
+ :return: Deferred to be fired with value None when time expires.
+ """
+ d = Deferred()
+ reactor.callLater(dt, lambda: d.callback(None))
+ return d
diff --git a/python/common/utils/consulhelpers.py b/python/common/utils/consulhelpers.py
new file mode 100644
index 0000000..853143b
--- /dev/null
+++ b/python/common/utils/consulhelpers.py
@@ -0,0 +1,178 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Some consul related convenience functions
+"""
+
+from structlog import get_logger
+from consul import Consul
+from random import randint
+from nethelpers import get_my_primary_local_ipv4
+
+log = get_logger()
+
+
+def connect_to_consult(consul_endpoint):
+ log.debug('getting-service-endpoint', consul=consul_endpoint)
+
+ host = consul_endpoint.split(':')[0].strip()
+ port = int(consul_endpoint.split(':')[1].strip())
+
+ return Consul(host=host, port=port)
+
+
+def verify_all_services_healthy(consul_endpoint, service_name=None,
+ number_of_expected_services=None):
+ """
+ Verify in consul if any service is healthy
+ :param consul_endpoint: a <host>:<port> string
+ :param service_name: name of service to check, optional
+ :param number_of_expected_services number of services to check for, optional
+ :return: true if healthy, false otherwise
+ """
+
+ def check_health(service):
+ _, serv_health = consul.health.service(service, passing=True)
+ return not serv_health == []
+
+ consul = connect_to_consult(consul_endpoint)
+
+ if service_name is not None:
+ return check_health(service_name)
+
+ services = get_all_services(consul_endpoint)
+
+ items = services.keys()
+
+ if number_of_expected_services is not None and \
+ len(items) != number_of_expected_services:
+ return False
+
+ for item in items:
+ if not check_health(item):
+ return False
+
+ return True
+
+
+def get_all_services(consul_endpoint):
+ log.debug('getting-service-verify-health')
+
+ consul = connect_to_consult(consul_endpoint)
+ _, services = consul.catalog.services()
+
+ return services
+
+
+def get_all_instances_of_service(consul_endpoint, service_name):
+ log.debug('getting-all-instances-of-service', service=service_name)
+
+ consul = connect_to_consult(consul_endpoint)
+ _, services = consul.catalog.service(service_name)
+
+ for service in services:
+ log.debug('service',
+ name=service['ServiceName'],
+ serviceid=service['ServiceID'],
+ serviceport=service['ServicePort'],
+ createindex=service['CreateIndex'])
+
+ return services
+
+
+def get_endpoint_from_consul(consul_endpoint, service_name):
+ """
+ Get endpoint of service_name from consul.
+ :param consul_endpoint: a <host>:<port> string
+ :param service_name: name of service for which endpoint
+ needs to be found.
+ :return: service endpoint if available, else exit.
+ """
+ log.debug('getting-service-info', service=service_name)
+
+ consul = connect_to_consult(consul_endpoint)
+ _, services = consul.catalog.service(service_name)
+
+ if len(services) == 0:
+ raise Exception(
+ 'Cannot find service {} in consul'.format(service_name))
+ os.exit(1)
+
+ """ Get host IPV4 address
+ """
+ local_ipv4 = get_my_primary_local_ipv4()
+ """ If host IP address from where the request came in matches
+ the IP address of the requested service's host IP address,
+ pick the endpoint
+ """
+ for i in range(len(services)):
+ service = services[i]
+ if service['ServiceAddress'] == local_ipv4:
+ log.debug("picking address locally")
+ endpoint = '{}:{}'.format(service['ServiceAddress'],
+ service['ServicePort'])
+ return endpoint
+
+ """ If service is not available locally, picak a random
+ endpoint for the service from the list
+ """
+ service = services[randint(0, len(services) - 1)]
+ endpoint = '{}:{}'.format(service['ServiceAddress'],
+ service['ServicePort'])
+
+ return endpoint
+
+
+def get_healthy_instances(consul_endpoint, service_name=None,
+ number_of_expected_services=None):
+ """
+ Verify in consul if any service is healthy
+ :param consul_endpoint: a <host>:<port> string
+ :param service_name: name of service to check, optional
+ :param number_of_expected_services number of services to check for, optional
+ :return: true if healthy, false otherwise
+ """
+
+ def check_health(service):
+ _, serv_health = consul.health.service(service, passing=True)
+ return not serv_health == []
+
+ consul = connect_to_consult(consul_endpoint)
+
+ if service_name is not None:
+ return check_health(service_name)
+
+ services = get_all_services(consul_endpoint)
+
+ items = services.keys()
+
+ if number_of_expected_services is not None and \
+ len(items) != number_of_expected_services:
+ return False
+
+ for item in items:
+ if not check_health(item):
+ return False
+
+ return True
+
+
+if __name__ == '__main__':
+ # print get_endpoint_from_consul('10.100.198.220:8500', 'kafka')
+ # print get_healthy_instances('10.100.198.220:8500', 'voltha-health')
+ # print get_healthy_instances('10.100.198.220:8500')
+ get_all_instances_of_service('10.100.198.220:8500', 'voltha-grpc')
diff --git a/python/common/utils/deferred_utils.py b/python/common/utils/deferred_utils.py
new file mode 100644
index 0000000..3c55c1a
--- /dev/null
+++ b/python/common/utils/deferred_utils.py
@@ -0,0 +1,56 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred
+from twisted.internet.error import AlreadyCalled
+
+
+class TimeOutError(Exception): pass
+
+
+class DeferredWithTimeout(Deferred):
+ """
+ Deferred with a timeout. If neither the callback nor the errback method
+ is not called within the given time, the deferred's errback will be called
+ with a TimeOutError() exception.
+
+ All other uses are the same as of Deferred().
+ """
+ def __init__(self, timeout=1.0):
+ Deferred.__init__(self)
+ self._timeout = timeout
+ self.timer = reactor.callLater(timeout, self.timed_out)
+
+ def timed_out(self):
+ self.errback(
+ TimeOutError('timed out after {} seconds'.format(self._timeout)))
+
+ def callback(self, result):
+ self._cancel_timer()
+ return Deferred.callback(self, result)
+
+ def errback(self, fail):
+ self._cancel_timer()
+ return Deferred.errback(self, fail)
+
+ def cancel(self):
+ self._cancel_timer()
+ return Deferred.cancel(self)
+
+ def _cancel_timer(self):
+ try:
+ self.timer.cancel()
+ except AlreadyCalled:
+ pass
+
diff --git a/python/common/utils/dockerhelpers.py b/python/common/utils/dockerhelpers.py
new file mode 100644
index 0000000..4620aef
--- /dev/null
+++ b/python/common/utils/dockerhelpers.py
@@ -0,0 +1,75 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Some docker related convenience functions
+"""
+from datetime import datetime
+from concurrent.futures import ThreadPoolExecutor
+
+import os
+import socket
+from structlog import get_logger
+
+from docker import Client, errors
+
+
+docker_socket = os.environ.get('DOCKER_SOCK', 'unix://tmp/docker.sock')
+log = get_logger()
+
+def get_my_containers_name():
+ """
+ Return the docker containers name in which this process is running.
+ To look up the container name, we use the container ID extracted from the
+ $HOSTNAME environment variable (which is set by docker conventions).
+ :return: String with the docker container name (or None if any issue is
+ encountered)
+ """
+ my_container_id = os.environ.get('HOSTNAME', None)
+
+ try:
+ docker_cli = Client(base_url=docker_socket)
+ info = docker_cli.inspect_container(my_container_id)
+
+ except Exception, e:
+ log.exception('failed', my_container_id=my_container_id, e=e)
+ raise
+
+ name = info['Name'].lstrip('/')
+
+ return name
+
+def get_all_running_containers():
+ try:
+ docker_cli = Client(base_url=docker_socket)
+ containers = docker_cli.containers()
+
+ except Exception, e:
+ log.exception('failed', e=e)
+ raise
+
+ return containers
+
+def inspect_container(id):
+ try:
+ docker_cli = Client(base_url=docker_socket)
+ info = docker_cli.inspect_container(id)
+ except Exception, e:
+ log.exception('failed-inspect-container', id=id, e=e)
+ raise
+
+ return info
+
diff --git a/python/common/utils/grpc_utils.py b/python/common/utils/grpc_utils.py
new file mode 100644
index 0000000..8df630e
--- /dev/null
+++ b/python/common/utils/grpc_utils.py
@@ -0,0 +1,109 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Utilities to handle gRPC server and client side code in a Twisted environment
+"""
+import structlog
+from concurrent.futures import Future
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred
+from twisted.python.threadable import isInIOThread
+
+
+log = structlog.get_logger()
+
+
+def twisted_async(func):
+ """
+ This decorator can be used to implement a gRPC method on the twisted
+ thread, allowing asynchronous programming in Twisted while serving
+ a gRPC call.
+
+ gRPC methods normally are called on the futures.ThreadPool threads,
+ so these methods cannot directly use Twisted protocol constructs.
+ If the implementation of the methods needs to touch Twisted, it is
+ safer (or mandatory) to wrap the method with this decorator, which will
+ call the inner method from the external thread and ensure that the
+ result is passed back to the foreign thread.
+
+ Example usage:
+
+ When implementing a gRPC server, typical pattern is:
+
+ class SpamService(SpamServicer):
+
+ def GetBadSpam(self, request, context):
+ '''this is called from a ThreadPoolExecutor thread'''
+ # generally unsafe to make Twisted calls
+
+ @twisted_async
+ def GetSpamSafely(self, request, context):
+ '''this method now is executed on the Twisted main thread
+ # safe to call any Twisted protocol functions
+
+ @twisted_async
+ @inlineCallbacks
+ def GetAsyncSpam(self, request, context):
+ '''this generator can use inlineCallbacks Twisted style'''
+ result = yield some_async_twisted_call(request)
+ returnValue(result)
+
+ """
+ def in_thread_wrapper(*args, **kw):
+
+ if isInIOThread():
+
+ return func(*args, **kw)
+
+ f = Future()
+
+ def twisted_wrapper():
+ try:
+ d = func(*args, **kw)
+ if isinstance(d, Deferred):
+
+ def _done(result):
+ f.set_result(result)
+ f.done()
+
+ def _error(e):
+ f.set_exception(e)
+ f.done()
+
+ d.addCallback(_done)
+ d.addErrback(_error)
+
+ else:
+ f.set_result(d)
+ f.done()
+
+ except Exception, e:
+ f.set_exception(e)
+ f.done()
+
+ reactor.callFromThread(twisted_wrapper)
+ try:
+ result = f.result()
+ except Exception, e:
+ log.exception(e=e, func=func, args=args, kw=kw)
+ raise
+
+ return result
+
+ return in_thread_wrapper
+
+
diff --git a/python/common/utils/id_generation.py b/python/common/utils/id_generation.py
new file mode 100644
index 0000000..e0fea1c
--- /dev/null
+++ b/python/common/utils/id_generation.py
@@ -0,0 +1,116 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# """ ID generation utils """
+
+from uuid import uuid4
+
+
+BROADCAST_CORE_ID=hex(0xFFFF)[2:]
+
+def get_next_core_id(current_id_in_hex_str):
+ """
+ :param current_id_in_hex_str: a hex string of the maximum core id
+ assigned without the leading 0x characters
+ :return: current_id_in_hex_str + 1 in hex string
+ """
+ if not current_id_in_hex_str or current_id_in_hex_str == '':
+ return '0001'
+ else:
+ return format(int(current_id_in_hex_str, 16) + 1, '04x')
+
+
+def create_cluster_logical_device_ids(core_id, switch_id):
+ """
+ Creates a logical device id and an OpenFlow datapath id that is unique
+ across the Voltha cluster.
+ The returned logical device id represents a 64 bits integer where the
+ lower 48 bits is the switch id and the upper 16 bits is the core id. For
+ the datapath id the core id is set to '0000' as it is not used for voltha
+ core routing
+ :param core_id: string
+ :param switch_id:int
+ :return: cluster logical device id and OpenFlow datapath id
+ """
+ switch_id = format(switch_id, '012x')
+ core_in_hex=format(int(core_id, 16), '04x')
+ ld_id = '{}{}'.format(core_in_hex[-4:], switch_id[-12:])
+ dpid_id = '{}{}'.format('0000', switch_id[-12:])
+ return ld_id, int(dpid_id, 16)
+
+def is_broadcast_core_id(id):
+ assert id and len(id) == 16
+ return id[:4] == BROADCAST_CORE_ID
+
+def create_empty_broadcast_id():
+ """
+ Returns an empty broadcast id (ffff000000000000). The id is used to
+ dispatch xPON objects across all the Voltha instances.
+ :return: An empty broadcast id
+ """
+ return '{}{}'.format(BROADCAST_CORE_ID, '0'*12)
+
+def create_cluster_id():
+ """
+ Returns an id that is common across all voltha instances. The id
+ is a str of 64 bits. The lower 48 bits refers to an id specific to that
+ object while the upper 16 bits refers a broadcast core_id
+ :return: An common id across all Voltha instances
+ """
+ return '{}{}'.format(BROADCAST_CORE_ID, uuid4().hex[:12])
+
+def create_cluster_device_id(core_id):
+ """
+ Creates a device id that is unique across the Voltha cluster.
+ The device id is a str of 64 bits. The lower 48 bits refers to the
+ device id while the upper 16 bits refers to the core id.
+ :param core_id: string
+ :return: cluster device id
+ """
+ return '{}{}'.format(format(int(core_id), '04x'), uuid4().hex[:12])
+
+
+def get_core_id_from_device_id(device_id):
+ # Device id is a string and the first 4 characters represent the core_id
+ assert device_id and len(device_id) == 16
+ # Get the leading 4 hexs and remove leading 0's
+ return device_id[:4]
+
+
+def get_core_id_from_logical_device_id(logical_device_id):
+ """
+ Logical Device id is a string and the first 4 characters represent the
+ core_id
+ :param logical_device_id:
+ :return: core_id string
+ """
+ assert logical_device_id and len(logical_device_id) == 16
+ # Get the leading 4 hexs and remove leading 0's
+ return logical_device_id[:4]
+
+
+def get_core_id_from_datapath_id(datapath_id):
+ """
+ datapath id is a uint64 where:
+ - low 48 bits -> switch_id
+ - high 16 bits -> core id
+ :param datapath_id:
+ :return: core_id string
+ """
+ assert datapath_id
+ # Get the hex string and remove the '0x' prefix
+ id_in_hex_str = hex(datapath_id)[2:]
+ assert len(id_in_hex_str) > 12
+ return id_in_hex_str[:-12]
diff --git a/python/common/utils/indexpool.py b/python/common/utils/indexpool.py
new file mode 100644
index 0000000..858cb3a
--- /dev/null
+++ b/python/common/utils/indexpool.py
@@ -0,0 +1,64 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from bitstring import BitArray
+import structlog
+
+log = structlog.get_logger()
+
+class IndexPool(object):
+ def __init__(self, max_entries, offset):
+ self.max_entries = max_entries
+ self.offset = offset
+ self.indices = BitArray(self.max_entries)
+
+ def get_next(self):
+ try:
+ _pos = self.indices.find('0b0')
+ self.indices.set(1, _pos)
+ return self.offset + _pos[0]
+ except IndexError:
+ log.info("exception-fail-to-allocate-id-all-bits-in-use")
+ return None
+
+ def allocate(self, index):
+ try:
+ _pos = index - self.offset
+ if not (0 <= _pos < self.max_entries):
+ log.info("{}-out-of-range".format(index))
+ return None
+ if self.indices[_pos]:
+ log.info("{}-is-already-allocated".format(index))
+ return None
+ self.indices.set(1, _pos)
+ return index
+
+ except IndexError:
+ return None
+
+ def release(self, index):
+ index -= self.offset
+ _pos = (index,)
+ try:
+ self.indices.set(0, _pos)
+ except IndexError:
+ log.info("bit-position-{}-out-of-range".format(index))
+
+ #index or multiple indices to set all of them to 1 - need to be a tuple
+ def pre_allocate(self, index):
+ if(isinstance(index, tuple)):
+ _lst = list(index)
+ for i in range(len(_lst)):
+ _lst[i] -= self.offset
+ index = tuple(_lst)
+ self.indices.set(1, index)
diff --git a/python/common/utils/json_format.py b/python/common/utils/json_format.py
new file mode 100644
index 0000000..c18d013
--- /dev/null
+++ b/python/common/utils/json_format.py
@@ -0,0 +1,105 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Monkey patched json_format to allow best effort decoding of Any fields.
+Use the additional flag (strict_any_handling=False) to trigger the
+best-effort behavior. Omit the flag, or just use the original json_format
+module fot the strict behavior.
+"""
+
+from google.protobuf import json_format
+
+class _PatchedPrinter(json_format._Printer):
+
+ def __init__(self, including_default_value_fields=False,
+ preserving_proto_field_name=False,
+ strict_any_handling=False):
+ super(_PatchedPrinter, self).__init__(including_default_value_fields,
+ preserving_proto_field_name)
+ self.strict_any_handling = strict_any_handling
+
+ def _BestEffortAnyMessageToJsonObject(self, msg):
+ try:
+ res = self._AnyMessageToJsonObject(msg)
+ except TypeError:
+ res = self._RegularMessageToJsonObject(msg, {})
+ return res
+
+
+def MessageToDict(message,
+ including_default_value_fields=False,
+ preserving_proto_field_name=False,
+ strict_any_handling=False):
+ """Converts protobuf message to a JSON dictionary.
+
+ Args:
+ message: The protocol buffers message instance to serialize.
+ including_default_value_fields: If True, singular primitive fields,
+ repeated fields, and map fields will always be serialized. If
+ False, only serialize non-empty fields. Singular message fields
+ and oneof fields are not affected by this option.
+ preserving_proto_field_name: If True, use the original proto field
+ names as defined in the .proto file. If False, convert the field
+ names to lowerCamelCase.
+ strict_any_handling: If True, converion will error out (like in the
+ original method) if an Any field with value for which the Any type
+ is not loaded is encountered. If False, the conversion will leave
+ the field un-packed, but otherwise will continue.
+
+ Returns:
+ A dict representation of the JSON formatted protocol buffer message.
+ """
+ printer = _PatchedPrinter(including_default_value_fields,
+ preserving_proto_field_name,
+ strict_any_handling=strict_any_handling)
+ # pylint: disable=protected-access
+ return printer._MessageToJsonObject(message)
+
+
+def MessageToJson(message,
+ including_default_value_fields=False,
+ preserving_proto_field_name=False,
+ strict_any_handling=False):
+ """Converts protobuf message to JSON format.
+
+ Args:
+ message: The protocol buffers message instance to serialize.
+ including_default_value_fields: If True, singular primitive fields,
+ repeated fields, and map fields will always be serialized. If
+ False, only serialize non-empty fields. Singular message fields
+ and oneof fields are not affected by this option.
+ preserving_proto_field_name: If True, use the original proto field
+ names as defined in the .proto file. If False, convert the field
+ names to lowerCamelCase.
+ strict_any_handling: If True, converion will error out (like in the
+ original method) if an Any field with value for which the Any type
+ is not loaded is encountered. If False, the conversion will leave
+ the field un-packed, but otherwise will continue.
+
+ Returns:
+ A string containing the JSON formatted protocol buffer message.
+ """
+ printer = _PatchedPrinter(including_default_value_fields,
+ preserving_proto_field_name,
+ strict_any_handling=strict_any_handling)
+ return printer.ToJsonString(message)
+
+
+json_format._WKTJSONMETHODS['google.protobuf.Any'] = [
+ '_BestEffortAnyMessageToJsonObject',
+ '_ConvertAnyMessage'
+]
+
+json_format._Printer._BestEffortAnyMessageToJsonObject = \
+ json_format._Printer._AnyMessageToJsonObject
diff --git a/python/common/utils/message_queue.py b/python/common/utils/message_queue.py
new file mode 100644
index 0000000..2b4257a
--- /dev/null
+++ b/python/common/utils/message_queue.py
@@ -0,0 +1,89 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from twisted.internet.defer import Deferred
+from twisted.internet.defer import succeed
+
+
+class MessageQueue(object):
+ """
+ An event driven queue, similar to twisted.internet.defer.DeferredQueue
+ but which allows selective dequeing based on a predicate function.
+ Unlike DeferredQueue, there is no limit on backlog, and there is no queue
+ limit.
+ """
+
+ def __init__(self):
+ self.waiting = [] # tuples of (d, predicate)
+ self.queue = [] # messages piling up here if no one is waiting
+
+ def reset(self):
+ """
+ Purge all content as well as waiters (by errback-ing their entries).
+ :return: None
+ """
+ for d, _ in self.waiting:
+ d.errback(Exception('mesage queue reset() was called'))
+ self.waiting = []
+ self.queue = []
+
+ def _cancelGet(self, d):
+ """
+ Remove a deferred from our waiting list.
+ :param d: The deferred that was been canceled.
+ :return: None
+ """
+ for i in range(len(self.waiting)):
+ if self.waiting[i][0] is d:
+ self.waiting.pop(i)
+
+ def put(self, obj):
+ """
+ Add an object to this queue
+ :param obj: arbitrary object that will be added to the queue
+ :return:
+ """
+
+ # if someone is waiting for this, return right away
+ for i in range(len(self.waiting)):
+ d, predicate = self.waiting[i]
+ if predicate is None or predicate(obj):
+ self.waiting.pop(i)
+ d.callback(obj)
+ return
+
+ # otherwise...
+ self.queue.append(obj)
+
+ def get(self, predicate=None):
+ """
+ Attempt to retrieve and remove an object from the queue that
+ matches the optional predicate.
+ :return: Deferred which fires with the next object available.
+ If predicate was provided, only objects for which
+ predicate(obj) is True will be considered.
+ """
+ for i in range(len(self.queue)):
+ msg = self.queue[i]
+ if predicate is None or predicate(msg):
+ self.queue.pop(i)
+ return succeed(msg)
+
+ # there were no matching entries if we got here, so we wait
+ d = Deferred(canceller=self._cancelGet)
+ self.waiting.append((d, predicate))
+ return d
+
+
diff --git a/python/common/utils/nethelpers.py b/python/common/utils/nethelpers.py
new file mode 100644
index 0000000..7df7f9f
--- /dev/null
+++ b/python/common/utils/nethelpers.py
@@ -0,0 +1,88 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Some network related convenience functions
+"""
+
+from netifaces import AF_INET
+
+import netifaces as ni
+import netaddr
+
+
+def _get_all_interfaces():
+ m_interfaces = []
+ for iface in ni.interfaces():
+ m_interfaces.append((iface, ni.ifaddresses(iface)))
+ return m_interfaces
+
+
+def _get_my_primary_interface():
+ gateways = ni.gateways()
+ assert 'default' in gateways, \
+ ("No default gateway on host/container, "
+ "cannot determine primary interface")
+ default_gw_index = gateways['default'].keys()[0]
+ # gateways[default_gw_index] has the format (example):
+ # [('10.15.32.1', 'en0', True)]
+ interface_name = gateways[default_gw_index][0][1]
+ return interface_name
+
+
+def get_my_primary_local_ipv4(inter_core_subnet=None, ifname=None):
+ if not inter_core_subnet:
+ return _get_my_primary_local_ipv4(ifname)
+ # My IP should belong to the specified subnet
+ for iface in ni.interfaces():
+ addresses = ni.ifaddresses(iface)
+ if AF_INET in addresses:
+ m_ip = addresses[AF_INET][0]['addr']
+ _ip = netaddr.IPAddress(m_ip).value
+ m_network = netaddr.IPNetwork(inter_core_subnet)
+ if _ip >= m_network.first and _ip <= m_network.last:
+ return m_ip
+ return None
+
+
+def get_my_primary_interface(pon_subnet=None):
+ if not pon_subnet:
+ return _get_my_primary_interface()
+ # My interface should have an IP that belongs to the specified subnet
+ for iface in ni.interfaces():
+ addresses = ni.ifaddresses(iface)
+ if AF_INET in addresses:
+ m_ip = addresses[AF_INET][0]['addr']
+ m_ip = netaddr.IPAddress(m_ip).value
+ m_network = netaddr.IPNetwork(pon_subnet)
+ if m_ip >= m_network.first and m_ip <= m_network.last:
+ return iface
+ return None
+
+def mac_str_to_tuple(mac):
+ return tuple(int(d, 16) for d in mac.split(':'))
+
+def _get_my_primary_local_ipv4(ifname=None):
+ try:
+ ifname = get_my_primary_interface() if ifname is None else ifname
+ addresses = ni.ifaddresses(ifname)
+ ipv4 = addresses[AF_INET][0]['addr']
+ return ipv4
+ except Exception as e:
+ return None
+
+if __name__ == '__main__':
+ print get_my_primary_local_ipv4()
diff --git a/python/common/utils/ordered_weakvalue_dict.py b/python/common/utils/ordered_weakvalue_dict.py
new file mode 100644
index 0000000..9ea739a
--- /dev/null
+++ b/python/common/utils/ordered_weakvalue_dict.py
@@ -0,0 +1,48 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from _weakref import ref
+from weakref import KeyedRef
+from collections import OrderedDict
+
+
+class OrderedWeakValueDict(OrderedDict):
+ """
+ Modified OrderedDict to use weak references as values. Entries disappear
+ automatically if the referred value has no more strong reference pointing
+ ot it.
+
+ Warning, this is not a complete implementation, only what is needed for
+ now. See test_ordered_wealvalue_dict.py to see what is tested behavior.
+ """
+ def __init__(self, *args, **kw):
+ def remove(wr, selfref=ref(self)):
+ self = selfref()
+ if self is not None:
+ super(OrderedWeakValueDict, self).__delitem__(wr.key)
+ self._remove = remove
+ super(OrderedWeakValueDict, self).__init__(*args, **kw)
+
+ def __setitem__(self, key, value):
+ super(OrderedWeakValueDict, self).__setitem__(
+ key, KeyedRef(value, self._remove, key))
+
+ def __getitem__(self, key):
+ o = super(OrderedWeakValueDict, self).__getitem__(key)()
+ if o is None:
+ raise KeyError, key
+ else:
+ return o
+
diff --git a/python/common/utils/registry.py b/python/common/utils/registry.py
new file mode 100644
index 0000000..270bd71
--- /dev/null
+++ b/python/common/utils/registry.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Simple component registry to provide centralized access to any registered
+components.
+"""
+from collections import OrderedDict
+from zope.interface import Interface
+
+
+class IComponent(Interface):
+ """
+ A Voltha Component
+ """
+
+ def start():
+ """
+ Called once the componet is instantiated. Can be used for async
+ initialization.
+ :return: (None or Deferred)
+ """
+
+ def stop():
+ """
+ Called once before the component is unloaded. Can be used for async
+ cleanup operations.
+ :return: (None or Deferred)
+ """
+
+
+class Registry(object):
+
+ def __init__(self):
+ self.components = OrderedDict()
+
+ def register(self, name, component):
+ assert IComponent.providedBy(component)
+ assert name not in self.components
+ self.components[name] = component
+ return component
+
+ def unregister(self, name):
+ if name in self.components:
+ del self.components[name]
+
+ def __call__(self, name):
+ return self.components[name]
+
+ def iterate(self):
+ return self.components.values()
+
+
+# public shared registry
+registry = Registry()
diff --git a/python/compose/adapters-openolt.yml b/python/compose/adapters-openolt.yml
new file mode 100644
index 0000000..da3b0c3
--- /dev/null
+++ b/python/compose/adapters-openolt.yml
@@ -0,0 +1,38 @@
+---
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+version: '2'
+services:
+ adapter_openolt:
+ image: "${REGISTRY}${REPOSITORY}voltha-adapter-openolt${TAG}"
+ logging:
+ driver: "json-file"
+ options:
+ max-size: "10m"
+ max-file: "3"
+ command: [
+ "/voltha/python/adapters/openolt/main.py",
+ "-v",
+ "--name=openolt",
+ "--kafka_adapter=${DOCKER_HOST_IP}:9092",
+ "--kafka_cluster=${DOCKER_HOST_IP}:9092",
+ "--core_topic=rwcore"
+ ]
+ networks:
+ - default
+#Add brcm_openomci_onu here if needed
+networks:
+ default:
+ driver: bridge
diff --git a/python/docker/Dockerfile.adapter_openolt b/python/docker/Dockerfile.adapter_openolt
new file mode 100644
index 0000000..c1fc607
--- /dev/null
+++ b/python/docker/Dockerfile.adapter_openolt
@@ -0,0 +1,44 @@
+# Copyright 2016 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+ARG TAG=latest
+ARG REGISTRY=
+ARG REPOSITORY=
+
+FROM ${REGISTRY}${REPOSITORY}voltha-protos:${TAG} as protos
+FROM ${REGISTRY}${REPOSITORY}voltha-openolt-protos:${TAG} as openolt_protos
+FROM ${REGISTRY}${REPOSITORY}voltha-base:${TAG}
+
+MAINTAINER Voltha Community <info@opennetworking.org>
+
+# Bundle app source
+RUN mkdir /voltha && touch /voltha/__init__.py
+ENV PYTHONPATH=/voltha
+COPY common /voltha/python/common/
+COPY adapters/common /voltha/python/adapters/common/
+COPY adapters/kafka /voltha/python/adapters/kafka
+COPY adapters/*.py /voltha/python/adapters/
+COPY adapters/openolt /voltha/python/adapters/openolt
+RUN touch /voltha/python/__init__.py
+RUN touch /voltha/python/adapters/__init__.py
+
+# Copy in the generated GRPC proto code
+COPY --from=protos /protos/voltha /voltha/python/protos
+COPY --from=protos /protos/google/api /voltha/python/protos/third_party/google/api
+COPY --from=openolt_protos /protos/voltha /voltha/python/protos
+COPY protos/third_party/__init__.py /voltha/python/protos/third_party
+RUN touch /voltha/python/protos/__init__.py
+RUN touch /voltha/python/protos/third_party/google/__init__.py
+
+# Exposing process and default entry point
+# CMD ["python", "/voltha/python/adapters/openolt/main.py"]
diff --git a/python/docker/Dockerfile.openolt_protos b/python/docker/Dockerfile.openolt_protos
new file mode 100644
index 0000000..44afb5f
--- /dev/null
+++ b/python/docker/Dockerfile.openolt_protos
@@ -0,0 +1,39 @@
+# Copyright 2018 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ARG REGISTRY=
+ARG REPOSITORY=
+ARG TAG=latest
+
+FROM ${REGISTRY}${REPOSITORY}voltha-protos:${TAG} as protos
+FROM ${REGISTRY}${REPOSITORY}voltha-protoc:${TAG} as builder
+
+MAINTAINER Voltha Community <info@opennetworking.org>
+
+COPY protos/third_party/google/api/*.proto /protos/google/api/
+COPY docker/config/Makefile.protos /protos/google/api/Makefile.protos
+COPY protos/*.proto /protos/voltha/
+COPY --from=protos /protos/voltha/ /protos/voltha/
+COPY docker/config/Makefile.protos /protos/voltha/Makefile.protos
+
+WORKDIR /protos
+RUN make -f google/api/Makefile.protos google_api
+RUN touch /protos/google/__init__.py /protos/google/api/__init__.py
+
+WORKDIR /protos/voltha
+RUN make -f Makefile.protos build
+
+# Copy the files to a scrach based container to minimize its size
+FROM ${REGISTRY}scratch
+COPY --from=builder /protos/ /protos/
diff --git a/python/docker/config/Makefile.protos b/python/docker/config/Makefile.protos
new file mode 100644
index 0000000..12ff9e3
--- /dev/null
+++ b/python/docker/config/Makefile.protos
@@ -0,0 +1,59 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Makefile to build all protobuf and gRPC related artifacts
+
+default: build
+
+PROTO_FILES := $(wildcard *.proto)
+PROTO_ALL_FILES := $(PROTO_FILES) $(PROTO_GOOGLE_API)
+PROTO_PB2_FILES := $(foreach f,$(PROTO_FILES),$(subst .proto,_pb2.py,$(f)))
+PROTO_PB2_GOOGLE_API := $(foreach f,$(PROTO_GOOGLE_API),$(subst .proto,_pb2.py,$(f)))
+PROTO_All_PB2_C_FILES := $(foreach f,$(PROTO_ALL_FILES),$(subst .proto,_pb2.pyc,$(f)))
+PROTO_ALL_PB2_GPRC_FILES := $(foreach f,$(PROTO_ALL_FILES),$(subst .proto,_pb2_grpc.py,$(f)))
+PROTO_ALL_DESC_FILES := $(foreach f,$(PROTO_ALL_FILES),$(subst .proto,.desc,$(f)))
+
+# Google API needs to be built from within the third party directory
+#
+google_api:
+ python -m grpc.tools.protoc \
+ -I. \
+ --python_out=. \
+ --grpc_python_out=. \
+ --descriptor_set_out=google/api/annotations.desc \
+ --include_imports \
+ --include_source_info \
+ google/api/annotations.proto google/api/http.proto
+
+build: $(PROTO_PB2_FILES)
+
+%_pb2.py: %.proto
+ python -m grpc.tools.protoc \
+ -I. \
+ -I/protos \
+ --python_out=. \
+ --grpc_python_out=. \
+ --descriptor_set_out=$(basename $<).desc \
+ --include_imports \
+ --include_source_info \
+ $<
+
+clean:
+ rm -f $(PROTO_PB2_FILES) \
+ $(PROTO_ALL_DESC_FILES) \
+ $(PROTO_ALL_PB2_GPRC_FILES) \
+ $(PROTO_All_PB2_C_FILES) \
+ $(PROTO_PB2_GOOGLE_API)
diff --git a/python/env.sh b/python/env.sh
new file mode 100644
index 0000000..433b975
--- /dev/null
+++ b/python/env.sh
@@ -0,0 +1,29 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# sourcing this file is needed to make local development and integration testing work
+export VOLTHA_BASE=$PWD
+
+# load local python virtualenv if exists, otherwise create it
+VENVDIR="venv-$(uname -s | tr '[:upper:]' '[:lower:]')"
+if [ ! -e "$VENVDIR/.built" ]; then
+ echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
+ echo "Initializing OS-appropriate virtual env."
+ echo "This will take a few minutes."
+ echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
+ make venv
+fi
+. $VENVDIR/bin/activate
+
+ #add top-level voltha dir to pythonpath
+export PYTHONPATH=$VOLTHA_BASE/$VENVDIR/lib/python2.7/site-packages:$PYTHONPATH:$VOLTHA_BASE:$VOLTHA_BASE/cli:$VOLTHA_BASE/protos/third_party
diff --git a/python/protos/openolt.proto b/python/protos/openolt.proto
new file mode 100644
index 0000000..f6c1da1
--- /dev/null
+++ b/python/protos/openolt.proto
@@ -0,0 +1,453 @@
+// Copyright (c) 2018 Open Networking Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at:
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+option go_package = "github.com/opencord/voltha-go/protos/voltha";
+
+package voltha;
+
+import "google/api/annotations.proto";
+import "openflow_13.proto";
+
+
+
+service Openolt {
+
+ rpc DisableOlt(Empty) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/Disable"
+ body: "*"
+ };
+ }
+
+ rpc ReenableOlt(Empty) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/Reenable"
+ body: "*"
+ };
+ }
+
+ rpc ActivateOnu(Onu) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/EnableOnu"
+ body: "*"
+ };
+ }
+
+ rpc DeactivateOnu(Onu) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/DisableOnu"
+ body: "*"
+ };
+ }
+
+ rpc DeleteOnu(Onu) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/DeleteOnu"
+ body: "*"
+ };
+ }
+
+ rpc OmciMsgOut(OmciMsg) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/OmciMsgOut"
+ body: "*"
+ };
+ }
+
+ rpc OnuPacketOut(OnuPacket) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/OnuPacketOut"
+ body: "*"
+ };
+ }
+
+ rpc UplinkPacketOut(UplinkPacket) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/UplinkPacketOut"
+ body: "*"
+ };
+ }
+
+ rpc FlowAdd(Flow) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/FlowAdd"
+ body: "*"
+ };
+ }
+
+ rpc FlowRemove(Flow) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/FlowRemove"
+ body: "*"
+ };
+ }
+
+ rpc HeartbeatCheck(Empty) returns (Heartbeat) {
+ option (google.api.http) = {
+ post: "/v1/HeartbeatCheck"
+ body: "*"
+ };
+ }
+
+ rpc EnablePonIf(Interface) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/EnablePonIf"
+ body: "*"
+ };
+ }
+
+ rpc DisablePonIf(Interface) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/DisablePonIf"
+ body: "*"
+ };
+ }
+
+ rpc GetDeviceInfo(Empty) returns (DeviceInfo) {
+ option (google.api.http) = {
+ post: "/v1/GetDeviceInfo"
+ body: "*"
+ };
+ }
+
+ rpc Reboot(Empty) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/Reboot"
+ body: "*"
+ };
+ }
+
+ rpc CollectStatistics(Empty) returns (Empty) {
+ option (google.api.http) = {
+ post: "/v1/CollectStatistics"
+ body: "*"
+ };
+ }
+
+ rpc EnableIndication(Empty) returns (stream Indication) {}
+}
+
+message Indication {
+ oneof data {
+ OltIndication olt_ind = 1;
+ IntfIndication intf_ind = 2;
+ IntfOperIndication intf_oper_ind = 3;
+ OnuDiscIndication onu_disc_ind = 4;
+ OnuIndication onu_ind = 5;
+ OmciIndication omci_ind = 6;
+ PacketIndication pkt_ind = 7;
+ PortStatistics port_stats = 8;
+ FlowStatistics flow_stats = 9;
+ AlarmIndication alarm_ind= 10;
+ }
+}
+
+message AlarmIndication {
+ oneof data {
+ LosIndication los_ind = 1;
+ DyingGaspIndication dying_gasp_ind = 2;
+ OnuAlarmIndication onu_alarm_ind = 3;
+ OnuStartupFailureIndication onu_startup_fail_ind = 4;
+ OnuSignalDegradeIndication onu_signal_degrade_ind = 5;
+ OnuDriftOfWindowIndication onu_drift_of_window_ind = 6;
+ OnuLossOfOmciChannelIndication onu_loss_omci_ind = 7;
+ OnuSignalsFailureIndication onu_signals_fail_ind = 8;
+ OnuTransmissionInterferenceWarning onu_tiwi_ind = 9;
+ OnuActivationFailureIndication onu_activation_fail_ind = 10;
+ OnuProcessingErrorIndication onu_processing_error_ind = 11;
+ }
+}
+
+message OltIndication {
+ string oper_state = 1; // up, down
+}
+
+message IntfIndication {
+ fixed32 intf_id = 1;
+ string oper_state = 2; // up, down
+}
+
+message OnuDiscIndication {
+ fixed32 intf_id = 1;
+ SerialNumber serial_number = 2;
+}
+
+message OnuIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string oper_state = 3; // up, down
+ string admin_state = 5; // up, down
+ SerialNumber serial_number = 4;
+}
+
+message IntfOperIndication {
+ string type = 1; // nni, pon
+ fixed32 intf_id = 2;
+ string oper_state = 3; // up, down
+}
+
+message OmciIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ bytes pkt = 3;
+}
+
+message PacketIndication {
+ string intf_type = 5; // nni, pon, unknown
+ fixed32 intf_id = 1;
+ fixed32 gemport_id = 2;
+ fixed32 flow_id = 3;
+ bytes pkt = 4;
+}
+
+message Interface {
+ fixed32 intf_id = 1;
+}
+
+message Heartbeat {
+ fixed32 heartbeat_signature = 1;
+}
+
+message Onu {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ SerialNumber serial_number = 3;
+ fixed32 pir = 4; // peak information rate assigned to onu
+ fixed32 alloc_id = 5;
+}
+
+message OmciMsg {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ bytes pkt = 3;
+}
+
+message OnuPacket {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ bytes pkt = 3;
+}
+
+message UplinkPacket {
+ fixed32 intf_id = 1;
+ bytes pkt = 2;
+}
+
+message DeviceInfo {
+ string vendor = 1;
+ string model = 2;
+ string hardware_version = 3;
+ string firmware_version = 4;
+
+ // Total number of pon intf ports on the device
+ fixed32 pon_ports = 12;
+
+ // If using global per-device technology profile. To be deprecated
+ string technology = 5;
+ fixed32 onu_id_start = 6;
+ fixed32 onu_id_end = 7;
+ fixed32 alloc_id_start = 8;
+ fixed32 alloc_id_end = 9;
+ fixed32 gemport_id_start = 10;
+ fixed32 gemport_id_end = 11;
+ fixed32 flow_id_start = 13;
+ fixed32 flow_id_end = 14;
+
+ message DeviceResourceRanges {
+
+ // List of 0 or more intf_ids that use the same technology and pools.
+ // If 0 intf_ids supplied, it implies ALL interfaces
+ repeated fixed32 intf_ids = 1;
+
+ // Technology profile for this pool
+ string technology = 2;
+
+ message Pool {
+ enum PoolType {
+ ONU_ID = 0;
+ ALLOC_ID = 1;
+ GEMPORT_ID = 2;
+ FLOW_ID = 3;
+ }
+
+ enum SharingType {
+ DEDICATED_PER_INTF = 0;
+ SHARED_BY_ALL_INTF_ALL_TECH = 1; // Shared across all interfaces in all technologies in all ranges
+ SHARED_BY_ALL_INTF_SAME_TECH = 2; // Shared across all interfaces of the same technology used in this range
+ }
+
+ PoolType type = 1;
+ SharingType sharing = 2;
+ fixed32 start = 3; // lower bound on IDs allocated from this pool
+ fixed32 end = 4; // upper bound on IDs allocated from this pool
+ }
+ repeated Pool pools = 3;
+ }
+ repeated DeviceResourceRanges ranges = 15;
+}
+
+message Classifier {
+ fixed32 o_tpid = 1;
+ fixed32 o_vid = 2;
+ fixed32 i_tpid = 3;
+ fixed32 i_vid = 4;
+ fixed32 o_pbits = 5;
+ fixed32 i_pbits = 6;
+ fixed32 eth_type = 7;
+ bytes dst_mac = 8;
+ bytes src_mac = 9;
+ fixed32 ip_proto = 10;
+ fixed32 dst_ip = 11;
+ fixed32 src_ip = 12;
+ fixed32 src_port = 13;
+ fixed32 dst_port = 14;
+ string pkt_tag_type = 15; // untagged, single_tag, double_tag
+}
+
+message ActionCmd {
+ bool add_outer_tag = 1;
+ bool remove_outer_tag = 2;
+ bool trap_to_host = 3;
+}
+
+message Action {
+ ActionCmd cmd = 1;
+ fixed32 o_vid = 2;
+ fixed32 o_pbits = 3;
+ fixed32 o_tpid = 4;
+ fixed32 i_vid = 5;
+ fixed32 i_pbits = 6;
+ fixed32 i_tpid = 7;
+}
+
+message Flow {
+ sfixed32 access_intf_id = 1;
+ sfixed32 onu_id = 2;
+ fixed32 flow_id = 3;
+ string flow_type = 4; // upstream, downstream, broadcast, multicast
+ sfixed32 alloc_id = 10;
+ sfixed32 network_intf_id = 5;
+ sfixed32 gemport_id = 6;
+ Classifier classifier = 7;
+ Action action = 8;
+ sfixed32 priority = 9;
+}
+
+message SerialNumber {
+ bytes vendor_id = 1;
+ bytes vendor_specific = 2;
+}
+
+message PortStatistics {
+ fixed32 intf_id = 1;
+ fixed64 rx_bytes = 2;
+ fixed64 rx_packets = 3;
+ fixed64 rx_ucast_packets = 4;
+ fixed64 rx_mcast_packets = 5;
+ fixed64 rx_bcast_packets = 6;
+ fixed64 rx_error_packets = 7;
+ fixed64 tx_bytes = 8;
+ fixed64 tx_packets = 9;
+ fixed64 tx_ucast_packets = 10;
+ fixed64 tx_mcast_packets = 11;
+ fixed64 tx_bcast_packets = 12;
+ fixed64 tx_error_packets = 13;
+ fixed64 rx_crc_errors = 14;
+ fixed64 bip_errors = 15;
+ fixed32 timestamp = 16;
+}
+
+message FlowStatistics {
+ fixed32 flow_id = 1;
+ fixed64 rx_bytes = 2;
+ fixed64 rx_packets = 3;
+ fixed64 tx_bytes = 8;
+ fixed64 tx_packets = 9;
+ fixed32 timestamp = 16;
+}
+
+message LosIndication {
+ fixed32 intf_id = 1;
+ string status = 2;
+}
+
+message DyingGaspIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+}
+
+message OnuAlarmIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string los_status = 3;
+ string lob_status = 4;
+ string lopc_miss_status = 5;
+ string lopc_mic_error_status = 6;
+}
+
+message OnuStartupFailureIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+}
+
+message OnuSignalDegradeIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+ fixed32 inverse_bit_error_rate = 4;
+}
+
+message OnuDriftOfWindowIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+ fixed32 drift = 4;
+ fixed32 new_eqd = 5;
+}
+
+message OnuLossOfOmciChannelIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+}
+
+message OnuSignalsFailureIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+ fixed32 inverse_bit_error_rate = 4;
+}
+
+message OnuTransmissionInterferenceWarning {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+ string status = 3;
+ fixed32 drift = 4;
+}
+
+message OnuActivationFailureIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+}
+
+message OnuProcessingErrorIndication {
+ fixed32 intf_id = 1;
+ fixed32 onu_id = 2;
+}
+
+
+message Empty {}
diff --git a/python/protos/third_party/__init__.py b/python/protos/third_party/__init__.py
new file mode 100644
index 0000000..1767870
--- /dev/null
+++ b/python/protos/third_party/__init__.py
@@ -0,0 +1,53 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+This helps loading http_pb2 and annotations_pb2.
+Without this, the Python importer will not be able to process the lines:
+from google.api import http_pb2 or
+from google.api import annotations_pb2
+(Without importing these, the protobuf loader will not recognize http options
+in the protobuf definitions.)
+"""
+
+from importlib import import_module
+import os
+import sys
+
+
+class GoogleApiImporter(object):
+
+ def find_module(self, full_name, path=None):
+ if full_name == 'google.api':
+ self.path = [os.path.dirname(__file__)]
+ return self
+
+ def load_module(self, name):
+ if name in sys.modules:
+ return sys.modules[name]
+ full_name = 'python.protos.third_party.' + name
+ import_module(full_name)
+ module = sys.modules[full_name]
+ sys.modules[name] = module
+ return module
+
+
+sys.meta_path.append(GoogleApiImporter())
+try:
+ from google.api import http_pb2, annotations_pb2
+ _ = http_pb2, annotations_pb2
+except AssertionError:
+ pass
diff --git a/python/protos/third_party/google/LICENSE b/python/protos/third_party/google/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/python/protos/third_party/google/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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/python/protos/third_party/google/__init__.py b/python/protos/third_party/google/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/protos/third_party/google/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/protos/third_party/google/api/__init__.py b/python/protos/third_party/google/api/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/python/protos/third_party/google/api/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/python/protos/third_party/google/api/annotations.desc b/python/protos/third_party/google/api/annotations.desc
new file mode 100644
index 0000000..4a4bd7d
--- /dev/null
+++ b/python/protos/third_party/google/api/annotations.desc
Binary files differ
diff --git a/python/protos/third_party/google/api/annotations.proto b/python/protos/third_party/google/api/annotations.proto
new file mode 100644
index 0000000..cbd18b8
--- /dev/null
+++ b/python/protos/third_party/google/api/annotations.proto
@@ -0,0 +1,29 @@
+// Copyright (c) 2015, Google 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.
+
+syntax = "proto3";
+
+package google.api;
+
+import "google/api/http.proto";
+import "google/protobuf/descriptor.proto";
+
+option java_multiple_files = true;
+option java_outer_classname = "AnnotationsProto";
+option java_package = "com.google.api";
+
+extend google.protobuf.MethodOptions {
+ // See `HttpRule`.
+ HttpRule http = 72295728;
+}
diff --git a/python/protos/third_party/google/api/http.proto b/python/protos/third_party/google/api/http.proto
new file mode 100644
index 0000000..ce07aa1
--- /dev/null
+++ b/python/protos/third_party/google/api/http.proto
@@ -0,0 +1,127 @@
+// Copyright (c) 2015, Google 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.
+
+syntax = "proto3";
+
+package google.api;
+
+option java_multiple_files = true;
+option java_outer_classname = "HttpProto";
+option java_package = "com.google.api";
+
+
+// `HttpRule` defines the mapping of an RPC method to one or more HTTP REST API
+// methods. The mapping determines what portions of the request message are
+// populated from the path, query parameters, or body of the HTTP request. The
+// mapping is typically specified as an `google.api.http` annotation, see
+// "google/api/annotations.proto" for details.
+//
+// The mapping consists of a mandatory field specifying a path template and an
+// optional `body` field specifying what data is represented in the HTTP request
+// body. The field name for the path indicates the HTTP method. Example:
+//
+// ```
+// package google.storage.v2;
+//
+// import "google/api/annotations.proto";
+//
+// service Storage {
+// rpc CreateObject(CreateObjectRequest) returns (Object) {
+// option (google.api.http) {
+// post: "/v2/{bucket_name=buckets/*}/objects"
+// body: "object"
+// };
+// };
+// }
+// ```
+//
+// Here `bucket_name` and `object` bind to fields of the request message
+// `CreateObjectRequest`.
+//
+// The rules for mapping HTTP path, query parameters, and body fields
+// to the request message are as follows:
+//
+// 1. The `body` field specifies either `*` or a field path, or is
+// omitted. If omitted, it assumes there is no HTTP body.
+// 2. Leaf fields (recursive expansion of nested messages in the
+// request) can be classified into three types:
+// (a) Matched in the URL template.
+// (b) Covered by body (if body is `*`, everything except (a) fields;
+// else everything under the body field)
+// (c) All other fields.
+// 3. URL query parameters found in the HTTP request are mapped to (c) fields.
+// 4. Any body sent with an HTTP request can contain only (b) fields.
+//
+// The syntax of the path template is as follows:
+//
+// Template = "/" Segments [ Verb ] ;
+// Segments = Segment { "/" Segment } ;
+// Segment = "*" | "**" | LITERAL | Variable ;
+// Variable = "{" FieldPath [ "=" Segments ] "}" ;
+// FieldPath = IDENT { "." IDENT } ;
+// Verb = ":" LITERAL ;
+//
+// `*` matches a single path component, `**` zero or more path components, and
+// `LITERAL` a constant. A `Variable` can match an entire path as specified
+// again by a template; this nested template must not contain further variables.
+// If no template is given with a variable, it matches a single path component.
+// The notation `{var}` is henceforth equivalent to `{var=*}`.
+//
+// Use CustomHttpPattern to specify any HTTP method that is not included in the
+// pattern field, such as HEAD, or "*" to leave the HTTP method unspecified for
+// a given URL path rule. The wild-card rule is useful for services that provide
+// content to Web (HTML) clients.
+message HttpRule {
+
+ // Determines the URL pattern is matched by this rules. This pattern can be
+ // used with any of the {get|put|post|delete|patch} methods. A custom method
+ // can be defined using the 'custom' field.
+ oneof pattern {
+ // Used for listing and getting information about resources.
+ string get = 2;
+
+ // Used for updating a resource.
+ string put = 3;
+
+ // Used for creating a resource.
+ string post = 4;
+
+ // Used for deleting a resource.
+ string delete = 5;
+
+ // Used for updating a resource.
+ string patch = 6;
+
+ // Custom pattern is used for defining custom verbs.
+ CustomHttpPattern custom = 8;
+ }
+
+ // The name of the request field whose value is mapped to the HTTP body, or
+ // `*` for mapping all fields not captured by the path pattern to the HTTP
+ // body.
+ string body = 7;
+
+ // Additional HTTP bindings for the selector. Nested bindings must not
+ // specify a selector and must not contain additional bindings.
+ repeated HttpRule additional_bindings = 11;
+}
+
+// A custom pattern is used for defining custom HTTP verb.
+message CustomHttpPattern {
+ // The name of this custom HTTP verb.
+ string kind = 1;
+
+ // The path matched by this custom verb.
+ string path = 2;
+}
diff --git a/python/requirements.txt b/python/requirements.txt
new file mode 100644
index 0000000..56cb356
--- /dev/null
+++ b/python/requirements.txt
@@ -0,0 +1,69 @@
+argparse==1.2.1
+arrow==0.10.0
+bitstring==3.1.5
+cmd2==0.7.0
+colorama==0.3.9
+confluent-kafka==0.11.5
+cython==0.24.1
+decorator==4.1.2
+docker-py==1.10.6
+fluent-logger==0.6.0
+grpc==0.3.post19
+grpcio==1.3.5
+grpcio-tools==1.3.5
+hash_ring==1.3.1
+hexdump==3.3
+jinja2==2.8
+jsonpatch==1.16
+kafka_python==1.3.5
+klein==17.10.0
+kubernetes==5.0.0
+netaddr==0.7.19
+networkx==2.0
+nose==1.3.7
+nose-exclude==0.5.0
+nose-testconfig==0.10
+mock==2.0.0
+netifaces==0.10.6
+pcapy==0.11.1
+pep8==1.7.1
+pep8-naming>=0.3.3
+protobuf==3.3.0
+protobuf-to-dict==0.1.0
+pyflakes==1.6.0
+pylint==1.7.6
+#pypcap>=1.1.5
+pyOpenSSL==17.3.0
+PyYAML==3.12
+requests==2.18.4
+scapy==2.3.3
+service-identity==17.0.0
+simplejson==3.12.0
+jsonschema==2.6.0
+six==1.11.0
+structlog==17.2.0
+termcolor==1.1.0
+transitions==0.6.4
+treq==17.8.0
+Twisted==17.9.0
+txaioetcd==0.3.0
+urllib3==1.22
+pyang==1.7.3
+lxml==3.6.4
+nosexcover==1.0.11
+zmq==0.0.0
+pyzmq==16.0.3
+txZMQ==0.8.0
+ncclient==0.5.3
+xmltodict==0.11.0
+dicttoxml==1.7.4
+etcd3==0.7.0
+pyparsing==2.2.0
+packaging==17.1
+
+# python-consul>=0.6.1 we need the pre-released version for now, because 0.6.1 does not
+# yet support Twisted. Once this is released, it will be the 0.6.2 version
+git+https://github.com/cablehead/python-consul.git
+
+# Twisted Python kafka client
+git+https://github.com/ciena/afkak.git