Add olt src 95/1395/1
authorxinhuili <lxinhui@vmware.com>
Mon, 12 Aug 2019 22:16:17 +0000 (06:16 +0800)
committerxinhuili <lxinhui@vmware.com>
Mon, 12 Aug 2019 22:16:17 +0000 (06:16 +0800)
This patch is to add olt-src on NSX

Signed-off-by: XINHUI LI <lxinhui@vmware.com>
Change-Id: I34cff828ce16c6746240973db9e8d028a3e9a715

47 files changed:
src/olt-service/.gitignore [new file with mode: 0644]
src/olt-service/.gitreview [new file with mode: 0644]
src/olt-service/Dockerfile.synchronizer [new file with mode: 0644]
src/olt-service/Jenkinsfile [new file with mode: 0644]
src/olt-service/LICENSE.txt [new file with mode: 0644]
src/olt-service/Makefile [new file with mode: 0644]
src/olt-service/README.md [new file with mode: 0644]
src/olt-service/VERSION [new file with mode: 0644]
src/olt-service/ci_scripts/push_containers.sh [new file with mode: 0755]
src/olt-service/ci_scripts/push_manifest.sh [new file with mode: 0755]
src/olt-service/docs/README.md [new file with mode: 0644]
src/olt-service/docs/static/vOLTService_ER_diagram.png [new file with mode: 0644]
src/olt-service/samples/README.md [new file with mode: 0644]
src/olt-service/samples/olt_device_host_and_port.yaml [new file with mode: 0644]
src/olt-service/samples/olt_device_mac_address.yaml [new file with mode: 0644]
src/olt-service/samples/onu_activate_event.py [new file with mode: 0644]
src/olt-service/samples/pon_port.yaml [new file with mode: 0644]
src/olt-service/samples/second_pon_port.yaml [new file with mode: 0644]
src/olt-service/samples/second_subscriber.yaml [new file with mode: 0644]
src/olt-service/samples/subscriber.yaml [new file with mode: 0644]
src/olt-service/samples/volt_service.yaml [new file with mode: 0644]
src/olt-service/xos/synchronizer/config.yaml [new file with mode: 0644]
src/olt-service/xos/synchronizer/event_steps/kubernetes_event.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/event_steps/test_kubernetes_event.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/helpers.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/model-deps [new file with mode: 0644]
src/olt-service/xos/synchronizer/model_policies/model_policy_voltserviceinstance.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/model_policies/test_model_policy_voltserviceinstance.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/models/convenience/voltservice.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/models/convenience/voltserviceinstance.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/models/models.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/models/test_models.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/models/volt.xproto [new file with mode: 0644]
src/olt-service/xos/synchronizer/pull_steps/pull_olts.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/pull_steps/pull_onus.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/pull_steps/test_pull_olts.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/pull_steps/test_pull_onus.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/steps/sync_olt_device.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/steps/sync_onu_device.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/steps/sync_volt_service_instance.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/steps/test_sync_olt_device.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/steps/test_sync_onu_device.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/steps/test_sync_volt_service_instance.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/test_config.yaml [new file with mode: 0644]
src/olt-service/xos/synchronizer/test_helpers.py [new file with mode: 0644]
src/olt-service/xos/synchronizer/volt-synchronizer.py [new file with mode: 0755]
src/olt-service/xos/unittest.cfg [new file with mode: 0644]

diff --git a/src/olt-service/.gitignore b/src/olt-service/.gitignore
new file mode 100644 (file)
index 0000000..a48d594
--- /dev/null
@@ -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 (file)
index 0000000..0e952d2
--- /dev/null
@@ -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 (file)
index 0000000..e2c0819
--- /dev/null
@@ -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 (file)
index 0000000..a58adb4
--- /dev/null
@@ -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 (file)
index 0000000..d9d9d30
--- /dev/null
@@ -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 (file)
index 0000000..e1591ca
--- /dev/null
@@ -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 (file)
index 0000000..3518d01
--- /dev/null
@@ -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 (file)
index 0000000..1b5105d
--- /dev/null
@@ -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 (executable)
index 0000000..c9dcb35
--- /dev/null
@@ -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 (executable)
index 0000000..de9c94b
--- /dev/null
@@ -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 (file)
index 0000000..4c7dff6
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..da8db0e
--- /dev/null
@@ -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 (file)
index 0000000..c0ec47c
--- /dev/null
@@ -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 (file)
index 0000000..42213bb
--- /dev/null
@@ -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 (file)
index 0000000..8786ed5
--- /dev/null
@@ -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 (file)
index 0000000..2dfe376
--- /dev/null
@@ -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 (file)
index 0000000..91882e0
--- /dev/null
@@ -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 (file)
index 0000000..063e49e
--- /dev/null
@@ -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 (file)
index 0000000..ebd3a1b
--- /dev/null
@@ -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 (file)
index 0000000..c12525e
--- /dev/null
@@ -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 (file)
index 0000000..8a1a51e
--- /dev/null
@@ -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 (file)
index 0000000..aa8bef8
--- /dev/null
@@ -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 (file)
index 0000000..32d8e1f
--- /dev/null
@@ -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 (file)
index 0000000..cb310af
--- /dev/null
@@ -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 (file)
index 0000000..0967ef4
--- /dev/null
@@ -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 (file)
index 0000000..1321955
--- /dev/null
@@ -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 (file)
index 0000000..f129de5
--- /dev/null
@@ -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 (file)
index 0000000..22f4188
--- /dev/null
@@ -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 (file)
index 0000000..6c59ef6
--- /dev/null
@@ -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 (file)
index 0000000..4d5b8b8
--- /dev/null
@@ -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 (file)
index 0000000..92a939b
--- /dev/null
@@ -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 (file)
index 0000000..927006f
--- /dev/null
@@ -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 (file)
index 0000000..30afbd4
--- /dev/null
@@ -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 (file)
index 0000000..18f047c
--- /dev/null
@@ -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 (file)
index 0000000..a06d3ff
--- /dev/null
@@ -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 (file)
index 0000000..6fc5816
--- /dev/null
@@ -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 (file)
index 0000000..40f884e
--- /dev/null
@@ -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 (file)
index 0000000..611496b
--- /dev/null
@@ -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 (file)
index 0000000..5ec0328
--- /dev/null
@@ -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 (file)
index 0000000..38da1a1
--- /dev/null
@@ -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 (file)
index 0000000..abd72f7
--- /dev/null
@@ -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 (file)
index 0000000..c247178
--- /dev/null
@@ -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 (file)
index 0000000..1560049
--- /dev/null
@@ -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 (file)
index 0000000..1ec5df7
--- /dev/null
@@ -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 (executable)
index 0000000..337472d
--- /dev/null
@@ -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 (file)
index 0000000..6480c6c
--- /dev/null
@@ -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