From: xinhuili Date: Mon, 12 Aug 2019 22:16:17 +0000 (+0800) Subject: Add olt src X-Git-Url: https://gerrit.akraino.org/r/gitweb?a=commitdiff_plain;h=refs%2Fchanges%2F95%2F1395%2F1;p=iec%2Fxconnect.git Add olt src This patch is to add olt-src on NSX Signed-off-by: XINHUI LI Change-Id: I34cff828ce16c6746240973db9e8d028a3e9a715 --- diff --git a/src/olt-service/.gitignore b/src/olt-service/.gitignore new file mode 100644 index 0000000..a48d594 --- /dev/null +++ b/src/olt-service/.gitignore @@ -0,0 +1,9 @@ +.idea/ +*.pyc +.DS_Store + +.coverage +htmlcov +coverage.xml +nose2-junit.xml + diff --git a/src/olt-service/.gitreview b/src/olt-service/.gitreview new file mode 100644 index 0000000..0e952d2 --- /dev/null +++ b/src/olt-service/.gitreview @@ -0,0 +1,5 @@ +[gerrit] +host=gerrit.opencord.org +port=29418 +project=olt-service.git +defaultremote=origin diff --git a/src/olt-service/Dockerfile.synchronizer b/src/olt-service/Dockerfile.synchronizer new file mode 100644 index 0000000..e2c0819 --- /dev/null +++ b/src/olt-service/Dockerfile.synchronizer @@ -0,0 +1,56 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# docker build -t xosproject/volt-synchronizer:candidate -f Dockerfile.synchronizer . + +# xosproject/volt-synchronizer + +FROM cachengo/xos-synchronizer-base:2.1.25 + +COPY xos/synchronizer /opt/xos/synchronizers/volt +COPY VERSION /opt/xos/synchronizers/volt/ +COPY samples/onu_activate_event.py /opt/xos/synchronizers/volt/ + +WORKDIR "/opt/xos/synchronizers/volt" + +# Label image +ARG org_label_schema_schema_version=1.0 +ARG org_label_schema_name=volt-synchronizer +ARG org_label_schema_version=unknown +ARG org_label_schema_vcs_url=unknown +ARG org_label_schema_vcs_ref=unknown +ARG org_label_schema_build_date=unknown +ARG org_opencord_vcs_commit_date=unknown +ARG org_opencord_component_chameleon_version=unknown +ARG org_opencord_component_chameleon_vcs_url=unknown +ARG org_opencord_component_chameleon_vcs_ref=unknown +ARG org_opencord_component_xos_version=unknown +ARG org_opencord_component_xos_vcs_url=unknown +ARG org_opencord_component_xos_vcs_ref=unknown + +LABEL org.label-schema.schema-version=$org_label_schema_schema_version \ + org.label-schema.name=$org_label_schema_name \ + org.label-schema.version=$org_label_schema_version \ + org.label-schema.vcs-url=$org_label_schema_vcs_url \ + org.label-schema.vcs-ref=$org_label_schema_vcs_ref \ + org.label-schema.build-date=$org_label_schema_build_date \ + org.opencord.vcs-commit-date=$org_opencord_vcs_commit_date \ + org.opencord.component.chameleon.version=$org_opencord_component_chameleon_version \ + org.opencord.component.chameleon.vcs-url=$org_opencord_component_chameleon_vcs_url \ + org.opencord.component.chameleon.vcs-ref=$org_opencord_component_chameleon_vcs_ref \ + org.opencord.component.xos.version=$org_opencord_component_xos_version \ + org.opencord.component.xos.vcs-url=$org_opencord_component_xos_vcs_url \ + org.opencord.component.xos.vcs-ref=$org_opencord_component_xos_vcs_ref + +CMD ["/usr/bin/python", "/opt/xos/synchronizers/volt/volt-synchronizer.py"] diff --git a/src/olt-service/Jenkinsfile b/src/olt-service/Jenkinsfile new file mode 100644 index 0000000..a58adb4 --- /dev/null +++ b/src/olt-service/Jenkinsfile @@ -0,0 +1,42 @@ +pipeline { + agent any + stages { + stage('Build') { + parallel { + stage('Build aarch64') { + agent { + node { + label 'aarch64' + } + + } + steps { + withDockerRegistry([ credentialsId: "fcf9c294-b8a9-4f7e-87d6-d0446f712411", url: "https://index.docker.io/v1/" ]) { + sh 'ci_scripts/push_containers.sh' + } + } + } + stage('Build x86') { + agent { + node { + label 'x86_64' + } + + } + steps { + withDockerRegistry([ credentialsId: "fcf9c294-b8a9-4f7e-87d6-d0446f712411", url: "https://index.docker.io/v1/" ]) { + sh 'ci_scripts/push_containers.sh' + } + } + } + } + } + stage('Push Manifest') { + steps { + withDockerRegistry([ credentialsId: "fcf9c294-b8a9-4f7e-87d6-d0446f712411", url: "https://index.docker.io/v1/" ]) { + sh 'ci_scripts/push_manifest.sh' + } + } + } + } +} diff --git a/src/olt-service/LICENSE.txt b/src/olt-service/LICENSE.txt new file mode 100644 index 0000000..d9d9d30 --- /dev/null +++ b/src/olt-service/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016 Open Networking Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/olt-service/Makefile b/src/olt-service/Makefile new file mode 100644 index 0000000..e1591ca --- /dev/null +++ b/src/olt-service/Makefile @@ -0,0 +1,13 @@ + +ISVENV := $(shell python -c 'import sys; print ("1" if hasattr(sys, "real_prefix") else "0")') + +build: + docker build -t xosproject/volt-synchronizer:candidate -f xos/synchronizer/Dockerfile.synchronizer ./xos/synchronizer/ + +test: +ifeq ($(ISVENV), 1) + pip install requests-mock + pushd xos/synchronizer/steps/; nosetests test_sync_olt_device.py; popd; +else + @echo "Please activate the virtualenv and the required libraries, you can do that using the 'scripts/setup_venv.sh' tool in the 'xos' repo" +endif diff --git a/src/olt-service/README.md b/src/olt-service/README.md new file mode 100644 index 0000000..3518d01 --- /dev/null +++ b/src/olt-service/README.md @@ -0,0 +1,22 @@ +# vOLT + +This repositoritory contains the XOS service that is responsible for the integration with VOLTHA. + +At the moment the `RCORDService` assume that this service (or a service exposing the same APIs) is sitting after it in the service chain. + +## ONU activation discovery + +This service will listen for events on the kafka bus, in the topic `onu.events` + +The events this service will react to are: +- onu activated + +### ONU Activation policy + +We assume that: +- the `vOLTService` service is a subscriber of `MyOssService` via `ServiceDependency` +- `MyOssService` has `type = oss` +- `MyOssService` expose and API named `validate_onu` + - the `validate_onu` API will be responsible to create a subscriber in XOS + +If no OSS Service is found in between the providers of `vOLTService` no action is taken as a consequence of an event. diff --git a/src/olt-service/VERSION b/src/olt-service/VERSION new file mode 100644 index 0000000..1b5105d --- /dev/null +++ b/src/olt-service/VERSION @@ -0,0 +1 @@ +2.1.14 diff --git a/src/olt-service/ci_scripts/push_containers.sh b/src/olt-service/ci_scripts/push_containers.sh new file mode 100755 index 0000000..c9dcb35 --- /dev/null +++ b/src/olt-service/ci_scripts/push_containers.sh @@ -0,0 +1,6 @@ +export IMAGE_TAG=$(cat VERSION) +export AARCH=`uname -m` +export IMAGE_NAME=volt-synchronizer + +docker build -t cachengo/$IMAGE_NAME-$AARCH:$IMAGE_TAG -f Dockerfile.synchronizer . +docker push cachengo/$IMAGE_NAME-$AARCH:$IMAGE_TAG diff --git a/src/olt-service/ci_scripts/push_manifest.sh b/src/olt-service/ci_scripts/push_manifest.sh new file mode 100755 index 0000000..de9c94b --- /dev/null +++ b/src/olt-service/ci_scripts/push_manifest.sh @@ -0,0 +1,7 @@ +export IMAGE_TAG=$(cat VERSION) +export AARCH=`uname -m` +export IMAGE_NAME=volt-synchronizer +export DOCKER_CLI_EXPERIMENTAL=enabled + +docker manifest create --amend cachengo/$IMAGE_NAME:$IMAGE_TAG cachengo/$IMAGE_NAME-x86_64:$IMAGE_TAG cachengo/$IMAGE_NAME-aarch64:$IMAGE_TAG +docker manifest push cachengo/$IMAGE_NAME:$IMAGE_TAG diff --git a/src/olt-service/docs/README.md b/src/olt-service/docs/README.md new file mode 100644 index 0000000..4c7dff6 --- /dev/null +++ b/src/olt-service/docs/README.md @@ -0,0 +1,85 @@ +# vOLT Service + +The `vOLTService` is responsible to configure the access network in RCORD and it +does that leveraging `VOLTHA`. + +## vOLT Modeling + +Here is an image describing the models that compose this service, +followed by a brief description of any of them. +For a full reference on these models, please take a look at their +[xProto](https://github.com/opencord/olt-service/blob/master/xos/synchronizer/models/volt.xproto) +representation. + +![ER Diagram](static/vOLTService_ER_diagram.png) + +**vOLTService** + +Contains the information that the synchronizer needs to access `VOLTHA` and +`ONOS-VOLTHA` + +**vOLTServiceInstance** + +Represent a subscriber in the service chain + +**OLTDevice** + +Contains the information needed to pre-provision and activate an OLT device in +VOLTHA + +**PONPort, NNIPort, ONUDevice** +These models contain informations about the respective components of the PON +network. In an RCORD deployment these models are pulled from VOLTHA to keep an +inventory of the system. + +## Synchronizations steps + +### Push steps + +There are two top-down steps in this service: + +- `SyncOLTDevice` to pre-provision and enable OLT devices +- `SyncVOLTServiceInstance` to add the subscriber in `ONOS-VOLTHA` + +### Pull steps + +The vOLT synchronizer is currently pulling `OLTDevice`, `PONPort`, `NNIPort` and +`ONUDevices` from `VOLTHA`. + +### Event steps + +The vOLT synchronizer is listening over the kafka bus for events in the `onu.events` +topic. + +#### ONU Activate event + +Event format: + +```json +{ + "status": "activated", + "serial_number": "BRCM1234", + "uni_port_id": 16, + "of_dpid": "of:109299321" +} +``` + +When an event is received: + +- the `vOLTService` checks if in between is provider services there is one with `kind = oss` +- It calls the `validate_onu` method exposed by that service +- the OSS Service will be responsible to create a subscriber in XOS + +If no OSS Service is found in between the providers of `vOLTService` +no action is taken as a consequence of an event. + +For more informations about the OSS Service, please look +[here](../hippie-oss/README.md) and in the [R-CORD Configuration guide](../profiles/rcord/configuration.md) + +## Test configuration + +If you are testing the R-CORD Service chain (meaning that you don't have a +running VOLTHA), you will need to manually create a `PONPort` and an `ONUDevice`. + +To do that, please check the example TOSCA in the repository +[samples](https://github.com/opencord/olt-service/tree/master/samples) folder. \ No newline at end of file diff --git a/src/olt-service/docs/static/vOLTService_ER_diagram.png b/src/olt-service/docs/static/vOLTService_ER_diagram.png new file mode 100644 index 0000000..5c832d3 Binary files /dev/null and b/src/olt-service/docs/static/vOLTService_ER_diagram.png differ diff --git a/src/olt-service/samples/README.md b/src/olt-service/samples/README.md new file mode 100644 index 0000000..da8db0e --- /dev/null +++ b/src/olt-service/samples/README.md @@ -0,0 +1,4 @@ +# Setup the vOLT Service to work with VOLTHA + +The TOSCA recipes in this directory are intended to support development, +but they can be used as a starting point to configure a real deployment. \ No newline at end of file diff --git a/src/olt-service/samples/olt_device_host_and_port.yaml b/src/olt-service/samples/olt_device_host_and_port.yaml new file mode 100644 index 0000000..c0ec47c --- /dev/null +++ b/src/olt-service/samples/olt_device_host_and_port.yaml @@ -0,0 +1,46 @@ +# 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. + +# curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X POST --data-binary @olt_device_host_and_port.yaml http://192.168.99.100:30007/run + +tosca_definitions_version: tosca_simple_yaml_1_0 +imports: + - custom_types/oltdevice.yaml + - custom_types/voltservice.yaml +description: Create a simulated OLT Device in VOLTHA +topology_template: + node_templates: + + service#volt: + type: tosca.nodes.VOLTService + properties: + name: volt + must-exist: true + + device#olt: + type: tosca.nodes.OLTDevice + properties: + name: test_olt + device_type: ponsim + host: 172.17.0.1 + port: 50060 + switch_datapath_id: of:0000000000000001 + switch_port: "1" + outer_tpid: "0x8100" + dp_id: of:0000000ce2314000 + uplink: "129" + requirements: + - volt_service: + node: service#volt + relationship: tosca.relationships.BelongsToOne diff --git a/src/olt-service/samples/olt_device_mac_address.yaml b/src/olt-service/samples/olt_device_mac_address.yaml new file mode 100644 index 0000000..42213bb --- /dev/null +++ b/src/olt-service/samples/olt_device_mac_address.yaml @@ -0,0 +1,43 @@ +# 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. + +# curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X POST --data-binary @olt_device_mac_address.yaml http://192.168.99.100:30007/run + +tosca_definitions_version: tosca_simple_yaml_1_0 +imports: + - custom_types/oltdevice.yaml + - custom_types/voltservice.yaml +description: Create a simulated OLT Device in VOLTHA +topology_template: + node_templates: + + service#volt: + type: tosca.nodes.VOLTService + properties: + name: volt + must-exist: true + + device#olt: + type: tosca.nodes.OLTDevice + properties: + name: test_olt + device_type: simulated_olt + mac_address: 00:0c:e2:31:40:00 + switch_datapath_id: of:0000000000000001 + switch_port: "1" + outer_tpid: "0x8100" + requirements: + - volt_service: + node: service#volt + relationship: tosca.relationships.BelongsToOne diff --git a/src/olt-service/samples/onu_activate_event.py b/src/olt-service/samples/onu_activate_event.py new file mode 100644 index 0000000..8786ed5 --- /dev/null +++ b/src/olt-service/samples/onu_activate_event.py @@ -0,0 +1,29 @@ + +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Manually send the event + +import json +from kafka import KafkaProducer + +event = json.dumps({ + 'status': 'activated', + 'serial_number': 'BRCM1234', + 'uni_port_id': 16, + 'of_dpid': 'of:109299321' +}) +producer = KafkaProducer(bootstrap_servers="cord-kafka") +producer.send("onu.events", event) +producer.flush() \ No newline at end of file diff --git a/src/olt-service/samples/pon_port.yaml b/src/olt-service/samples/pon_port.yaml new file mode 100644 index 0000000..2dfe376 --- /dev/null +++ b/src/olt-service/samples/pon_port.yaml @@ -0,0 +1,62 @@ +# 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. + +# curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X POST --data-binary @pon_port.yaml http://192.168.99.100:30007/run + +tosca_definitions_version: tosca_simple_yaml_1_0 +imports: + - custom_types/oltdevice.yaml + - custom_types/onudevice.yaml + - custom_types/ponport.yaml + - custom_types/voltservice.yaml + - custom_types/uniport.yaml +description: Create a simulated OLT Device in VOLTHA +topology_template: + node_templates: + + device#olt: + type: tosca.nodes.OLTDevice + properties: + name: test_olt + must-exist: true + + pon_port: + type: tosca.nodes.PONPort + properties: + name: test_pon_port_1 + port_no: 2 + requirements: + - olt_device: + node: device#olt + relationship: tosca.relationships.BelongsToOne + + device#onu: + type: tosca.nodes.ONUDevice + properties: + serial_number: BRCM1234 + vendor: Broadcom + requirements: + - pon_port: + node: pon_port + relationship: tosca.relationships.BelongsToOne + + uni_port: + type: tosca.nodes.UNIPort + properties: + name: test_uni_port_1 + port_no: 2 + requirements: + - onu_device: + node: device#onu + relationship: tosca.relationships.BelongsToOne diff --git a/src/olt-service/samples/second_pon_port.yaml b/src/olt-service/samples/second_pon_port.yaml new file mode 100644 index 0000000..91882e0 --- /dev/null +++ b/src/olt-service/samples/second_pon_port.yaml @@ -0,0 +1,62 @@ +# 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. + +# curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X POST --data-binary @pon_port.yaml http://192.168.99.100:30007/run + +tosca_definitions_version: tosca_simple_yaml_1_0 +imports: + - custom_types/oltdevice.yaml + - custom_types/onudevice.yaml + - custom_types/ponport.yaml + - custom_types/voltservice.yaml + - custom_types/uniport.yaml +description: Create a simulated OLT Device in VOLTHA +topology_template: + node_templates: + + device#olt: + type: tosca.nodes.OLTDevice + properties: + name: test_olt + must-exist: true + + pon_port2: + type: tosca.nodes.PONPort + properties: + name: test_pon_port_2 + port_no: 3 + requirements: + - olt_device: + node: device#olt + relationship: tosca.relationships.BelongsToOne + + device#onu2: + type: tosca.nodes.ONUDevice + properties: + serial_number: BRCM1235 + vendor: Broadcom + requirements: + - pon_port: + node: pon_port2 + relationship: tosca.relationships.BelongsToOne + + uni_port2: + type: tosca.nodes.UNIPort + properties: + name: test_uni_port_2 + port_no: 3 + requirements: + - onu_device: + node: device#onu2 + relationship: tosca.relationships.BelongsToOne diff --git a/src/olt-service/samples/second_subscriber.yaml b/src/olt-service/samples/second_subscriber.yaml new file mode 100644 index 0000000..063e49e --- /dev/null +++ b/src/olt-service/samples/second_subscriber.yaml @@ -0,0 +1,32 @@ +# 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. + +# curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X POST --data-binary @subscriber.yaml http://192.168.99.100:30007/run + +tosca_definitions_version: tosca_simple_yaml_1_0 +imports: + - custom_types/rcordsubscriber.yaml +description: Create a test subscriber +topology_template: + node_templates: + # A subscriber + my_house2: + type: tosca.nodes.RCORDSubscriber + properties: + name: My Second House + c_tag: 112 + s_tag: 222 + onu_device: BRCM1234 + ip_address: "1.2.3.5" + mac_address: "11:22:33:44:55:77" diff --git a/src/olt-service/samples/subscriber.yaml b/src/olt-service/samples/subscriber.yaml new file mode 100644 index 0000000..ebd3a1b --- /dev/null +++ b/src/olt-service/samples/subscriber.yaml @@ -0,0 +1,32 @@ +# 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. + +# curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X POST --data-binary @subscriber.yaml http://192.168.99.100:30007/run + +tosca_definitions_version: tosca_simple_yaml_1_0 +imports: + - custom_types/rcordsubscriber.yaml +description: Create a test subscriber +topology_template: + node_templates: + # A subscriber + my_house: + type: tosca.nodes.RCORDSubscriber + properties: + name: My House + c_tag: 111 + s_tag: 222 + onu_device: BRCM1234 + ip_address: "1.2.3.4" + mac_address: "11:22:33:44:55:66" diff --git a/src/olt-service/samples/volt_service.yaml b/src/olt-service/samples/volt_service.yaml new file mode 100644 index 0000000..c12525e --- /dev/null +++ b/src/olt-service/samples/volt_service.yaml @@ -0,0 +1,33 @@ +# 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. + +# curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X POST --data-binary @volt_service.yaml http://192.168.99.100:30007/run + +tosca_definitions_version: tosca_simple_yaml_1_0 +imports: + - custom_types/voltservice.yaml +description: Updates the existing vOLT service with deployment specific data +topology_template: + node_templates: + service#volt: + type: tosca.nodes.VOLTService + properties: + name: volt + must-exist: true + voltha_url: 10.128.22.3 + voltha_port: 8882 + onos_voltha_url: 10.128.22.3 + onos_voltha_port: 8181 + onos_voltha_user: karaf + onos_voltha_pass: karaf diff --git a/src/olt-service/xos/synchronizer/config.yaml b/src/olt-service/xos/synchronizer/config.yaml new file mode 100644 index 0000000..8a1a51e --- /dev/null +++ b/src/olt-service/xos/synchronizer/config.yaml @@ -0,0 +1,38 @@ + +# 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. + + +name: volt +required_models: + - VOLTService + - VOLTServiceInstance + - ServiceInstanceLink + - OLTDevice +dependency_graph: "/opt/xos/synchronizers/volt/model-deps" +model_policies_dir: "/opt/xos/synchronizers/volt/model_policies" +models_dir: "/opt/xos/synchronizers/volt/models" +steps_dir: "/opt/xos/synchronizers/volt/steps" +pull_steps_dir: "/opt/xos/synchronizers/volt/pull_steps" +event_steps_dir: "/opt/xos/synchronizers/volt/event_steps" +logging: + version: 1 + handlers: + console: + class: logging.StreamHandler + loggers: + 'multistructlog': + handlers: + - console + level: DEBUG diff --git a/src/olt-service/xos/synchronizer/event_steps/kubernetes_event.py b/src/olt-service/xos/synchronizer/event_steps/kubernetes_event.py new file mode 100644 index 0000000..aa8bef8 --- /dev/null +++ b/src/olt-service/xos/synchronizer/event_steps/kubernetes_event.py @@ -0,0 +1,68 @@ + +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import json +import os +import sys +from synchronizers.new_base.eventstep import EventStep +from synchronizers.new_base.modelaccessor import VOLTService, VOLTServiceInstance, Service +from xosconfig import Config +from multistructlog import create_logger + +log = create_logger(Config().get('logging')) + +class KubernetesPodDetailsEventStep(EventStep): + topics = ["xos.kubernetes.pod-details"] + technology = "kafka" + + def __init__(self, *args, **kwargs): + super(KubernetesPodDetailsEventStep, self).__init__(*args, **kwargs) + + @staticmethod + def get_onos(service): + service = Service.objects.get(id=service.id) + + # get the onos_fabric service + onos = [s.leaf_model for s in service.subscriber_services if "onos" in s.name.lower()] + + if len(onos) == 0: + raise Exception('Cannot find ONOS service in provider_services of Fabric-Crossconnect') + + return onos[0] + + def process_event(self, event): + value = json.loads(event.value) + + if (value.get("status") != "created"): + return + + if "labels" not in value: + return + + xos_service = value["labels"].get("xos_service") + if not xos_service: + return + + for service in VOLTService.objects.all(): + onos = KubernetesPodDetailsEventStep.get_onos(service) + if (onos.name.lower() != xos_service.lower()): + continue + + for service_instance in service.service_instances.all(): + log.info("Dirtying VOLTServiceInstance", service_instance=service_instance) + service_instance.backend_code=0 + service_instance.backend_status="resynchronize due to kubernetes event" + service_instance.save(update_fields=["updated", "backend_code", "backend_status"], always_update_timestamp=True) diff --git a/src/olt-service/xos/synchronizer/event_steps/test_kubernetes_event.py b/src/olt-service/xos/synchronizer/event_steps/test_kubernetes_event.py new file mode 100644 index 0000000..32d8e1f --- /dev/null +++ b/src/olt-service/xos/synchronizer/event_steps/test_kubernetes_event.py @@ -0,0 +1,214 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import json +import functools +from mock import patch, call, Mock, PropertyMock +import requests_mock + +import os, sys + +# Hack to load synchronizer framework +test_path=os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +xos_dir=os.path.join(test_path, "../../..") +if not os.path.exists(os.path.join(test_path, "new_base")): + xos_dir=os.path.join(test_path, "../../../../../../orchestration/xos/xos") + services_dir = os.path.join(xos_dir, "../../xos_services") +sys.path.append(xos_dir) +sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) +# END Hack to load synchronizer framework + +# generate model from xproto +def get_models_fn(service_name, xproto_name): + name = os.path.join(service_name, "xos", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + else: + name = os.path.join(service_name, "xos", "synchronizer", "models", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + raise Exception("Unable to find service=%s xproto=%s" % (service_name, xproto_name)) +# END generate model from xproto + +class TestKubernetesEvent(unittest.TestCase): + + def setUp(self): + global DeferredException + + self.sys_path_save = sys.path + sys.path.append(xos_dir) + sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) + + config = os.path.join(test_path, "../test_config.yaml") + from xosconfig import Config + Config.clear() + Config.init(config, 'synchronizer-config-schema.yaml') + + from synchronizers.new_base.mock_modelaccessor_build import build_mock_modelaccessor + build_mock_modelaccessor(xos_dir, services_dir, [ + get_models_fn("olt-service", "volt.xproto"), + get_models_fn("vsg", "vsg.xproto"), + get_models_fn("../profiles/rcord", "rcord.xproto"), + get_models_fn("onos-service", "onos.xproto"), + ]) + + import synchronizers.new_base.mock_modelaccessor + reload(synchronizers.new_base.mock_modelaccessor) # in case nose2 loaded it in a previous test + + import synchronizers.new_base.modelaccessor + reload(synchronizers.new_base.modelaccessor) # in case nose2 loaded it in a previous test + + from synchronizers.new_base.modelaccessor import model_accessor + from mock_modelaccessor import MockObjectList + + from kubernetes_event import KubernetesPodDetailsEventStep + + # import all class names to globals + for (k, v) in model_accessor.all_model_classes.items(): + globals()[k] = v + + self.event_step = KubernetesPodDetailsEventStep + + self.onos = ONOSService(name="myonos", + id=1111, + rest_hostname = "onos-url", + rest_port = "8181", + rest_username = "karaf", + rest_password = "karaf", + backend_code=1, + backend_status="succeeded") + + self.fcservice = VOLTService(name="myoltservice", + id=1112, + backend_code=1, + backend_status="succeeded", + subscriber_services=[self.onos]) + + self.fcsi1 = VOLTServiceInstance(name="myfcsi1", + owner=self.fcservice, + backend_code=1, + backend_status="succeeded") + + self.fcsi2 = VOLTServiceInstance(name="myfcsi2", + owner=self.fcservice, + backend_code=1, + backend_status="succeeded") + + self.fcservice.service_instances = MockObjectList([self.fcsi1, self.fcsi2]) + + self.log = Mock() + + def tearDown(self): + sys.path = self.sys_path_save + + def test_process_event(self): + with patch.object(VOLTService.objects, "get_items") as fcservice_objects, \ + patch.object(Service.objects, "get_items") as service_objects, \ + patch.object(VOLTServiceInstance, "save", autospec=True) as fcsi_save: + fcservice_objects.return_value = [self.fcservice] + service_objects.return_value = [self.onos, self.fcservice] + + event_dict = {"status": "created", + "labels": {"xos_service": "myonos"}} + event = Mock() + event.value = json.dumps(event_dict) + + step = self.event_step(log=self.log) + step.process_event(event) + + self.assertEqual(self.fcsi1.backend_code, 0) + self.assertEqual(self.fcsi1.backend_status, "resynchronize due to kubernetes event") + + self.assertEqual(self.fcsi2.backend_code, 0) + self.assertEqual(self.fcsi2.backend_status, "resynchronize due to kubernetes event") + + fcsi_save.assert_has_calls([call(self.fcsi1, update_fields=["updated", "backend_code", "backend_status"], + always_update_timestamp=True), + call(self.fcsi2, update_fields=["updated", "backend_code", "backend_status"], + always_update_timestamp=True)]) + + def test_process_event_unknownstatus(self): + with patch.object(VOLTService.objects, "get_items") as fcservice_objects, \ + patch.object(Service.objects, "get_items") as service_objects, \ + patch.object(VOLTServiceInstance, "save") as fcsi_save: + fcservice_objects.return_value = [self.fcservice] + service_objects.return_value = [self.onos, self.fcservice] + + event_dict = {"status": "something_else", + "labels": {"xos_service": "myonos"}} + event = Mock() + event.value = json.dumps(event_dict) + + step = self.event_step(log=self.log) + step.process_event(event) + + self.assertEqual(self.fcsi1.backend_code, 1) + self.assertEqual(self.fcsi1.backend_status, "succeeded") + + self.assertEqual(self.fcsi2.backend_code, 1) + self.assertEqual(self.fcsi2.backend_status, "succeeded") + + fcsi_save.assert_not_called() + + def test_process_event_unknownservice(self): + with patch.object(VOLTService.objects, "get_items") as fcservice_objects, \ + patch.object(Service.objects, "get_items") as service_objects, \ + patch.object(VOLTServiceInstance, "save") as fcsi_save: + fcservice_objects.return_value = [self.fcservice] + service_objects.return_value = [self.onos, self.fcservice] + + event_dict = {"status": "created", + "labels": {"xos_service": "something_else"}} + event = Mock() + event.value = json.dumps(event_dict) + + step = self.event_step(log=self.log) + step.process_event(event) + + self.assertEqual(self.fcsi1.backend_code, 1) + self.assertEqual(self.fcsi1.backend_status, "succeeded") + + self.assertEqual(self.fcsi2.backend_code, 1) + self.assertEqual(self.fcsi2.backend_status, "succeeded") + + fcsi_save.assert_not_called() + + def test_process_event_nolabels(self): + with patch.object(VOLTService.objects, "get_items") as fcservice_objects, \ + patch.object(Service.objects, "get_items") as service_objects, \ + patch.object(VOLTServiceInstance, "save") as fcsi_save: + fcservice_objects.return_value = [self.fcservice] + service_objects.return_value = [self.onos, self.fcservice] + + event_dict = {"status": "created"} + event = Mock() + event.value = json.dumps(event_dict) + + step = self.event_step(log=self.log) + step.process_event(event) + + self.assertEqual(self.fcsi1.backend_code, 1) + self.assertEqual(self.fcsi1.backend_status, "succeeded") + + self.assertEqual(self.fcsi2.backend_code, 1) + self.assertEqual(self.fcsi2.backend_status, "succeeded") + + fcsi_save.assert_not_called() + +if __name__ == '__main__': + unittest.main() + + + diff --git a/src/olt-service/xos/synchronizer/helpers.py b/src/olt-service/xos/synchronizer/helpers.py new file mode 100644 index 0000000..cb310af --- /dev/null +++ b/src/olt-service/xos/synchronizer/helpers.py @@ -0,0 +1,45 @@ +# 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. + +class Helpers(): + @staticmethod + def format_url(url): + if 'http' in url: + return url + else: + return 'http://%s' % url + + @staticmethod + def get_voltha_info(olt_service): + return { + 'url': Helpers.format_url(olt_service.voltha_url), + 'port': olt_service.voltha_port, + 'user': olt_service.voltha_user, + 'pass': olt_service.voltha_pass + } + + @staticmethod + def get_onos_voltha_info(olt_service): + return { + 'url': Helpers.format_url(olt_service.onos_voltha_url), + 'port': olt_service.onos_voltha_port, + 'user': olt_service.onos_voltha_user, + 'pass': olt_service.onos_voltha_pass + } + + @staticmethod + def datapath_id_to_hex(id): + if isinstance(id, basestring): + id = int(id) + return "{0:0{1}x}".format(id, 16) \ No newline at end of file diff --git a/src/olt-service/xos/synchronizer/model-deps b/src/olt-service/xos/synchronizer/model-deps new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/src/olt-service/xos/synchronizer/model-deps @@ -0,0 +1 @@ +{} diff --git a/src/olt-service/xos/synchronizer/model_policies/model_policy_voltserviceinstance.py b/src/olt-service/xos/synchronizer/model_policies/model_policy_voltserviceinstance.py new file mode 100644 index 0000000..1321955 --- /dev/null +++ b/src/olt-service/xos/synchronizer/model_policies/model_policy_voltserviceinstance.py @@ -0,0 +1,71 @@ + +# 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 synchronizers.new_base.modelaccessor import VOLTServiceInstance, ServiceInstanceLink, ONUDevice, ServiceInstance, model_accessor +from synchronizers.new_base.policy import Policy + +class VOLTServiceInstancePolicy(Policy): + model_name = "VOLTServiceInstance" + + def handle_create(self, si): + return self.handle_update(si) + + def handle_update(self, si): + + if (si.link_deleted_count > 0) and (not si.provided_links.exists()): + # If this instance has no links pointing to it, delete + self.handle_delete(si) + if VOLTServiceInstance.objects.filter(id=si.id).exists(): + si.delete() + return + + self.create_eastbound_instance(si) + self.associate_onu_device(si) + + def handle_delete(self, si): + pass + + def create_eastbound_instance(self, si): + links = si.owner.subscribed_dependencies.all() + for link in links: + # SEBA-216 prevent any attempt to create an ONOSServiceInstance + if "onos" in link.provider_service.name.lower(): + continue + + provider_service = link.provider_service.leaf_model + + valid_provider_service_instance = provider_service.validate_links(si) + if not valid_provider_service_instance: + provider_service.acquire_service_instance(si) + + def associate_onu_device(self, si): + + self.logger.debug("MODEL_POLICY: attaching ONUDevice to VOLTServiceInstance %s" % si.id) + + base_si = ServiceInstance.objects.get(id=si.id) + try: + onu_device_serial_number = base_si.get_westbound_service_instance_properties("onu_device") + except Exception as e: + raise Exception( + "VOLTServiceInstance %s has no westbound ServiceInstance specifying the onu_device, you need to manually specify it" % self.id) + + try: + onu = ONUDevice.objects.get(serial_number=onu_device_serial_number) + except IndexError: + raise Exception("ONUDevice with serial number %s can't be found" % onu_device_serial_number) + + si.onu_device_id = onu.id + si.save() diff --git a/src/olt-service/xos/synchronizer/model_policies/test_model_policy_voltserviceinstance.py b/src/olt-service/xos/synchronizer/model_policies/test_model_policy_voltserviceinstance.py new file mode 100644 index 0000000..f129de5 --- /dev/null +++ b/src/olt-service/xos/synchronizer/model_policies/test_model_policy_voltserviceinstance.py @@ -0,0 +1,154 @@ + +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import unittest +from mock import patch, call, Mock, PropertyMock + +import os, sys + +test_path=os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +service_dir=os.path.join(test_path, "../../../..") +xos_dir=os.path.join(test_path, "../../..") +if not os.path.exists(os.path.join(test_path, "new_base")): + xos_dir=os.path.join(test_path, "../../../../../../orchestration/xos/xos") + services_dir=os.path.join(xos_dir, "../../xos_services") + +# While transitioning from static to dynamic load, the path to find neighboring xproto files has changed. So check +# both possible locations... +def get_models_fn(service_name, xproto_name): + name = os.path.join(service_name, "xos", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + else: + name = os.path.join(service_name, "xos", "synchronizer", "models", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + raise Exception("Unable to find service=%s xproto=%s" % (service_name, xproto_name)) + +class TestModelPolicyVOLTServiceInstance(unittest.TestCase): + def setUp(self): + global VOLTServiceInstancePolicy, MockObjectList + + self.sys_path_save = sys.path + sys.path.append(xos_dir) + sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) + + config = os.path.join(test_path, "../test_config.yaml") + from xosconfig import Config + Config.clear() + Config.init(config, 'synchronizer-config-schema.yaml') + + from synchronizers.new_base.mock_modelaccessor_build import build_mock_modelaccessor + build_mock_modelaccessor(xos_dir, services_dir, [get_models_fn("olt-service", "volt.xproto"), + get_models_fn("vsg", "vsg.xproto"), + get_models_fn("../profiles/rcord", "rcord.xproto")]) + + import synchronizers.new_base.modelaccessor + import model_policy_voltserviceinstance + from model_policy_voltserviceinstance import VOLTServiceInstancePolicy, model_accessor + + from mock_modelaccessor import MockObjectList + + # import all class names to globals + for (k, v) in model_accessor.all_model_classes.items(): + globals()[k] = v + + # Some of the functions we call have side-effects. For example, creating a VSGServiceInstance may lead to creation of + # tags. Ideally, this wouldn't happen, but it does. So make sure we reset the world. + model_accessor.reset_all_object_stores() + + self.policy = VOLTServiceInstancePolicy() + self.si = Mock() + + def tearDown(self): + sys.path = self.sys_path_save + + def test_handle_create(self): + with patch.object(VOLTServiceInstancePolicy, "create_eastbound_instance") as create_eastbound_instance, \ + patch.object(VOLTServiceInstancePolicy, "associate_onu_device") as associate_onu_device: + + self.policy.handle_create(self.si) + create_eastbound_instance.assert_called_with(self.si) + associate_onu_device.assert_called_with(self.si) + + def test_create_vsg(self): + with patch.object(ServiceInstanceLink, "save", autospec=True) as save_link, \ + patch.object(VSGServiceInstance, "save", autospec=True) as save_vsg: + + subscriber_si = Mock() + + link = Mock() + link.provider_service.get_service_instance_class_name.return_value = "VSGServiceInstance" + link.provider_service.name = "FabricCrossconnect" + link.provider_service.validate_links = Mock(return_value=[]) + link.provider_service.acquire_service_instance = Mock(return_value=subscriber_si) + link.provider_service.leaf_model = link.provider_service + + si = Mock() + si.subscribed_links.all.return_value = [] + si.owner.subscribed_dependencies.all.return_value = [link] + + self.policy.create_eastbound_instance(si) + + link.provider_service.validate_links.assert_called_with(si) + link.provider_service.acquire_service_instance.assert_called_with(si) + + def test_create_vsg_already_exists(self): + with patch.object(ServiceInstanceLink, "save", autospec=True) as save_link, \ + patch.object(VSGServiceInstance, "save", autospec=True) as save_vsg: + + subscriber_si = Mock() + + link = Mock() + link.provider_service.get_service_instance_class_name.return_value = "VSGServiceInstance" + link.provider_service.name = "FabricCrossconnect" + link.provider_service.validate_links = Mock(return_value=subscriber_si) + link.provider_service.acquire_service_instance = Mock() + link.provider_service.leaf_model = link.provider_service + + si = Mock() + si.subscribed_links.all.return_value = [] + si.owner.subscribed_dependencies.all.return_value = [link] + + self.policy.create_eastbound_instance(si) + + link.provider_service.validate_links.assert_called_with(si) + link.provider_service.acquire_service_instance.assert_not_called() + + def test_associate_onu(self): + with patch.object(ServiceInstance.objects, "get") as get_si, \ + patch.object(ONUDevice.objects, "get") as get_onu: + + mock_si = Mock() + mock_si.get_westbound_service_instance_properties.return_value = "BRCM1234" + get_si.return_value = mock_si + + mock_onu = Mock() + mock_onu.id = 12 + get_onu.return_value = mock_onu + + self.policy.associate_onu_device(self.si) + + self.assertEqual(self.si.onu_device_id, mock_onu.id) + self.si.save.assert_called() + + def test_handle_delete(self): + self.policy.handle_delete(self.si) + # handle delete does nothing, and should trivially succeed + +if __name__ == '__main__': + unittest.main() + diff --git a/src/olt-service/xos/synchronizer/models/convenience/voltservice.py b/src/olt-service/xos/synchronizer/models/convenience/voltservice.py new file mode 100644 index 0000000..22f4188 --- /dev/null +++ b/src/olt-service/xos/synchronizer/models/convenience/voltservice.py @@ -0,0 +1,51 @@ +# 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 xosapi.orm import ORMWrapper, register_convenience_wrapper +from xosapi.convenience.service import ORMWrapperService + +import logging as log + + +class ORMWrapperVOLTService(ORMWrapperService): + + def get_onu_sn_from_openflow(self, dp_id, port_no): + """Return the ONU serial number from logical_device informations + + example usage: + volt = VOLTService.objects.first() + sn = volt.get_onu_from_openflow("of:0000000ce2314000", 2) + # BRCM1234 + + Arguments: + dp_id {string} -- The openflow id of the OLT device + port_no {int} -- The openflow port id (UNI Port) + + Returns: + string -- ONU Serial Number + """ + + log.debug("Searching ONUDevice for %s:%s" % (dp_id, port_no)) + try: + olt = self.stub.OLTDevice.objects.get(dp_id=dp_id) + uni_ports = self.stub.UNIPort.objects.filter(port_no=port_no) + onu = [o.onu_device for o in uni_ports if o.onu_device.pon_port.olt_device.id == olt.id][0] + return onu.serial_number + except IndexError: + log.error("Can't find ONU for %s:%s" % (dp_id, port_no)) + except Exception: + log.exception("Error while finding ONUDevice for %s:%s" % (dp_id, port_no)) + + +register_convenience_wrapper("VOLTService", ORMWrapperVOLTService) diff --git a/src/olt-service/xos/synchronizer/models/convenience/voltserviceinstance.py b/src/olt-service/xos/synchronizer/models/convenience/voltserviceinstance.py new file mode 100644 index 0000000..6c59ef6 --- /dev/null +++ b/src/olt-service/xos/synchronizer/models/convenience/voltserviceinstance.py @@ -0,0 +1,68 @@ + +# 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 xosapi.orm import ORMWrapper, register_convenience_wrapper +from xosapi.convenience.serviceinstance import ORMWrapperServiceInstance + +import logging as log + +class ORMWrapperVOLTServiceInstance(ORMWrapperServiceInstance): + + def get_olt_device_by_subscriber(self): + pon_port = self.get_pon_port_by_subscriber() + return pon_port.olt_device + + def get_pon_port_by_subscriber(self): + si = self.stub.ServiceInstance.objects.get(id=self.id) + onu_sn = si.get_westbound_service_instance_properties("onu_device") + onu = self.stub.ONUDevice.objects.get(serial_number=onu_sn) + return onu.pon_port + + @property + def switch_datapath_id(self): + try: + olt_device = self.get_olt_device_by_subscriber() + if olt_device: + return olt_device.switch_datapath_id + return None + except Exception, e: + log.exception('Error while reading switch_datapath_id: %s' % e.message) + return None + + @property + def switch_port(self): + try: + olt_device = self.get_olt_device_by_subscriber() + if olt_device: + return olt_device.switch_port + return None + except Exception, e: + log.exception('Error while reading switch_port: %s' % e.message) + return None + + @property + def outer_tpid(self): + try: + olt_device = self.get_olt_device_by_subscriber() + if olt_device: + return olt_device.outer_tpid + return None + except Exception, e: + log.exception('Error while reading outer_tpid: %s' % e.message) + return None + + +register_convenience_wrapper("VOLTServiceInstance", ORMWrapperVOLTServiceInstance) diff --git a/src/olt-service/xos/synchronizer/models/models.py b/src/olt-service/xos/synchronizer/models/models.py new file mode 100644 index 0000000..4d5b8b8 --- /dev/null +++ b/src/olt-service/xos/synchronizer/models/models.py @@ -0,0 +1,118 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random + +from xos.exceptions import XOSValidationError + +from models_decl import VOLTService_decl +from models_decl import VOLTServiceInstance_decl +from models_decl import OLTDevice_decl +from models_decl import PortBase_decl +from models_decl import PONPort_decl +from models_decl import NNIPort_decl +from models_decl import ONUDevice_decl +from models_decl import PONONUPort_decl +from models_decl import UNIPort_decl + +class VOLTService(VOLTService_decl): + class Meta: + proxy = True + + @staticmethod + def has_access_device(serial_number): + try: + ONUDevice.objects.get(serial_number=serial_number) + return True + except IndexError, e: + return False + +class VOLTServiceInstance(VOLTServiceInstance_decl): + class Meta: + proxy = True + +class OLTDevice(OLTDevice_decl): + class Meta: + proxy = True + + def get_volt_si(self): + return VOLTServiceInstance.objects.all() + + def save(self, *args, **kwargs): + + if (self.host or self.port) and self.mac_address: + raise XOSValidationError("You can't specify both host/port and mac_address for OLTDevice [host=%s, port=%s, mac_address=%s]" % (self.host, self.port, self.mac_address)) + + super(OLTDevice, self).save(*args, **kwargs) + + def delete(self, *args, **kwargs): + + onus = [] + pon_ports = self.pon_ports.all() + for port in pon_ports: + onus = onus + list(port.onu_devices.all()) + + + if len(onus) > 0: + onus = [o.id for o in onus] + + # find the ONUs used by VOLTServiceInstances + used_onus = [o.onu_device_id for o in self.get_volt_si()] + + # find the intersection between the onus associated with this OLT and the used one + used_onus_to_delete = [o for o in onus if o in used_onus] + + if len(used_onus_to_delete) > 0: + if hasattr(self, "device_id") and self.device_id: + item = self.device_id + elif hasattr(self, "name") and self.name: + item = self.name + else: + item = self.id + raise XOSValidationError('OLT "%s" can\'t be deleted as it has subscribers associated with its ONUs' % item) + + super(OLTDevice, self).delete(*args, **kwargs) + +class PortBase(PortBase_decl): + class Meta: + proxy = True + +class PONPort(PONPort_decl): + class Meta: + proxy = True + +class NNIPort(NNIPort_decl): + class Meta: + proxy = True + + +class ONUDevice(ONUDevice_decl): + class Meta: + proxy = True + + def delete(self, *args, **kwargs): + + if len(self.volt_service_instances.all()) > 0: + raise XOSValidationError('ONU "%s" can\'t be deleted as it has subscribers associated with it' % self.serial_number) + + super(ONUDevice, self).delete(*args, **kwargs) + +class PONONUPort(PONONUPort_decl): + class Meta: + proxy = True + +class UNIPort(UNIPort_decl): + class Meta: + proxy = True + diff --git a/src/olt-service/xos/synchronizer/models/test_models.py b/src/olt-service/xos/synchronizer/models/test_models.py new file mode 100644 index 0000000..92a939b --- /dev/null +++ b/src/olt-service/xos/synchronizer/models/test_models.py @@ -0,0 +1,131 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from mock import patch, Mock, MagicMock + +# mocking XOS exception, as they're based in Django +class Exceptions: + XOSValidationError = Exception + +class XOS: + exceptions = Exceptions + +class TestOLTDeviceModel(unittest.TestCase): + def setUp(self): + self.xos = XOS + + self.models_decl = Mock() + self.models_decl.OLTDevice_decl = MagicMock + self.models_decl.OLTDevice_decl.delete = Mock() + + modules = { + 'xos.exceptions': self.xos.exceptions, + 'models_decl': self.models_decl, + } + + self.module_patcher = patch.dict('sys.modules', modules) + self.module_patcher.start() + + from models import OLTDevice + + self.olt_device = OLTDevice() + self.olt_device.id = None # this is a new model + self.olt_device.is_new = True + self.olt_device.device_id = 1234 + + def tearDown(self): + self.module_patcher.stop() + + def test_create_mac_address(self): + from models import OLTDevice + olt = OLTDevice() + + olt.host = "1.1.1.1" + olt.port = "9101" + olt.mac_address = "00:0c:d5:00:05:40" + + with self.assertRaises(Exception) as e: + olt.save() + + self.assertEqual(e.exception.message, + "You can't specify both host/port and mac_address for OLTDevice [host=%s, port=%s, mac_address=%s]" % (olt.host, olt.port, olt.mac_address)) + + def test_delete(self): + self.olt_device.delete() + self.models_decl.OLTDevice_decl.delete.assert_called() + + def test_prevent_delete(self): + + onu1 = Mock() + onu1.id = 1 + + pon1 = Mock() + pon1.onu_devices.all.return_value = [onu1] + + self.olt_device.pon_ports.all.return_value = [pon1] + + volt_si_1 = Mock() + volt_si_1.onu_device_id = onu1.id + + with patch.object(self.olt_device, "get_volt_si")as volt_si_get: + volt_si_get.return_value = [volt_si_1] + with self.assertRaises(Exception) as e: + self.olt_device.delete() + + self.assertEqual(e.exception.message, 'OLT "1234" can\'t be deleted as it has subscribers associated with its ONUs') + self.models_decl.OLTDevice_decl.delete.assert_not_called() + +class TestONUDeviceModel(unittest.TestCase): + + def setUp(self): + self.xos = XOS + + self.models_decl = Mock() + self.models_decl.ONUDevice_decl = MagicMock + self.models_decl.ONUDevice_decl.delete = Mock() + + modules = { + 'xos.exceptions': self.xos.exceptions, + 'models_decl': self.models_decl, + } + + self.module_patcher = patch.dict('sys.modules', modules) + self.module_patcher.start() + + from models import ONUDevice + + self.onu_device = ONUDevice() + self.onu_device.id = None # this is a new model + self.onu_device.is_new = True + self.onu_device.serial_number = 1234 + + def test_delete(self): + self.onu_device.delete() + self.models_decl.ONUDevice_decl.delete.assert_called() + + def test_prevent_delete(self): + volt_si_1 = Mock() + volt_si_1.onu_device_id = self.onu_device.id + self.onu_device.volt_service_instances.all.return_value = [volt_si_1] + + with self.assertRaises(Exception) as e: + self.onu_device.delete() + + self.assertEqual(e.exception.message, + 'ONU "1234" can\'t be deleted as it has subscribers associated with it') + self.models_decl.OLTDevice_decl.delete.assert_not_called() + +if __name__ == '__main__': + unittest.main() diff --git a/src/olt-service/xos/synchronizer/models/volt.xproto b/src/olt-service/xos/synchronizer/models/volt.xproto new file mode 100644 index 0000000..927006f --- /dev/null +++ b/src/olt-service/xos/synchronizer/models/volt.xproto @@ -0,0 +1,102 @@ +option name = "volt"; +option app_label = "volt"; +option legacy="True"; + +message VOLTService (Service){ + option verbose_name = "vOLT Service"; + option kind = "vOLT"; + + required string voltha_url = 1 [help_text = "The Voltha API address. By default voltha.voltha.svc.cluster.local", default = "voltha.voltha.svc.cluster.local", max_length = 254, db_index = False]; + required int32 voltha_port = 2 [help_text = "The Voltha API port. By default 8882", default=8882, db_index = False]; + required string voltha_user = 3 [help_text = "The Voltha username. By default voltha", default="voltha", max_length = 254, db_index = False]; + required string voltha_pass = 4 [help_text = "The Voltha password. By default admin", default="admin", max_length = 254, db_index = False]; + required string onos_voltha_url = 5 [help_text = "The ONOS Voltha address. By default onos-voltha-ui.voltha.svc.cluster.local", default="onos-voltha-ui.voltha.svc.cluster.local", max_length = 254, db_index = False]; + required int32 onos_voltha_port = 6 [help_text = "The Voltha API port. By default 8181", default=8181, db_index = False]; + required string onos_voltha_user = 7 [help_text = "The ONOS Voltha username. By default sdn", max_length = 254, default="onos", db_index = False]; + required string onos_voltha_pass = 8 [help_text = "The ONOS Voltha password. By default rocks", max_length = 254, default="rocks", db_index = False]; +} + +message OLTDevice (XOSBase){ + option verbose_name = "OLT Device"; + option description="Represents a physical OLT device"; + + required manytoone volt_service->VOLTService:volt_devices = 1:1001 [db_index = True]; + optional string name = 2 [help_text = "name of device", max_length = 254, db_index = False, unique = True]; + required string device_type = 3 [help_text = "Device Type", default = "openolt", max_length = 254, db_index = False]; + optional string host = 4 [help_text = "Device IP", max_length = 254, db_index = False]; + optional int32 port = 5 [help_text = "Device port", db_index = False, unique_with = "host"]; + optional string mac_address = 6 [help_text = "Device mac address", db_index = False]; + + optional string serial_number = 9 [help_text = "Serial Number", db_index = False]; + optional string device_id = 10 [help_text = "Device ID", db_index = False, feedback_state = True]; + optional string admin_state = 11 [help_text = "admin_state", db_index = False, feedback_state = True]; + optional string oper_status = 12 [help_text = "oper_status", db_index = False, feedback_state = True]; + optional string of_id = 13 [help_text = "openflow id", db_index = False, feedback_state = True]; + optional string dp_id = 14 [help_text = "datapath id", db_index = False]; + + required string uplink = 15 [help_text = "uplink port", db_index = False]; + required string driver = 16 [default="voltha", help_text = "Olt driver", db_index = False]; + + optional string switch_datapath_id = 17 [help_text = "Fabric switch to which the OLT is connected", db_index = False]; + optional string switch_port = 18 [help_text = "Fabric port to which the OLT is connected", db_index = False]; + optional string outer_tpid = 19 [help_text = "Outer VLAN id field EtherType", db_index = False]; + + optional string nas_id = 20 [help_text = "Authentication ID (propagated to the free-radius server via sadis)", db_index = False]; +} + +message PortBase (XOSBase){ + option gui_hidden = True; + + required string name = 1 [db_index = True]; + required int32 port_no = 3 [help_text = "Port ID", db_index = False]; + + optional string admin_state = 4 [help_text = "admin_state", db_index = False, feedback_state = True]; + optional string oper_status = 5 [help_text = "oper_status", db_index = False, feedback_state = True]; +} + +message PONPort (PortBase){ + option verbose_name = "PON Port"; + option description="PON Port"; + + required manytoone olt_device->OLTDevice:pon_ports = 1:1001 [db_index = True]; +} + +message NNIPort (PortBase) { + option verbose_name = "NNI Port"; + required manytoone olt_device->OLTDevice:nni_ports = 1:1002 [db_index = True]; +} + +message ONUDevice (XOSBase){ + option verbose_name = "ONU Device"; + option description = "Represents a physical ONU device"; + + required manytoone pon_port->PONPort:onu_devices = 1:1001 [db_index = True]; + required string serial_number = 2 [max_length = 254, db_index = False, tosca_key=True, unique = True]; + required string vendor = 3 [max_length = 254, db_index = False]; + required string device_type = 4 [help_text = "Device Type", default = "asfvolt16_olt", max_length = 254, db_index = False]; + + optional string device_id = 5 [max_length = 254, db_index = False, feedback_state = True]; + optional string admin_state = 6 [choices = "(('DISABLED', 'DISABLED'), ('ENABLED', 'ENABLED'))", default="ENABLED", help_text = "admin_state", db_index = False]; + optional string oper_status = 7 [help_text = "oper_status", db_index = False, feedback_state = True]; + optional string connect_status = 8 [help_text = "connect_status", db_index = False, feedback_state = True]; +} + +message PONONUPort (PortBase) { + option verbose_name = "ANI Port"; + option description="ANI Port"; + required manytoone onu_device->ONUDevice:pononu_ports = 1:1001 [db_index = True]; +} + +message UNIPort (PortBase) { + option verbose_name = "UNI Port"; + required manytoone onu_device->ONUDevice:uni_ports = 1:1002 [db_index = True]; +} + +message VOLTServiceInstance (ServiceInstance){ + option kind = "vOLT"; + option owner_class_name = "VOLTService"; + option verbose_name = "vOLT Service Instance"; + + optional string description = 1 [max_length = 254, db_index = False]; + optional manytoone onu_device->ONUDevice:volt_service_instances = 2:1003 [db_index = True]; +} diff --git a/src/olt-service/xos/synchronizer/pull_steps/pull_olts.py b/src/olt-service/xos/synchronizer/pull_steps/pull_olts.py new file mode 100644 index 0000000..30afbd4 --- /dev/null +++ b/src/olt-service/xos/synchronizer/pull_steps/pull_olts.py @@ -0,0 +1,263 @@ +# 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 synchronizers.new_base.pullstep import PullStep +from synchronizers.new_base.modelaccessor import model_accessor, OLTDevice, VOLTService, PONPort, NNIPort + +from xosconfig import Config +from multistructlog import create_logger + +import requests +from requests import ConnectionError +from requests.models import InvalidURL + +import os, sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from helpers import Helpers + +log = create_logger(Config().get('logging')) + +class OLTDevicePullStep(PullStep): + def __init__(self): + super(OLTDevicePullStep, self).__init__(observed_model=OLTDevice) + + @staticmethod + def get_ids_from_logical_device(o): + voltha_url = Helpers.get_voltha_info(o.volt_service)['url'] + voltha_port = Helpers.get_voltha_info(o.volt_service)['port'] + + r = requests.get("%s:%s/api/v1/logical_devices" % (voltha_url, voltha_port), timeout=1) + + if r.status_code != 200: + raise Exception("Failed to retrieve logical devices from VOLTHA: %s" % r.text) + + res = r.json() + + for ld in res["items"]: + if ld["root_device_id"] == o.device_id: + o.of_id = ld["id"] + o.dp_id = "of:" + Helpers.datapath_id_to_hex(ld["datapath_id"]) # convert to hex + return o + + raise Exception("Can't find a logical device for device id: %s" % o.device_id) + + def pull_records(self): + log.debug("[OLT pull step] pulling OLT devices from VOLTHA") + + try: + self.volt_service = VOLTService.objects.all()[0] + except IndexError: + log.warn('VOLTService not found') + return + + voltha_url = Helpers.get_voltha_info(self.volt_service)['url'] + voltha_port = Helpers.get_voltha_info(self.volt_service)['port'] + + try: + r = requests.get("%s:%s/api/v1/devices" % (voltha_url, voltha_port), timeout=1) + + if r.status_code != 200: + log.debug("[OLT pull step] It was not possible to fetch devices from VOLTHA") + + # keeping only OLTs + devices = [d for d in r.json()["items"] if "olt" in d["type"]] + + log.trace("[OLT pull step] received devices", olts=devices) + + olts_in_voltha = self.create_or_update_olts(devices) + + self.delete_olts(olts_in_voltha) + + + except ConnectionError, e: + log.warn("[OLT pull step] It was not possible to connect to VOLTHA", reason=e) + return + except InvalidURL, e: + log.warn("[OLT pull step] VOLTHA url is invalid, is it configured in the VOLTService?", reason=e) + return + + def create_or_update_olts(self, olts): + + updated_olts = [] + + for olt in olts: + if olt["type"] == "simulated_olt": + [host, port] = ["172.17.0.1", "50060"] + else: + [host, port] = olt["host_and_port"].split(":") + + olt_ports = self.fetch_olt_ports(olt["id"]) + + try: + model = OLTDevice.objects.filter(device_type=olt["type"], host=host, port=port)[0] + + log.trace("[OLT pull step] OLTDevice already exists, updating it", device_type=olt["type"], host=host, port=port) + + if model.enacted < model.updated: + log.debug("[OLT pull step] Skipping pull on OLTDevice %s as enacted < updated" % model.name, name=model.name, id=model.id, enacted=model.enacted, updated=model.updated) + # if we are not updating the device we still need to pull ports + if olt_ports: + self.create_or_update_ports(olt_ports, model) + updated_olts.append(model) + continue + + except IndexError: + + model = OLTDevice() + model.device_type = olt["type"] + + if olt["type"] == "simulated_olt": + model.host = "172.17.0.1" + model.port = 50060 + else: + [host, port] = olt["host_and_port"].split(":") + model.host = host + model.port = int(port) + + # there's no name in voltha, so make one up based on the id + model.name = "OLT-%s" % olt["id"] + + nni_ports = [p for p in olt_ports if "ETHERNET_NNI" in p["type"]] + if not nni_ports: + log.warning("[OLT pull step] No NNI ports, so no way to determine uplink. Skipping.", device_type=olt["type"], host=host, port=port) + continue + + # Exctract uplink from the first NNI port. This decision is arbitrary, we will worry about multiple + # NNI ports when that situation arises. + model.uplink = str(nni_ports[0]["port_no"]) + + log.debug("[OLT pull step] OLTDevice is new, creating it", device_type=olt["type"], host=host, port=port) + + # Adding feedback state to the device + model.device_id = olt["id"] + model.admin_state = olt["admin_state"] + model.oper_status = olt["oper_status"] + model.serial_number = olt['serial_number'] + + model.volt_service = self.volt_service + model.volt_service_id = self.volt_service.id + + # get logical device + OLTDevicePullStep.get_ids_from_logical_device(model) + + model.save() + + if olt_ports: + self.create_or_update_ports(olt_ports, model) + + updated_olts.append(model) + + return updated_olts + + def fetch_olt_ports(self, olt_device_id): + """ Given an olt device_id, query voltha for the set of ports associated with that OLT. + + Returns a list of port dictionaries, or None in case of error. + """ + + voltha_url = Helpers.get_voltha_info(self.volt_service)['url'] + voltha_port = Helpers.get_voltha_info(self.volt_service)['port'] + + try: + r = requests.get("%s:%s/api/v1/devices/%s/ports" % (voltha_url, voltha_port, olt_device_id), timeout=1) + + if r.status_code != 200: + log.warn("[OLT pull step] It was not possible to fetch ports from VOLTHA for device %s" % olt_device_id, + status_code=r.status_code) + return None + + ports = r.json()['items'] + + log.trace("[OLT pull step] received ports", ports=ports, olt=olt_device_id) + + return ports + + except ConnectionError, e: + log.warn("[OLT pull step] It was not possible to connect to VOLTHA", reason=e) + return None + except InvalidURL, e: + log.warn("[OLT pull step] VOLTHA url is invalid, is it configured in the VOLTService?", reason=e) + return None + + return None + + def create_or_update_ports(self, ports, olt): + nni_ports = [p for p in ports if "ETHERNET_NNI" in p["type"]] + pon_ports = [p for p in ports if "PON_OLT" in p["type"]] + + self.create_or_update_nni_port(nni_ports, olt) + self.create_or_update_pon_port(pon_ports, olt) + + def create_or_update_pon_port(self, pon_ports, olt): + + update_ports = [] + + for port in pon_ports: + try: + model = PONPort.objects.filter(port_no=port["port_no"], olt_device_id=olt.id)[0] + log.trace("[OLT pull step] PONPort already exists, updating it", port_no=port["port_no"], olt_device_id=olt.id) + except IndexError: + model = PONPort() + model.port_no = port["port_no"] + model.olt_device_id = olt.id + model.name = port["label"] + log.debug("[OLT pull step] PONPort is new, creating it", port_no=port["port_no"], olt_device_id=olt.id) + + model.admin_state = port["admin_state"] + model.oper_status = port["oper_status"] + model.save() + update_ports.append(model) + return update_ports + + def create_or_update_nni_port(self, nni_ports, olt): + update_ports = [] + + for port in nni_ports: + try: + model = NNIPort.objects.filter(port_no=port["port_no"], olt_device_id=olt.id)[0] + model.xos_managed = False + log.trace("[OLT pull step] NNIPort already exists, updating it", port_no=port["port_no"], olt_device_id=olt.id) + except IndexError: + model = NNIPort() + model.port_no = port["port_no"] + model.olt_device_id = olt.id + model.name = port["label"] + model.xos_managed = False + log.debug("[OLT pull step] NNIPort is new, creating it", port_no=port["port_no"], olt_device_id=olt.id) + + model.admin_state = port["admin_state"] + model.oper_status = port["oper_status"] + model.save() + update_ports.append(model) + return update_ports + + def delete_olts(self, olts_in_voltha): + + olts_id_in_voltha = [m.device_id for m in olts_in_voltha] + + xos_olts = OLTDevice.objects.all() + + deleted_in_voltha = [o for o in xos_olts if o.device_id not in olts_id_in_voltha] + + for model in deleted_in_voltha: + + if model.enacted < model.updated: + # DO NOT delete a model that is being processed + log.debug("[OLT pull step] device is not present in VOLTHA, skipping deletion as sync is in progress", device_id=o.device_id, + name=o.name) + continue + + log.debug("[OLT pull step] deleting device as it's not present in VOLTHA", device_id=o.device_id, name=o.name) + model.delete() diff --git a/src/olt-service/xos/synchronizer/pull_steps/pull_onus.py b/src/olt-service/xos/synchronizer/pull_steps/pull_onus.py new file mode 100644 index 0000000..18f047c --- /dev/null +++ b/src/olt-service/xos/synchronizer/pull_steps/pull_onus.py @@ -0,0 +1,226 @@ +# 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 synchronizers.new_base.pullstep import PullStep +from synchronizers.new_base.modelaccessor import model_accessor, ONUDevice, VOLTService, OLTDevice, PONPort, PONONUPort, UNIPort + +from xosconfig import Config +from multistructlog import create_logger + +import requests +from requests import ConnectionError +from requests.models import InvalidURL + +import os, sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from helpers import Helpers + +log = create_logger(Config().get('logging')) + +class ONUDevicePullStep(PullStep): + def __init__(self): + super(ONUDevicePullStep, self).__init__(observed_model=ONUDevice) + + def pull_records(self): + log.debug("pulling ONU devices from VOLTHA") + + try: + self.volt_service = VOLTService.objects.all()[0] + except IndexError: + log.warn('VOLTService not found') + return + + voltha_url = Helpers.get_voltha_info(self.volt_service)['url'] + voltha_port = Helpers.get_voltha_info(self.volt_service)['port'] + + try: + r = requests.get("%s:%s/api/v1/devices" % (voltha_url, voltha_port), timeout=1) + + if r.status_code != 200: + log.warn("It was not possible to fetch devices from VOLTHA") + + # keeping only ONUs + devices = [d for d in r.json()["items"] if "onu" in d["type"]] + + log.trace("received devices", onus=devices) + + # TODO + # [ ] delete ONUS as ONUDevice.objects.all() - updated ONUs + + onus_in_voltha = self.create_or_update_onus(devices) + + except ConnectionError, e: + log.warn("It was not possible to connect to VOLTHA", reason=e) + return + except InvalidURL, e: + log.warn("VOLTHA url is invalid, is it configured in the VOLTService?", reason=e) + return + + def create_or_update_onus(self, onus): + + updated_onus = [] + + for onu in onus: + try: + + model = ONUDevice.objects.filter(serial_number=onu["serial_number"])[0] + log.trace("ONUDevice already exists, updating it", serial_number=onu["serial_number"]) + + if model.enacted < model.updated: + log.debug("Skipping pull on ONUDevice %s as enacted < updated" % model.serial_number, serial_number=model.serial_number, id=model.id, enacted=model.enacted, updated=model.updated) + # if we are not updating the device we still need to pull ports + self.fetch_onu_ports(model) + continue + + except IndexError: + model = ONUDevice() + model.serial_number = onu["serial_number"] + + log.debug("ONUDevice is new, creating it", serial_number=onu["serial_number"]) + + try: + olt = OLTDevice.objects.get(device_id=onu["parent_id"]) + except IndexError: + log.warning("Unable to find olt for ONUDevice", serial_number=onu["serial_number"], olt_device_id=onu["parent_id"]) + continue + + try: + pon_port = PONPort.objects.get(port_no=onu["parent_port_no"], olt_device_id=olt.id) + except IndexError: + log.warning("Unable to find pon_port for ONUDevice", serial_number=onu["serial_number"], olt_device_id=onu["parent_id"], port_no=onu["parent_port_no"]) + continue + + # Adding feedback state to the device + model.vendor = onu["vendor"] + model.device_type = onu["type"] + model.device_id = onu["id"] + + model.admin_state = onu["admin_state"] + model.oper_status = onu["oper_status"] + model.connect_status = onu["connect_status"] + model.xos_managed = False + + model.pon_port = pon_port + model.pon_port_id = pon_port.id + + model.save() + + self.fetch_onu_ports(model) + + updated_onus.append(model) + + return updated_onus + + def fetch_onu_ports(self, onu): + voltha_url = Helpers.get_voltha_info(self.volt_service)['url'] + voltha_port = Helpers.get_voltha_info(self.volt_service)['port'] + + try: + r = requests.get("%s:%s/api/v1/devices/%s/ports" % (voltha_url, voltha_port, onu.device_id), timeout=1) + + if r.status_code != 200: + log.warn("It was not possible to fetch ports from VOLTHA for ONUDevice %s" % onu.device_id) + + ports = r.json()['items'] + + log.trace("received ports", ports=ports, onu=onu.device_id) + + self.create_or_update_ports(ports, onu) + + except ConnectionError, e: + log.warn("It was not possible to connect to VOLTHA", reason=e) + return + except InvalidURL, e: + log.warn("VOLTHA url is invalid, is it configured in the VOLTService?", reason=e) + return + return + + def create_or_update_ports(self, ports, onu): + uni_ports = [p for p in ports if "ETHERNET_UNI" in p["type"]] + pon_onu_ports = [p for p in ports if "PON_ONU" in p["type"]] + + self.create_or_update_uni_port(uni_ports, onu) + self.create_or_update_pon_onu_port(pon_onu_ports, onu) + + def get_onu_port_id(self, port, onu): + # find the correct port id as represented in the logical_device + logical_device_id = onu.pon_port.olt_device.of_id + + voltha_url = Helpers.get_voltha_info(self.volt_service)['url'] + voltha_port = Helpers.get_voltha_info(self.volt_service)['port'] + + try: + r = requests.get("%s:%s/api/v1/logical_devices/%s/ports" % (voltha_url, voltha_port, logical_device_id), timeout=1) + + if r.status_code != 200: + log.warn("It was not possible to fetch ports from VOLTHA for logical_device %s" % logical_device_id) + + logical_ports = r.json()['items'] + log.trace("logical device ports for ONUDevice %s" % onu.device_id, logical_ports=logical_ports) + + ports = [p['ofp_port']['port_no'] for p in logical_ports if p['device_id'] == onu.device_id] + # log.debug("Port_id for port %s on ONUDevice %s: %s" % (port['label'], onu.device_id, ports), logical_ports=logical_ports) + # FIXME if this throws an error ONUs from other OTLs are not sync'ed + return int(ports[0]) + + except ConnectionError, e: + log.warn("It was not possible to connect to VOLTHA", reason=e) + return + except InvalidURL, e: + log.warn("VOLTHA url is invalid, is it configured in the VOLTService?", reason=e) + return + + def create_or_update_uni_port(self, uni_ports, onu): + update_ports = [] + + for port in uni_ports: + port_no = self.get_onu_port_id(port, onu) + try: + model = UNIPort.objects.filter(port_no=port_no, onu_device_id=onu.id)[0] + log.trace("UNIPort already exists, updating it", port_no=port_no, onu_device_id=onu.id) + except IndexError: + model = UNIPort() + model.port_no = port_no + model.onu_device_id = onu.id + model.name = port["label"] + log.debug("UNIPort is new, creating it", port_no=port["port_no"], onu_device_id=onu.id) + + model.admin_state = port["admin_state"] + model.oper_status = port["oper_status"] + model.save() + update_ports.append(model) + return update_ports + + def create_or_update_pon_onu_port(self, pon_onu_ports, onu): + update_ports = [] + + for port in pon_onu_ports: + try: + model = PONONUPort.objects.filter(port_no=port["port_no"], onu_device_id=onu.id)[0] + model.xos_managed = False + log.trace("PONONUPort already exists, updating it", port_no=port["port_no"], onu_device_id=onu.id) + except IndexError: + model = PONONUPort() + model.port_no = port["port_no"] + model.onu_device_id = onu.id + model.name = port["label"] + model.xos_managed = False + log.debug("PONONUPort is new, creating it", port_no=port["port_no"], onu_device_id=onu.id) + + model.admin_state = port["admin_state"] + model.oper_status = port["oper_status"] + model.save() + update_ports.append(model) + return update_ports diff --git a/src/olt-service/xos/synchronizer/pull_steps/test_pull_olts.py b/src/olt-service/xos/synchronizer/pull_steps/test_pull_olts.py new file mode 100644 index 0000000..a06d3ff --- /dev/null +++ b/src/olt-service/xos/synchronizer/pull_steps/test_pull_olts.py @@ -0,0 +1,241 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from mock import patch, call, Mock, PropertyMock +import requests_mock + +import os, sys + +# Hack to load synchronizer framework +test_path=os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +xos_dir=os.path.join(test_path, "../../..") +if not os.path.exists(os.path.join(test_path, "new_base")): + xos_dir=os.path.join(test_path, "../../../../../../orchestration/xos/xos") + services_dir = os.path.join(xos_dir, "../../xos_services") +sys.path.append(xos_dir) +sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) +# END Hack to load synchronizer framework + +# generate model from xproto +def get_models_fn(service_name, xproto_name): + name = os.path.join(service_name, "xos", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + else: + name = os.path.join(service_name, "xos", "synchronizer", "models", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + raise Exception("Unable to find service=%s xproto=%s" % (service_name, xproto_name)) +# END generate model from xproto + +class TestSyncOLTDevice(unittest.TestCase): + + def setUp(self): + global DeferredException + + self.sys_path_save = sys.path + sys.path.append(xos_dir) + sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) + + # Setting up the config module + from xosconfig import Config + config = os.path.join(test_path, "../test_config.yaml") + Config.clear() + Config.init(config, "synchronizer-config-schema.yaml") + # END Setting up the config module + + from synchronizers.new_base.mock_modelaccessor_build import build_mock_modelaccessor + # build_mock_modelaccessor(xos_dir, services_dir, [get_models_fn("olt-service", "volt.xproto")]) + + # FIXME this is to get jenkins to pass the tests, somehow it is running tests in a different order + # and apparently it is not overriding the generated model accessor + build_mock_modelaccessor(xos_dir, services_dir, [get_models_fn("olt-service", "volt.xproto"), + get_models_fn("vsg", "vsg.xproto"), + get_models_fn("../profiles/rcord", "rcord.xproto")]) + import synchronizers.new_base.modelaccessor + from pull_olts import OLTDevicePullStep, model_accessor + + # import all class names to globals + for (k, v) in model_accessor.all_model_classes.items(): + globals()[k] = v + + self.sync_step = OLTDevicePullStep + + # mock volt service + self.volt_service = Mock() + self.volt_service.id = "volt_service_id" + self.volt_service.voltha_url = "voltha_url" + self.volt_service.voltha_user = "voltha_user" + self.volt_service.voltha_pass = "voltha_pass" + self.volt_service.voltha_port = 1234 + + # mock voltha responses + self.devices = { + "items": [ + { + "id": "test_id", + "type": "simulated_olt", + "host_and_port": "172.17.0.1:50060", + "admin_state": "ENABLED", + "oper_status": "ACTIVE", + "serial_number": "serial_number", + } + ] + } + + self.logical_devices = { + "items": [ + { + "root_device_id": "test_id", + "id": "of_id", + "datapath_id": "55334486016" + } + ] + } + + self.ports = { + "items": [ + { + "label": "PON port", + "port_no": 1, + "type": "PON_OLT", + "admin_state": "ENABLED", + "oper_status": "ACTIVE" + }, + { + "label": "NNI facing Ethernet port", + "port_no": 2, + "type": "ETHERNET_NNI", + "admin_state": "ENABLED", + "oper_status": "ACTIVE" + } + ] + } + + def tearDown(self): + sys.path = self.sys_path_save + + @requests_mock.Mocker() + def test_missing_volt_service(self, m): + self.assertFalse(m.called) + + @requests_mock.Mocker() + def test_pull(self, m): + + with patch.object(VOLTService.objects, "all") as olt_service_mock, \ + patch.object(OLTDevice, "save") as mock_olt_save, \ + patch.object(PONPort, "save") as mock_pon_save, \ + patch.object(NNIPort, "save") as mock_nni_save: + olt_service_mock.return_value = [self.volt_service] + + m.get("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.devices) + m.get("http://voltha_url:1234/api/v1/devices/test_id/ports", status_code=200, json=self.ports) + m.get("http://voltha_url:1234/api/v1/logical_devices", status_code=200, json=self.logical_devices) + + self.sync_step().pull_records() + + # TODO how to asster this? + # self.assertEqual(existing_olt.admin_state, "ENABLED") + # self.assertEqual(existing_olt.oper_status, "ACTIVE") + # self.assertEqual(existing_olt.volt_service_id, "volt_service_id") + # self.assertEqual(existing_olt.device_id, "test_id") + # self.assertEqual(existing_olt.of_id, "of_id") + # self.assertEqual(existing_olt.dp_id, "of:0000000ce2314000") + + mock_olt_save.assert_called() + mock_pon_save.assert_called() + mock_nni_save.assert_called() + + @requests_mock.Mocker() + def test_pull_existing(self, m): + + existing_olt = Mock() + existing_olt.enacted = 2 + existing_olt.updated = 1 + + with patch.object(VOLTService.objects, "all") as olt_service_mock, \ + patch.object(OLTDevice.objects, "filter") as mock_get, \ + patch.object(PONPort, "save") as mock_pon_save, \ + patch.object(NNIPort, "save") as mock_nni_save, \ + patch.object(existing_olt, "save") as mock_olt_save: + olt_service_mock.return_value = [self.volt_service] + mock_get.return_value = [existing_olt] + + m.get("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.devices) + m.get("http://voltha_url:1234/api/v1/devices/test_id/ports", status_code=200, json=self.ports) + m.get("http://voltha_url:1234/api/v1/logical_devices", status_code=200, json=self.logical_devices) + + self.sync_step().pull_records() + + self.assertEqual(existing_olt.admin_state, "ENABLED") + self.assertEqual(existing_olt.oper_status, "ACTIVE") + self.assertEqual(existing_olt.volt_service_id, "volt_service_id") + self.assertEqual(existing_olt.device_id, "test_id") + self.assertEqual(existing_olt.of_id, "of_id") + self.assertEqual(existing_olt.dp_id, "of:0000000ce2314000") + + mock_olt_save.assert_called() + mock_pon_save.assert_called() + mock_nni_save.assert_called() + + @requests_mock.Mocker() + def test_pull_existing_do_not_sync(self, m): + existing_olt = Mock() + existing_olt.enacted = 1 + existing_olt.updated = 2 + existing_olt.device_id = "test_id" + + with patch.object(VOLTService.objects, "all") as olt_service_mock, \ + patch.object(OLTDevice.objects, "filter") as mock_get, \ + patch.object(PONPort, "save") as mock_pon_save, \ + patch.object(NNIPort, "save") as mock_nni_save, \ + patch.object(existing_olt, "save") as mock_olt_save: + + olt_service_mock.return_value = [self.volt_service] + mock_get.return_value = [existing_olt] + + m.get("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.devices) + m.get("http://voltha_url:1234/api/v1/devices/test_id/ports", status_code=200, json=self.ports) + m.get("http://voltha_url:1234/api/v1/logical_devices", status_code=200, json=self.logical_devices) + + self.sync_step().pull_records() + + mock_olt_save.assert_not_called() + mock_pon_save.assert_called() + mock_nni_save.assert_called() + + @requests_mock.Mocker() + def test_pull_deleted_object(self, m): + existing_olt = Mock() + existing_olt.enacted = 2 + existing_olt.updated = 1 + existing_olt.device_id = "test_id" + + m.get("http://voltha_url:1234/api/v1/devices", status_code=200, json={"items": []}) + + with patch.object(VOLTService.objects, "all") as olt_service_mock, \ + patch.object(OLTDevice.objects, "get_items") as mock_get, \ + patch.object(existing_olt, "delete") as mock_olt_delete: + + olt_service_mock.return_value = [self.volt_service] + mock_get.return_value = [existing_olt] + + self.sync_step().pull_records() + + mock_olt_delete.assert_called() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/olt-service/xos/synchronizer/pull_steps/test_pull_onus.py b/src/olt-service/xos/synchronizer/pull_steps/test_pull_onus.py new file mode 100644 index 0000000..6fc5816 --- /dev/null +++ b/src/olt-service/xos/synchronizer/pull_steps/test_pull_onus.py @@ -0,0 +1,277 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +import unittest +from mock import patch, call, Mock, PropertyMock +import requests_mock + +import os, sys + +# Hack to load synchronizer framework +test_path=os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +xos_dir=os.path.join(test_path, "../../..") +if not os.path.exists(os.path.join(test_path, "new_base")): + xos_dir=os.path.join(test_path, "../../../../../../orchestration/xos/xos") + services_dir = os.path.join(xos_dir, "../../xos_services") +sys.path.append(xos_dir) +sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) +# END Hack to load synchronizer framework + +# generate model from xproto +def get_models_fn(service_name, xproto_name): + name = os.path.join(service_name, "xos", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + else: + name = os.path.join(service_name, "xos", "synchronizer", "models", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + raise Exception("Unable to find service=%s xproto=%s" % (service_name, xproto_name)) +# END generate model from xproto + +class TestPullONUDevice(unittest.TestCase): + + def setUp(self): + global DeferredException + + self.sys_path_save = sys.path + sys.path.append(xos_dir) + sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) + + # Setting up the config module + from xosconfig import Config + config = os.path.join(test_path, "../test_config.yaml") + Config.clear() + Config.init(config, "synchronizer-config-schema.yaml") + # END Setting up the config module + + from synchronizers.new_base.mock_modelaccessor_build import build_mock_modelaccessor + # build_mock_modelaccessor(xos_dir, services_dir, [get_models_fn("olt-service", "volt.xproto")]) + + # FIXME this is to get jenkins to pass the tests, somehow it is running tests in a different order + # and apparently it is not overriding the generated model accessor + build_mock_modelaccessor(xos_dir, services_dir, [get_models_fn("olt-service", "volt.xproto"), + get_models_fn("vsg", "vsg.xproto"), + get_models_fn("../profiles/rcord", "rcord.xproto")]) + import synchronizers.new_base.modelaccessor + from pull_onus import ONUDevicePullStep, model_accessor + + # import all class names to globals + for (k, v) in model_accessor.all_model_classes.items(): + globals()[k] = v + + self.sync_step = ONUDevicePullStep + + # mock volt service + self.volt_service = Mock() + self.volt_service.id = "volt_service_id" + self.volt_service.voltha_url = "voltha_url" + self.volt_service.voltha_user = "voltha_user" + self.volt_service.voltha_pass = "voltha_pass" + self.volt_service.voltha_port = 1234 + + # mock OLTDevice + self.olt = Mock() + self.olt.id = 1 + + # second mock OLTDevice + self.olt2 = Mock() + self.olt2.id = 2 + + # mock pon port + self.pon_port = Mock() + self.pon_port.id = 1 + + # mock pon port + self.pon_port2 = Mock() + self.pon_port2.id = 2 + + # mock voltha responses + self.devices = { + "items": [ + { + "id": "0001130158f01b2d", + "type": "broadcom_onu", + "vendor": "Broadcom", + "serial_number": "BRCM22222222", + "vendor_id": "BRCM", + "adapter": "broadcom_onu", + "vlan": 0, + "admin_state": "ENABLED", + "oper_status": "ACTIVE", + "connect_status": "REACHABLE", + "parent_id": "00010fc93996afea", + "parent_port_no": 1 + } + ] + } + + self.two_devices = { + "items": [ + { + "id": "0001130158f01b2d", + "type": "broadcom_onu", + "vendor": "Broadcom", + "serial_number": "BRCM22222222", + "vendor_id": "BRCM", + "adapter": "broadcom_onu", + "vlan": 0, + "admin_state": "ENABLED", + "oper_status": "ACTIVE", + "connect_status": "REACHABLE", + "parent_id": "00010fc93996afea", + "parent_port_no": 1 + }, + { + "id": "0001130158f01b2e", + "type": "broadcom_onu", + "vendor": "Broadcom", + "serial_number": "BRCM22222223", + "vendor_id": "BRCM", + "adapter": "broadcom_onu", + "vlan": 0, + "admin_state": "ENABLED", + "oper_status": "ACTIVE", + "connect_status": "REACHABLE", + "parent_id": "00010fc93996afeb", + "parent_port_no": 1 + } + ], + } + + # TODO add ports + self.ports = { + "items": [] + } + + def tearDown(self): + sys.path = self.sys_path_save + + @requests_mock.Mocker() + def test_missing_volt_service(self, m): + self.assertFalse(m.called) + + @requests_mock.Mocker() + def test_pull(self, m): + + with patch.object(VOLTService.objects, "all") as olt_service_mock, \ + patch.object(OLTDevice.objects, "get") as mock_olt_device, \ + patch.object(PONPort.objects, "get") as mock_pon_port, \ + patch.object(ONUDevice, "save", autospec=True) as mock_save: + olt_service_mock.return_value = [self.volt_service] + mock_pon_port.return_value = self.pon_port + mock_olt_device.return_value = self.olt + + m.get("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.devices) + m.get("http://voltha_url:1234/api/v1/devices/0001130158f01b2d/ports", status_code=200, json=self.ports) + + self.sync_step().pull_records() + + saved_onu = mock_save.call_args[0][0] + + self.assertEqual(saved_onu.admin_state, "ENABLED") + self.assertEqual(saved_onu.oper_status, "ACTIVE") + self.assertEqual(saved_onu.connect_status, "REACHABLE") + self.assertEqual(saved_onu.device_type, "broadcom_onu") + self.assertEqual(saved_onu.vendor, "Broadcom") + self.assertEqual(saved_onu.device_id, "0001130158f01b2d") + + self.assertEqual(mock_save.call_count, 1) + + @requests_mock.Mocker() + def test_pull_bad_pon(self, m): + + def olt_side_effect(device_id): + # fail the first onu device + if device_id=="00010fc93996afea": + return self.olt + else: + return self.olt2 + + def pon_port_side_effect(mock_pon_port, port_no, olt_device_id): + # fail the first onu device + if olt_device_id==1: + raise IndexError() + return self.pon_port2 + + with patch.object(VOLTService.objects, "all") as olt_service_mock, \ + patch.object(OLTDevice.objects, "get") as mock_olt_device, \ + patch.object(PONPort.objects, "get") as mock_pon_port, \ + patch.object(ONUDevice, "save", autospec=True) as mock_save: + olt_service_mock.return_value = [self.volt_service] + mock_pon_port.side_effect = functools.partial(pon_port_side_effect, self.pon_port) + mock_olt_device.side_effect = olt_side_effect + + m.get("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.two_devices) + m.get("http://voltha_url:1234/api/v1/devices/0001130158f01b2d/ports", status_code=200, json=self.ports) + m.get("http://voltha_url:1234/api/v1/devices/0001130158f01b2e/ports", status_code=200, json=self.ports) + + self.sync_step().pull_records() + + self.assertEqual(mock_save.call_count, 1) + saved_onu = mock_save.call_args[0][0] + + # we should get the second onu in self.two_onus + + self.assertEqual(saved_onu.admin_state, "ENABLED") + self.assertEqual(saved_onu.oper_status, "ACTIVE") + self.assertEqual(saved_onu.connect_status, "REACHABLE") + self.assertEqual(saved_onu.device_type, "broadcom_onu") + self.assertEqual(saved_onu.vendor, "Broadcom") + self.assertEqual(saved_onu.device_id, "0001130158f01b2e") + + self.assertEqual(mock_save.call_count, 1) + + @requests_mock.Mocker() + def test_pull_bad_olt(self, m): + + def olt_side_effect(device_id): + # fail the first onu device + if device_id=="00010fc93996afea": + raise IndexError() + else: + return self.olt2 + + with patch.object(VOLTService.objects, "all") as olt_service_mock, \ + patch.object(OLTDevice.objects, "get") as mock_olt_device, \ + patch.object(PONPort.objects, "get") as mock_pon_port, \ + patch.object(ONUDevice, "save", autospec=True) as mock_save: + olt_service_mock.return_value = [self.volt_service] + mock_pon_port.return_value = self.pon_port2 + mock_olt_device.side_effect = olt_side_effect + + m.get("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.two_devices) + m.get("http://voltha_url:1234/api/v1/devices/0001130158f01b2d/ports", status_code=200, json=self.ports) + m.get("http://voltha_url:1234/api/v1/devices/0001130158f01b2e/ports", status_code=200, json=self.ports) + + self.sync_step().pull_records() + + self.assertEqual(mock_save.call_count, 1) + saved_onu = mock_save.call_args[0][0] + + # we should get the second onu in self.two_onus + + self.assertEqual(saved_onu.admin_state, "ENABLED") + self.assertEqual(saved_onu.oper_status, "ACTIVE") + self.assertEqual(saved_onu.connect_status, "REACHABLE") + self.assertEqual(saved_onu.device_type, "broadcom_onu") + self.assertEqual(saved_onu.vendor, "Broadcom") + self.assertEqual(saved_onu.device_id, "0001130158f01b2e") + + self.assertEqual(mock_save.call_count, 1) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/olt-service/xos/synchronizer/steps/sync_olt_device.py b/src/olt-service/xos/synchronizer/steps/sync_olt_device.py new file mode 100644 index 0000000..40f884e --- /dev/null +++ b/src/olt-service/xos/synchronizer/steps/sync_olt_device.py @@ -0,0 +1,209 @@ +# 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 time import sleep + +import requests +from multistructlog import create_logger +from requests.auth import HTTPBasicAuth +from synchronizers.new_base.syncstep import SyncStep, DeferredException +from synchronizers.new_base.modelaccessor import OLTDevice, model_accessor +from xosconfig import Config + +import os, sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from helpers import Helpers + +log = create_logger(Config().get('logging')) + +class SyncOLTDevice(SyncStep): + provides = [OLTDevice] + observes = OLTDevice + + max_attempt = 120 # we give 10 minutes to the OLT to activate + + @staticmethod + def get_ids_from_logical_device(o): + voltha = Helpers.get_voltha_info(o.volt_service) + + request = requests.get("%s:%d/api/v1/logical_devices" % (voltha['url'], voltha['port'])) + + if request.status_code != 200: + raise Exception("Failed to retrieve logical devices from VOLTHA: %s" % request.text) + + response = request.json() + + for ld in response["items"]: + if ld["root_device_id"] == o.device_id: + o.of_id = ld["id"] + o.dp_id = "of:%s" % (Helpers.datapath_id_to_hex(ld["datapath_id"])) # Convert to hex + return o + + raise Exception("Can't find a logical_device for OLT device id: %s" % o.device_id) + + def pre_provision_olt_device(self, model): + log.info("Pre-provisioning OLT device in VOLTHA", object=str(model), **model.tologdict()) + + voltha = Helpers.get_voltha_info(model.volt_service) + + data = { + "type": model.device_type + } + + if hasattr(model, "host") and hasattr(model, "port"): + data["host_and_port"] = "%s:%s" % (model.host, model.port) + elif hasattr(model, "mac_address"): + data["mac_address"] = model.mac_address + + log.info("Pushing OLT to Voltha", data=data) + + request = requests.post("%s:%d/api/v1/devices" % (voltha['url'], voltha['port']), json=data) + + if request.status_code != 200: + raise Exception("Failed to add OLT device: %s" % request.text) + + log.info("Add device response", text=request.text) + + res = request.json() + + log.info("Add device json res", res=res) + + if not res['id']: + raise Exception( + 'VOLTHA Device Id is empty. This probably means that the OLT device is already provisioned in VOLTHA') + else: + model.device_id = res['id'] + model.serial_number = res['serial_number'] + + + return model + + def activate_olt(self, model): + + attempted = 0 + + voltha = Helpers.get_voltha_info(model.volt_service) + + # Enable device + request = requests.post("%s:%d/api/v1/devices/%s/enable" % (voltha['url'], voltha['port'], model.device_id)) + + if request.status_code != 200: + raise Exception("Failed to enable OLT device: %s" % request.text) + + model.backend_status = "Waiting for device to be activated" + model.save(always_update_timestamp=False) # we don't want to kickoff a new loop + + # Read state + request = requests.get("%s:%d/api/v1/devices/%s" % (voltha['url'], voltha['port'], model.device_id)).json() + while request['oper_status'] == "ACTIVATING" and attempted < self.max_attempt: + log.info("Waiting for OLT device %s (%s) to activate" % (model.name, model.device_id)) + sleep(5) + request = requests.get("%s:%d/api/v1/devices/%s" % (voltha['url'], voltha['port'], model.device_id)).json() + attempted = attempted + 1 + + + model.admin_state = request['admin_state'] + model.oper_status = request['oper_status'] + model.serial_number = request['serial_number'] + + if model.oper_status != "ACTIVE": + raise Exception("It was not possible to activate OLTDevice with id %s" % model.id) + + # Find the of_id of the device + model = self.get_ids_from_logical_device(model) + model.save() + + return model + + def configure_onos(self, model): + + log.info("Adding OLT device in onos-voltha", object=str(model), **model.tologdict()) + + onos_voltha = Helpers.get_onos_voltha_info(model.volt_service) + onos_voltha_basic_auth = HTTPBasicAuth(onos_voltha['user'], onos_voltha['pass']) + + # Add device info to onos-voltha + data = { + "devices": { + model.dp_id: { + "basic": { + "name": model.name + } + } + } + } + + log.info("Calling ONOS", data=data) + + url = "%s:%d/onos/v1/network/configuration/" % (onos_voltha['url'], onos_voltha['port']) + request = requests.post(url, json=data, auth=onos_voltha_basic_auth) + + if request.status_code != 200: + log.error(request.text) + raise Exception("Failed to add OLT device %s into ONOS" % model.name) + else: + try: + print request.json() + except Exception: + print request.text + return model + + def sync_record(self, model): + log.info("Synching device", object=str(model), **model.tologdict()) + + # If the device has feedback_state is already present in voltha + if not model.device_id and not model.admin_state and not model.oper_status and not model.of_id: + log.info("Pushing OLT device to VOLTHA", object=str(model), **model.tologdict()) + model = self.pre_provision_olt_device(model) + self.activate_olt(model) + elif model.oper_status != "ACTIVE": + raise Exception("It was not possible to activate OLTDevice with id %s" % model.id) + else: + log.info("OLT device already exists in VOLTHA", object=str(model), **model.tologdict()) + + self.configure_onos(model) + + def delete_record(self, model): + log.info("Deleting OLT device", object=str(model), **model.tologdict()) + + voltha = Helpers.get_voltha_info(model.volt_service) + + if not model.device_id or model.backend_code == 2: + # NOTE if the device was not synchronized, just remove it from the data model + log.warning("OLTDevice %s has no device_id, it was never saved in VOLTHA" % model.name) + return + else: + try: + # Disable the OLT device + request = requests.post("%s:%d/api/v1/devices/%s/disable" % (voltha['url'], voltha['port'], model.device_id)) + + if request.status_code != 200: + log.error("Failed to disable OLT device in VOLTHA: %s - %s" % (model.name, model.device_id), rest_response=request.text, rest_status_code=request.status_code) + raise Exception("Failed to disable OLT device in VOLTHA") + + # NOTE [teo] wait some time after the disable to let VOLTHA doing its things + i = 0 + for i in list(reversed(range(10))): + sleep(1) + log.info("Deleting the OLT in %s seconds" % i) + + # Delete the OLT device + request = requests.delete("%s:%d/api/v1/devices/%s/delete" % (voltha['url'], voltha['port'], model.device_id)) + + if request.status_code != 200: + log.error("Failed to delete OLT device from VOLTHA: %s - %s" % (model.name, model.device_id), rest_response=request.text, rest_status_code=request.status_code) + raise Exception("Failed to delete OLT device from VOLTHA") + except requests.ConnectionError: + log.warning("ConnectionError when contacting Voltha in OLT delete step", name=model.name, device_id=model.device_id) diff --git a/src/olt-service/xos/synchronizer/steps/sync_onu_device.py b/src/olt-service/xos/synchronizer/steps/sync_onu_device.py new file mode 100644 index 0000000..611496b --- /dev/null +++ b/src/olt-service/xos/synchronizer/steps/sync_onu_device.py @@ -0,0 +1,59 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os, sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from helpers import Helpers + +import requests +from multistructlog import create_logger +from requests.auth import HTTPBasicAuth +from synchronizers.new_base.modelaccessor import ONUDevice, model_accessor +from synchronizers.new_base.syncstep import SyncStep +from xosconfig import Config + +log = create_logger(Config().get("logging")) + +class SyncONUDevice(SyncStep): + provides = [ONUDevice] + + observes = ONUDevice + + def disable_onu(self, o): + volt_service = o.pon_port.olt_device.volt_service + voltha = Helpers.get_voltha_info(volt_service) + + log.info("Disabling device %s in voltha" % o.device_id) + request = requests.post("%s:%d/api/v1/devices/%s/disable" % (voltha['url'], voltha['port'], o.device_id)) + + if request.status_code != 200: + raise Exception("Failed to disable ONU device %s: %s" % (o.serial_number, request.text)) + + def enable_onu(self, o): + volt_service = o.pon_port.olt_device.volt_service + voltha = Helpers.get_voltha_info(volt_service) + + log.info("Enabling device %s in voltha" % o.device_id) + request = requests.post("%s:%d/api/v1/devices/%s/enable" % (voltha['url'], voltha['port'], o.device_id)) + + if request.status_code != 200: + raise Exception("Failed to enable ONU device %s: %s" % (o.serial_number, request.text)) + + def sync_record(self, o): + + if o.admin_state == "DISABLED": + self.disable_onu(o) + if o.admin_state == "ENABLED": + self.enable_onu(o) \ No newline at end of file diff --git a/src/olt-service/xos/synchronizer/steps/sync_volt_service_instance.py b/src/olt-service/xos/synchronizer/steps/sync_volt_service_instance.py new file mode 100644 index 0000000..5ec0328 --- /dev/null +++ b/src/olt-service/xos/synchronizer/steps/sync_volt_service_instance.py @@ -0,0 +1,95 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os, sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from helpers import Helpers + +import requests +from multistructlog import create_logger +from requests.auth import HTTPBasicAuth +from synchronizers.new_base.modelaccessor import VOLTService, VOLTServiceInstance, ServiceInstance, model_accessor +from synchronizers.new_base.syncstep import SyncStep, DeferredException +from xosconfig import Config + +log = create_logger(Config().get("logging")) + +class SyncVOLTServiceInstance(SyncStep): + provides = [VOLTServiceInstance] + + observes = VOLTServiceInstance + + def sync_record(self, o): + + if o.policy_code != 1: + raise DeferredException("Waiting for ModelPolicy to complete") + + volt_service = VOLTService.objects.get(id=o.owner_id) + + log.info("Synching OLTServiceInstance", object=str(o), **o.tologdict()) + + olt_device = o.onu_device.pon_port.olt_device + + try: + # NOTE each ONU has only one UNI port! + uni_port_id = o.onu_device.uni_ports.first().port_no + except AttributeError: + # This is because the ONUDevice is set by model_policy + raise DeferredException("Waiting for ONUDevice %s " % olt_device.name) + + if not olt_device.dp_id: + raise DeferredException("Waiting for OLTDevice %s to be synchronized" % olt_device.name) + + log.debug("Adding subscriber with info", + uni_port_id = uni_port_id, + dp_id = olt_device.dp_id + ) + + # Send request to ONOS + onos_voltha = Helpers.get_onos_voltha_info(volt_service) + onos_voltha_basic_auth = HTTPBasicAuth(onos_voltha['user'], onos_voltha['pass']) + + handle = "%s/%s" % (olt_device.dp_id, uni_port_id) + + full_url = "%s:%d/onos/olt/oltapp/%s" % (onos_voltha['url'], onos_voltha['port'], handle) + + log.info("Sending request to onos-voltha", url=full_url) + + request = requests.post(full_url, auth=onos_voltha_basic_auth) + + if request.status_code != 200: + raise Exception("Failed to add subscriber in onos-voltha: %s" % request.text) + + o.backend_handle = handle + o.save(update_fields=["backend_handle"]) + + log.info("Added Subscriber in onos voltha", response=request.text) + + def delete_record(self, o): + + log.info("Removing OLTServiceInstance", object=str(o), **o.tologdict()) + + volt_service = VOLTService.objects.get(id=o.owner_id) + onos_voltha = Helpers.get_onos_voltha_info(volt_service) + onos_voltha_basic_auth = HTTPBasicAuth(onos_voltha['user'], onos_voltha['pass']) + + full_url = "%s:%d/onos/olt/oltapp/%s" % (onos_voltha['url'], onos_voltha['port'], o.backend_handle) + + request = requests.delete(full_url, auth=onos_voltha_basic_auth) + + if request.status_code != 204: + raise Exception("Failed to remove subscriber from onos-voltha: %s" % request.text) + + log.info("Removed Subscriber from onos voltha", response=request.text) \ No newline at end of file diff --git a/src/olt-service/xos/synchronizer/steps/test_sync_olt_device.py b/src/olt-service/xos/synchronizer/steps/test_sync_olt_device.py new file mode 100644 index 0000000..38da1a1 --- /dev/null +++ b/src/olt-service/xos/synchronizer/steps/test_sync_olt_device.py @@ -0,0 +1,363 @@ +# 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 requests import ConnectionError +import unittest +import functools +from mock import patch, call, Mock, PropertyMock +import requests_mock + +import os, sys + +# Hack to load synchronizer framework +test_path=os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +xos_dir=os.path.join(test_path, "../../..") +if not os.path.exists(os.path.join(test_path, "new_base")): + xos_dir=os.path.join(test_path, "../../../../../../orchestration/xos/xos") + services_dir = os.path.join(xos_dir, "../../xos_services") +sys.path.append(xos_dir) +sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) +# END of hack to load synchronizer framework + +# Generate model from xproto +def get_models_fn(service_name, xproto_name): + name = os.path.join(service_name, "xos", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + else: + name = os.path.join(service_name, "xos", "synchronizer", "models", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + raise Exception("Unable to find service=%s xproto=%s" % (service_name, xproto_name)) +# END generate model from xproto + +def match_onos_req(req): + request = req.json()['devices'] + if not request['of:0000000ce2314000']: + return False + else: + if not request['of:0000000ce2314000']['basic']['driver'] == 'voltha': + return False + if not request['of:0000000ce2314000']['accessDevice']['vlan'] == 1 or not request['of:0000000ce2314000']['accessDevice']['uplink'] == "129": + return False + return True + +def match_json(desired, req): + if desired!=req.json(): + raise Exception("Got request %s, but body is not matching" % req.url) + return False + return True + +class TestSyncOLTDevice(unittest.TestCase): + def setUp(self): + global DeferredException + self.sys_path_save = sys.path + sys.path.append(xos_dir) + sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) + + # Setting up the config module + from xosconfig import Config + config = os.path.join(test_path, "../test_config.yaml") + Config.clear() + Config.init(config, "synchronizer-config-schema.yaml") + # END setting up the config module + + from synchronizers.new_base.mock_modelaccessor_build import build_mock_modelaccessor + # build_mock_modelaccessor(xos_dir, services_dir, [get_models_fn("olt-service", "volt.xproto")]) + + # FIXME this is to get jenkins to pass the tests, somehow it is running tests in a different order + # and apparently it is not overriding the generated model accessor + build_mock_modelaccessor(xos_dir, services_dir, [get_models_fn("olt-service", "volt.xproto"), + get_models_fn("vsg", "vsg.xproto"), + get_models_fn("../profiles/rcord", "rcord.xproto")]) + + import synchronizers.new_base.modelaccessor + from sync_olt_device import SyncOLTDevice, DeferredException + self.sync_step = SyncOLTDevice + + pon_port = Mock() + pon_port.port_id = "00ff00" + + # Create a mock OLTDevice + o = Mock() + o.volt_service.voltha_url = "voltha_url" + o.volt_service.voltha_port = 1234 + o.volt_service.voltha_user = "voltha_user" + o.volt_service.voltha_pass = "voltha_pass" + + o.volt_service.onos_voltha_port = 4321 + o.volt_service.onos_voltha_url = "onos" + o.volt_service.onos_voltha_user = "karaf" + o.volt_service.onos_voltha_pass = "karaf" + + o.device_type = "ponsim_olt" + o.host = "172.17.0.1" + o.port = "50060" + o.uplink = "129" + o.driver = "voltha" + o.name = "Test Device" + + # feedback state + o.device_id = None + o.admin_state = None + o.oper_status = None + o.of_id = None + o.id = 1 + + o.tologdict.return_value = {'name': "Mock VOLTServiceInstance"} + + o.save.return_value = "Saved" + + o.pon_ports.all.return_value = [pon_port] + + self.o = o + + self.voltha_devices_response = {"id": "123", "serial_number": "foobar"} + + def tearDown(self): + self.o = None + sys.path = self.sys_path_save + + @requests_mock.Mocker() + def test_get_of_id_from_device(self, m): + logical_devices = { + "items": [ + {"root_device_id": "123", "id": "0001000ce2314000", "datapath_id": "55334486016"}, + {"root_device_id": "0001cc4974a62b87", "id": "0001000000000001"} + ] + } + m.get("http://voltha_url:1234/api/v1/logical_devices", status_code=200, json=logical_devices) + self.o.device_id = "123" + self.o = self.sync_step.get_ids_from_logical_device(self.o) + self.assertEqual(self.o.of_id, "0001000ce2314000") + self.assertEqual(self.o.dp_id, "of:0000000ce2314000") + + with self.assertRaises(Exception) as e: + self.o.device_id = "idonotexist" + self.sync_step.get_ids_from_logical_device(self.o) + self.assertEqual(e.exception.message, "Can't find a logical_device for OLT device id: idonotexist") + + @requests_mock.Mocker() + def test_sync_record_fail_add(self, m): + """ + Should print an error if we can't add the device in VOLTHA + """ + m.post("http://voltha_url:1234/api/v1/devices", status_code=500, text="MockError") + + with self.assertRaises(Exception) as e: + self.sync_step().sync_record(self.o) + self.assertEqual(e.exception.message, "Failed to add OLT device: MockError") + + @requests_mock.Mocker() + def test_sync_record_fail_no_id(self, m): + """ + Should print an error if VOLTHA does not return the device id + """ + m.post("http://voltha_url:1234/api/v1/devices", status_code=200, json={"id": ""}) + + with self.assertRaises(Exception) as e: + self.sync_step().sync_record(self.o) + self.assertEqual(e.exception.message, "VOLTHA Device Id is empty. This probably means that the OLT device is already provisioned in VOLTHA") + + @requests_mock.Mocker() + def test_sync_record_fail_enable(self, m): + """ + Should print an error if device.enable fails + """ + m.post("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.voltha_devices_response) + m.post("http://voltha_url:1234/api/v1/devices/123/enable", status_code=500, text="EnableError") + + with self.assertRaises(Exception) as e: + self.sync_step().sync_record(self.o) + + self.assertEqual(e.exception.message, "Failed to enable OLT device: EnableError") + + @requests_mock.Mocker() + def test_sync_record_success(self, m): + """ + If device.enable succed should fetch the state, retrieve the of_id and push it to ONOS + """ + + expected_conf = { + "type": self.o.device_type, + "host_and_port": "%s:%s" % (self.o.host, self.o.port) + } + + m.post("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.voltha_devices_response, additional_matcher=functools.partial(match_json, expected_conf)) + m.post("http://voltha_url:1234/api/v1/devices/123/enable", status_code=200) + m.get("http://voltha_url:1234/api/v1/devices/123", json={"oper_status": "ACTIVE", "admin_state": "ENABLED", "serial_number": "foobar"}) + logical_devices = { + "items": [ + {"root_device_id": "123", "id": "0001000ce2314000", "datapath_id": "55334486016"}, + {"root_device_id": "0001cc4974a62b87", "id": "0001000000000001"} + ] + } + m.get("http://voltha_url:1234/api/v1/logical_devices", status_code=200, json=logical_devices) + + onos_expected_conf = { + "devices": { + "of:0000000ce2314000": { + "basic": { + "name": self.o.name + } + } + } + } + m.post("http://onos:4321/onos/v1/network/configuration/", status_code=200, json=onos_expected_conf, + additional_matcher=functools.partial(match_json, onos_expected_conf)) + + self.sync_step().sync_record(self.o) + self.assertEqual(self.o.admin_state, "ENABLED") + self.assertEqual(self.o.oper_status, "ACTIVE") + self.assertEqual(self.o.serial_number, "foobar") + self.assertEqual(self.o.of_id, "0001000ce2314000") + self.assertEqual(self.o.save.call_count, 2) # we're updating the backend_status when activating and then adding logical device ids + + @requests_mock.Mocker() + def test_sync_record_success_mac_address(self, m): + """ + A device should be pre-provisioned via mac_address, the the process is the same + """ + + del self.o.host + del self.o.port + self.o.mac_address = "00:0c:e2:31:40:00" + + expected_conf = { + "type": self.o.device_type, + "mac_address": self.o.mac_address + } + + onos_expected_conf = { + "devices": { + "of:0000000ce2314000": { + "basic": { + "name": self.o.name + } + } + } + } + m.post("http://onos:4321/onos/v1/network/configuration/", status_code=200, json=onos_expected_conf, + additional_matcher=functools.partial(match_json, onos_expected_conf)) + + m.post("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.voltha_devices_response, + additional_matcher=functools.partial(match_json, expected_conf)) + m.post("http://voltha_url:1234/api/v1/devices/123/enable", status_code=200) + m.get("http://voltha_url:1234/api/v1/devices/123", json={"oper_status": "ACTIVE", "admin_state": "ENABLED", "serial_number": "foobar"}) + logical_devices = { + "items": [ + {"root_device_id": "123", "id": "0001000ce2314000", "datapath_id": "55334486016"}, + {"root_device_id": "0001cc4974a62b87", "id": "0001000000000001"} + ] + } + m.get("http://voltha_url:1234/api/v1/logical_devices", status_code=200, json=logical_devices) + + self.sync_step().sync_record(self.o) + self.assertEqual(self.o.admin_state, "ENABLED") + self.assertEqual(self.o.oper_status, "ACTIVE") + self.assertEqual(self.o.of_id, "0001000ce2314000") + self.assertEqual(self.o.save.call_count, 2) + + @requests_mock.Mocker() + def test_sync_record_enable_timeout(self, m): + """ + If device.enable fails we need to tell the suer + """ + + expected_conf = { + "type": self.o.device_type, + "host_and_port": "%s:%s" % (self.o.host, self.o.port) + } + + m.post("http://voltha_url:1234/api/v1/devices", status_code=200, json=self.voltha_devices_response, + additional_matcher=functools.partial(match_json, expected_conf)) + m.post("http://voltha_url:1234/api/v1/devices/123/enable", status_code=200) + m.get("http://voltha_url:1234/api/v1/devices/123", [ + {"json": {"oper_status": "ACTIVATING", "admin_state": "ENABLED", "serial_number": "foobar"}, "status_code": 200}, + {"json": {"oper_status": "ERROR", "admin_state": "FAILED", "serial_number": "foobar"}, "status_code": 200} + ]) + + logical_devices = { + "items": [ + {"root_device_id": "123", "id": "0001000ce2314000", "datapath_id": "55334486016"}, + {"root_device_id": "0001cc4974a62b87", "id": "0001000000000001"} + ] + } + m.get("http://voltha_url:1234/api/v1/logical_devices", status_code=200, json=logical_devices) + + with self.assertRaises(Exception) as e: + self.sync_step().sync_record(self.o) + + self.assertEqual(e.exception.message, "It was not possible to activate OLTDevice with id 1") + self.assertEqual(self.o.oper_status, "ERROR") + self.assertEqual(self.o.admin_state, "FAILED") + self.assertEqual(self.o.save.call_count, 1) + + @requests_mock.Mocker() + def test_sync_record_already_existing_in_voltha(self, m): + # mock device feedback state + self.o.device_id = "123" + self.o.admin_state = "ENABLED" + self.o.oper_status = "ACTIVE" + self.o.dp_id = "of:0000000ce2314000" + self.o.of_id = "0001000ce2314000" + + expected_conf = { + "devices": { + self.o.dp_id: { + "basic": { + "name": self.o.name + } + } + } + } + m.post("http://onos:4321/onos/v1/network/configuration/", status_code=200, json=expected_conf, + additional_matcher=functools.partial(match_json, expected_conf)) + + self.sync_step().sync_record(self.o) + self.o.save.assert_not_called() + + @requests_mock.Mocker() + def test_delete_record(self, m): + self.o.of_id = "0001000ce2314000" + self.o.device_id = "123" + + m.post("http://voltha_url:1234/api/v1/devices/123/disable", status_code=200) + m.delete("http://voltha_url:1234/api/v1/devices/123/delete", status_code=200) + + self.sync_step().delete_record(self.o) + + self.assertEqual(m.call_count, 2) + + @patch('requests.post') + def test_delete_record_connectionerror(self, m): + self.o.of_id = "0001000ce2314000" + self.o.device_id = "123" + + m.side_effect = ConnectionError() + + self.sync_step().delete_record(self.o) + + # No exception thrown, as ConnectionError will be caught + + + @requests_mock.Mocker() + def test_delete_unsynced_record(self, m): + + self.sync_step().delete_record(self.o) + + self.assertEqual(m.call_count, 0) + +if __name__ == "__main__": + unittest.main() diff --git a/src/olt-service/xos/synchronizer/steps/test_sync_onu_device.py b/src/olt-service/xos/synchronizer/steps/test_sync_onu_device.py new file mode 100644 index 0000000..abd72f7 --- /dev/null +++ b/src/olt-service/xos/synchronizer/steps/test_sync_onu_device.py @@ -0,0 +1,117 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from mock import patch, call, Mock, PropertyMock +import requests_mock + +import os, sys + +# Hack to load synchronizer framework +test_path=os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +xos_dir=os.path.join(test_path, "../../..") +if not os.path.exists(os.path.join(test_path, "new_base")): + xos_dir=os.path.join(test_path, "../../../../../../orchestration/xos/xos") + services_dir = os.path.join(xos_dir, "../../xos_services") +sys.path.append(xos_dir) +sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) +# END Hack to load synchronizer framework + +# generate model from xproto +def get_models_fn(service_name, xproto_name): + name = os.path.join(service_name, "xos", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + else: + name = os.path.join(service_name, "xos", "synchronizer", "models", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + raise Exception("Unable to find service=%s xproto=%s" % (service_name, xproto_name)) +# END generate model from xproto + +class TestSyncVOLTServiceInstance(unittest.TestCase): + def setUp(self): + + self.sys_path_save = sys.path + sys.path.append(xos_dir) + sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) + + # Setting up the config module + from xosconfig import Config + config = os.path.join(test_path, "../test_config.yaml") + Config.clear() + Config.init(config, "synchronizer-config-schema.yaml") + # END Setting up the config module + + from synchronizers.new_base.mock_modelaccessor_build import build_mock_modelaccessor + # build_mock_modelaccessor(xos_dir, services_dir, [get_models_fn("olt-service", "volt.xproto")]) + + # FIXME this is to get jenkins to pass the tests, somehow it is running tests in a different order + # and apparently it is not overriding the generated model accessor + build_mock_modelaccessor(xos_dir, services_dir, [get_models_fn("olt-service", "volt.xproto"), + get_models_fn("vsg", "vsg.xproto"), + get_models_fn("../profiles/rcord", "rcord.xproto")]) + import synchronizers.new_base.modelaccessor + from synchronizers.new_base.syncstep import DeferredException + from sync_onu_device import SyncONUDevice, model_accessor + + # import all class names to globals + for (k, v) in model_accessor.all_model_classes.items(): + globals()[k] = v + + self.sync_step = SyncONUDevice + + volt_service = Mock() + volt_service.voltha_url = "voltha_url" + volt_service.voltha_port = 1234 + volt_service.voltha_user = "voltha_user" + volt_service.voltha_pass = "voltha_pass" + + self.o = Mock() + self.o.device_id = "test_id" + self.o.pon_port.olt_device.volt_service = volt_service + + def tearDown(self): + self.o = None + sys.path = self.sys_path_save + + @requests_mock.Mocker() + def test_enable(self, m): + m.post("http://voltha_url:1234/api/v1/devices/test_id/enable") + + self.o.admin_state = "ENABLED" + self.sync_step().sync_record(self.o) + self.assertTrue(m.called) + + @requests_mock.Mocker() + def test_disable(self, m): + m.post("http://voltha_url:1234/api/v1/devices/test_id/disable") + + self.o.admin_state = "DISABLED" + self.sync_step().sync_record(self.o) + self.assertTrue(m.called) + + @requests_mock.Mocker() + def test_disable_fail(self, m): + m.post("http://voltha_url:1234/api/v1/devices/test_id/disable", status_code=500, text="Mock Error") + + self.o.admin_state = "DISABLED" + + with self.assertRaises(Exception) as e: + self.sync_step().sync_record(self.o) + self.assertTrue(m.called) + self.assertEqual(e.exception.message, "Failed to disable ONU device: Mock Error") + +if __name__ == "__main__": + unittest.main() diff --git a/src/olt-service/xos/synchronizer/steps/test_sync_volt_service_instance.py b/src/olt-service/xos/synchronizer/steps/test_sync_volt_service_instance.py new file mode 100644 index 0000000..c247178 --- /dev/null +++ b/src/olt-service/xos/synchronizer/steps/test_sync_volt_service_instance.py @@ -0,0 +1,165 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import functools +from mock import patch, call, Mock, PropertyMock +import requests_mock + +import os, sys + +# Hack to load synchronizer framework +test_path=os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +xos_dir=os.path.join(test_path, "../../..") +if not os.path.exists(os.path.join(test_path, "new_base")): + xos_dir=os.path.join(test_path, "../../../../../../orchestration/xos/xos") + services_dir = os.path.join(xos_dir, "../../xos_services") +sys.path.append(xos_dir) +sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) +# END Hack to load synchronizer framework + +# generate model from xproto +def get_models_fn(service_name, xproto_name): + name = os.path.join(service_name, "xos", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + else: + name = os.path.join(service_name, "xos", "synchronizer", "models", xproto_name) + if os.path.exists(os.path.join(services_dir, name)): + return name + raise Exception("Unable to find service=%s xproto=%s" % (service_name, xproto_name)) +# END generate model from xproto + +class TestSyncVOLTServiceInstance(unittest.TestCase): + def setUp(self): + global DeferredException + + self.sys_path_save = sys.path + sys.path.append(xos_dir) + sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base')) + + # Setting up the config module + from xosconfig import Config + config = os.path.join(test_path, "../test_config.yaml") + Config.clear() + Config.init(config, "synchronizer-config-schema.yaml") + # END Setting up the config module + + from synchronizers.new_base.mock_modelaccessor_build import build_mock_modelaccessor + # build_mock_modelaccessor(xos_dir, services_dir, [get_models_fn("olt-service", "volt.xproto")]) + + # FIXME this is to get jenkins to pass the tests, somehow it is running tests in a different order + # and apparently it is not overriding the generated model accessor + build_mock_modelaccessor(xos_dir, services_dir, [get_models_fn("olt-service", "volt.xproto"), + get_models_fn("vsg", "vsg.xproto"), + get_models_fn("../profiles/rcord", "rcord.xproto")]) + import synchronizers.new_base.modelaccessor + from synchronizers.new_base.syncstep import DeferredException + from sync_volt_service_instance import SyncVOLTServiceInstance, model_accessor + + # import all class names to globals + for (k, v) in model_accessor.all_model_classes.items(): + globals()[k] = v + + self.sync_step = SyncVOLTServiceInstance + + volt_service = Mock() + volt_service.onos_voltha_url = "onos_voltha_url" + volt_service.onos_voltha_port = 4321 + volt_service.onos_voltha_user = "onos_voltha_user" + volt_service.onos_voltha_pass = "onos_voltha_pass" + + uni_port = Mock() + uni_port.port_no = "uni_port_id" + + onu_device = Mock() + onu_device.name = "BRCM1234" + onu_device.pon_port.olt_device.dp_id = None + onu_device.pon_port.olt_device.name = "Test OLT Device" + onu_device.uni_ports.first.return_value = uni_port + + # create a mock service instance + o = Mock() + o.policy_code = 1 + o.id = 1 + o.owner_id = "volt_service" + o.onu_device = onu_device + o.tologdict.return_value = {} + + self.o = o + self.onu_device = onu_device + self.volt_service = volt_service + + def tearDown(self): + self.o = None + sys.path = self.sys_path_save + + @requests_mock.Mocker() + def test_do_not_sync(self, m): + self.onu_device.pon_port.olt_device.dp_id = None + + with patch.object(VOLTService.objects, "get") as olt_service_mock: + olt_service_mock.return_value = self.volt_service + + with self.assertRaises(DeferredException) as e: + self.sync_step().sync_record(self.o) + + self.assertFalse(m.called) + self.assertEqual(e.exception.message, "Waiting for OLTDevice Test OLT Device to be synchronized") + + @requests_mock.Mocker() + def test_do_sync(self, m): + + self.onu_device.pon_port.olt_device.dp_id = "of:dp_id" + + m.post("http://onos_voltha_url:4321/onos/olt/oltapp/of:dp_id/uni_port_id", status_code=200, json={}) + + with patch.object(VOLTService.objects, "get") as olt_service_mock: + olt_service_mock.return_value = self.volt_service + + self.sync_step().sync_record(self.o) + self.assertTrue(m.called) + self.assertEqual(self.o.backend_handle, "of:dp_id/uni_port_id") + + @requests_mock.Mocker() + def test_do_sync_fail(self, m): + + m.post("http://onos_voltha_url:4321/onos/olt/oltapp/of:dp_id/uni_port_id", status_code=500, text="Mock Error") + + self.onu_device.pon_port.olt_device.dp_id = "of:dp_id" + + with patch.object(VOLTService.objects, "get") as olt_service_mock: + olt_service_mock.return_value = self.volt_service + + with self.assertRaises(Exception) as e: + self.sync_step().sync_record(self.o) + self.assertTrue(m.called) + self.assertEqual(e.exception.message, "Failed to add subscriber in onos voltha: Mock Error") + + @requests_mock.Mocker() + def test_delete(self, m): + m.delete("http://onos_voltha_url:4321/onos/olt/oltapp/of:dp_id/uni_port_id", status_code=204) + + self.onu_device.pon_port.olt_device.dp_id = "of:dp_id" + self.o.backend_handle = "of:dp_id/uni_port_id" + + with patch.object(VOLTService.objects, "get") as olt_service_mock: + olt_service_mock.return_value = self.volt_service + + self.sync_step().delete_record(self.o) + self.assertTrue(m.called) + self.assertEqual(m.call_count, 1) + +if __name__ == "__main__": + unittest.main() diff --git a/src/olt-service/xos/synchronizer/test_config.yaml b/src/olt-service/xos/synchronizer/test_config.yaml new file mode 100644 index 0000000..1560049 --- /dev/null +++ b/src/olt-service/xos/synchronizer/test_config.yaml @@ -0,0 +1,31 @@ + +# 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. + + +name: test-model-policies +accessor: + username: xosadmin@opencord.org + password: "sample" + kind: "testframework" +logging: + version: 1 + handlers: + console: + class: logging.StreamHandler + loggers: + 'multistructlog': + handlers: + - console +# level: DEBUG diff --git a/src/olt-service/xos/synchronizer/test_helpers.py b/src/olt-service/xos/synchronizer/test_helpers.py new file mode 100644 index 0000000..1ec5df7 --- /dev/null +++ b/src/olt-service/xos/synchronizer/test_helpers.py @@ -0,0 +1,68 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from mock import Mock + +from helpers import Helpers + + +class TestHelpers(unittest.TestCase): + + def setUp(self): + # Create a mock service instance + o = Mock() + o.voltha_url = "voltha_url" + o.voltha_port = 1234 + o.voltha_user = "voltha_user" + o.voltha_pass = "voltha_pass" + o.onos_voltha_url = "onos_voltha_url" + o.onos_voltha_port = 4321 + o.onos_voltha_user = "onos_voltha_user" + o.onos_voltha_pass = "onos_voltha_pass" + + self.o = o + + def test_format_url(self): + url = Helpers.format_url("onf.com") + self.assertEqual(url, "http://onf.com") + url = Helpers.format_url("http://onf.com") + self.assertEqual(url, "http://onf.com") + + def test_get_voltha_info(self): + voltha_dict = Helpers.get_voltha_info(self.o) + + self.assertEqual(voltha_dict["url"], "http://voltha_url") + self.assertEqual(voltha_dict["port"], 1234) + self.assertEqual(voltha_dict["user"], "voltha_user") + self.assertEqual(voltha_dict["pass"], "voltha_pass") + + def test_get_onos_info(self): + onos_voltha_dict = Helpers.get_onos_voltha_info(self.o) + + self.assertEqual(onos_voltha_dict["url"], "http://onos_voltha_url") + self.assertEqual(onos_voltha_dict["port"], 4321) + self.assertEqual(onos_voltha_dict["user"], "onos_voltha_user") + self.assertEqual(onos_voltha_dict["pass"], "onos_voltha_pass") + + def test_datapath_id_to_hex(self): + hex = Helpers.datapath_id_to_hex(55334486016) + self.assertEqual(hex, "0000000ce2314000") + + hex = Helpers.datapath_id_to_hex("55334486016") + self.assertEqual(hex, "0000000ce2314000") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/src/olt-service/xos/synchronizer/volt-synchronizer.py b/src/olt-service/xos/synchronizer/volt-synchronizer.py new file mode 100755 index 0000000..337472d --- /dev/null +++ b/src/olt-service/xos/synchronizer/volt-synchronizer.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +# 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. + +# This imports and runs ../../xos-observer.py + +import importlib +import os +import sys +from xosconfig import Config + +base_config_file = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + '/config.yaml') +mounted_config_file = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + '/mounted_config.yaml') + +if os.path.isfile(mounted_config_file): + Config.init(base_config_file, 'synchronizer-config-schema.yaml', mounted_config_file) +else: + Config.init(base_config_file, 'synchronizer-config-schema.yaml') + +observer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../../synchronizers/new_base") +sys.path.append(observer_path) +mod = importlib.import_module("xos-synchronizer") +mod.main() diff --git a/src/olt-service/xos/unittest.cfg b/src/olt-service/xos/unittest.cfg new file mode 100644 index 0000000..6480c6c --- /dev/null +++ b/src/olt-service/xos/unittest.cfg @@ -0,0 +1,15 @@ +[unittest] +plugins=nose2.plugins.junitxml +code-directories=synchronizer + model_policies + steps + pull_steps + event_steps + models + +[coverage] +always-on = True +coverage = synchronizer +coverage-report = term +coverage-report = html +coverage-report = xml