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