Add related patch for fabric support 98/1398/2
authorxinhuili <lxinhui@vmware.com>
Wed, 14 Aug 2019 06:03:17 +0000 (14:03 +0800)
committerxinhuili <lxinhui@vmware.com>
Wed, 14 Aug 2019 06:05:19 +0000 (06:05 +0000)
This patch is to enable fabric-crossconnect

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

26 files changed:
src/fabric-crossconnect/.gitignore [new file with mode: 0644]
src/fabric-crossconnect/Dockerfile.synchronizer [new file with mode: 0644]
src/fabric-crossconnect/Jenkinsfile [new file with mode: 0644]
src/fabric-crossconnect/LICENSE.txt [new file with mode: 0644]
src/fabric-crossconnect/VERSION [new file with mode: 0644]
src/fabric-crossconnect/ci_scripts/push_containers.sh [new file with mode: 0755]
src/fabric-crossconnect/ci_scripts/push_manifest.sh [new file with mode: 0755]
src/fabric-crossconnect/docs/README.md [new file with mode: 0644]
src/fabric-crossconnect/samples/bng_mapping_any.yaml [new file with mode: 0644]
src/fabric-crossconnect/samples/bng_mapping_range.yaml [new file with mode: 0644]
src/fabric-crossconnect/samples/bng_mapping_single.yaml [new file with mode: 0644]
src/fabric-crossconnect/samples/vm_crossconnect.yaml [new file with mode: 0644]
src/fabric-crossconnect/xos/synchronizer/config.yaml [new file with mode: 0644]
src/fabric-crossconnect/xos/synchronizer/event_steps/kubernetes_event.py [new file with mode: 0644]
src/fabric-crossconnect/xos/synchronizer/event_steps/test_kubernetes_event.py [new file with mode: 0644]
src/fabric-crossconnect/xos/synchronizer/fabric-crossconnect-synchronizer.py [new file with mode: 0755]
src/fabric-crossconnect/xos/synchronizer/model_policies/model_policy_fabriccrossconnectserviceinstance.py [new file with mode: 0644]
src/fabric-crossconnect/xos/synchronizer/model_policies/test_model_policy_fabriccrossconnectserviceinstance.py [new file with mode: 0644]
src/fabric-crossconnect/xos/synchronizer/models/convenience/fabric_crossconnect_service.py [new file with mode: 0644]
src/fabric-crossconnect/xos/synchronizer/models/fabric-crossconnect.xproto [new file with mode: 0644]
src/fabric-crossconnect/xos/synchronizer/models/models.py [new file with mode: 0644]
src/fabric-crossconnect/xos/synchronizer/models/test_models.py [new file with mode: 0755]
src/fabric-crossconnect/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py [new file with mode: 0644]
src/fabric-crossconnect/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py [new file with mode: 0644]
src/fabric-crossconnect/xos/synchronizer/test_fabric_crossconnect_config.yaml [new file with mode: 0644]
src/fabric-crossconnect/xos/unittest.cfg [new file with mode: 0644]

diff --git a/src/fabric-crossconnect/.gitignore b/src/fabric-crossconnect/.gitignore
new file mode 100644 (file)
index 0000000..723ef36
--- /dev/null
@@ -0,0 +1 @@
+.idea
\ No newline at end of file
diff --git a/src/fabric-crossconnect/Dockerfile.synchronizer b/src/fabric-crossconnect/Dockerfile.synchronizer
new file mode 100644 (file)
index 0000000..005b443
--- /dev/null
@@ -0,0 +1,55 @@
+# 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/fabric-crossconnect-synchronizer:candidate -f Dockerfile.synchronizer .
+
+# xosproject/fabric-crossconnect-synchronizer
+
+FROM cachengo/xos-synchronizer-base:2.1.25
+
+COPY xos/synchronizer /opt/xos/synchronizers/fabric-crossconnect
+COPY VERSION /opt/xos/synchronizers/fabric-crossconnect/
+
+WORKDIR "/opt/xos/synchronizers/fabric-crossconnect"
+
+# Label image
+ARG org_label_schema_schema_version=1.0
+ARG org_label_schema_name=fabric-crossconnect-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/fabric-crossconnect/fabric-crossconnect-synchronizer.py"]
diff --git a/src/fabric-crossconnect/Jenkinsfile b/src/fabric-crossconnect/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/fabric-crossconnect/LICENSE.txt b/src/fabric-crossconnect/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/fabric-crossconnect/VERSION b/src/fabric-crossconnect/VERSION
new file mode 100644 (file)
index 0000000..65087b4
--- /dev/null
@@ -0,0 +1 @@
+1.1.4
diff --git a/src/fabric-crossconnect/ci_scripts/push_containers.sh b/src/fabric-crossconnect/ci_scripts/push_containers.sh
new file mode 100755 (executable)
index 0000000..1af94ce
--- /dev/null
@@ -0,0 +1,6 @@
+export IMAGE_TAG=$(cat VERSION)
+export AARCH=`uname -m`
+export IMAGE_NAME=fabric-crossconnect-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/fabric-crossconnect/ci_scripts/push_manifest.sh b/src/fabric-crossconnect/ci_scripts/push_manifest.sh
new file mode 100755 (executable)
index 0000000..b9ac357
--- /dev/null
@@ -0,0 +1,7 @@
+export IMAGE_TAG=$(cat VERSION)
+export AARCH=`uname -m`
+export IMAGE_NAME=fabric-crossconnect-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/fabric-crossconnect/docs/README.md b/src/fabric-crossconnect/docs/README.md
new file mode 100644 (file)
index 0000000..c7272e7
--- /dev/null
@@ -0,0 +1,89 @@
+# Fabric Crossconnect Service
+
+The Fabric Crossconnect service creates an L2 bridge between two given ports on the same device. For example, this service can be used on a SEBA pod to connect OLT devices to a BNG via the aggregation switch.
+
+A fabric crossconnect entry is a tuple (`deviceid`, `vlanid`, `port1`, `port2`).
+
+For more information, see [VLAN Cross Connect](https://wiki.opencord.org/display/CORD/VLAN+Cross+Connect)
+
+## Models
+
+This service is comprised of three models:
+
+- `FabricCrossconnectService` global service-related parameters, such as the name of the service. There is currently no additional state here beyond the default `XOS` `Service` model.
+- `FabricCrossconnectServiceInstance` represents one half of a vlan crossconnect. Fields include the following:
+       - `s-tag` the vlan_id that will be connected
+       - `switch_datapath_id` switch id where the vlan crossconnect will be enacted
+       - `source_port` port number on the switch
+- `BNGPortMapping` represents the other half of a vlan crossconnect. Fields include the following:
+       - `s_tag` the vlan_id that will be connected. In addition to specifying a single vlan_id, the keyword `ANY` may be used, or a range (`123-456`) may be used.
+       - `switch_port` port number on the switch
+
+`FabricCrossconnectServiceInstance` and `BNGPortMapping` work together to create the vlan crossconnect tuple, linked by a common `s-tag`.
+
+### Example TOSCA
+
+Below is an example TOSCA recipe that creates a `FabricCrossconnectServiceInstance`:
+
+```yaml
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+  - custom_types/fabriccrossconnectservice.yaml
+  - custom_types/fabriccrossconnectserviceinstance.yaml
+description: Create a FabricCrossconnectServiceInstance
+topology_template:
+  node_templates:
+    service#fabric-crossconnect:
+      type: tosca.nodes.FabricCrossconnectService
+      properties:
+        name: fabric-crossconnect
+        must-exist: true
+
+    fcsi:
+      type: tosca.nodes.FabricCrossconnectServiceInstance
+      properties:
+        name: "custom_vm_crossconnect"
+        s_tag: 123
+        source_port: 3
+        switch_datapath_id: "of:0000000000000201"
+      requirements:
+        - owner:
+            node: service#fabric-crossconnect
+            relationship: tosca.relationships.BelongsToOne
+```
+
+Below is an example TOSCA recipe that creates a `BNGPortMapping` for a single s-tag:
+
+```yaml
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+  - custom_types/bngportmapping.yaml
+description: Create a bng port mapping
+topology_template:
+  node_templates:
+    bngmapping:
+      type: tosca.nodes.BNGPortMapping
+      properties:
+        s_tag: "222"
+        switch_port: 4
+```
+
+## Integration with other Services
+
+The western neighbor of the `FabricCrossconnectService` is typically an access service such as `VOLTService`. `FabricCrossconnectServiceInstance` participates in the dataplane chain for a given subscriber.
+
+`FabricCrossConnectService` features a method `acquire_service_instance(subscriber_service_instance)` that may be used as a helper for creating service instances. Given that many subscribers may map to a single `s_tag`, it's often the case that a single `FabricCrossconnectServiceInstance` is used by several subscribers. `acquire_service_instance` does the following:
+
+1) Check to see if an eligible `FabricCrossconnnectServiceInstance` already exists, and if so links it to the subscriber_service_instance.
+2) If no eligible `FabricCrossconnectServiceInstance` already exists, then a new one will be created and linked.
+
+## Synchronization workflow
+
+### FabricCrossconnectServiceInstance
+
+When a `FabricCrossconnectServiceInstance` is created, updated, or deleted, the synchronizer will make a REST API call to ONOS.
+
+### BNGPortMapping
+
+No specific processing is performed if a `BNGPortMapping` is created, updated, or deleted, as the workflow is driven by `FabricCrossconnectServiceInstance`. Responding to changes in `BNGPortMapping` is future work. For now it is suggested that if you need to change one, afterward you touch any `FabricCrossconnectServiceInstance` that may be affected, causing them to resynchronize.
\ No newline at end of file
diff --git a/src/fabric-crossconnect/samples/bng_mapping_any.yaml b/src/fabric-crossconnect/samples/bng_mapping_any.yaml
new file mode 100644 (file)
index 0000000..cc9c2b8
--- /dev/null
@@ -0,0 +1,27 @@
+# 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/bngportmapping.yaml
+description: Create a bng port mapping
+topology_template:
+  node_templates:
+    bngmapping:
+      type: tosca.nodes.BNGPortMapping
+      properties:
+        s_tag: "any"
+        switch_port: 4
diff --git a/src/fabric-crossconnect/samples/bng_mapping_range.yaml b/src/fabric-crossconnect/samples/bng_mapping_range.yaml
new file mode 100644 (file)
index 0000000..5286ff5
--- /dev/null
@@ -0,0 +1,27 @@
+# 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/bngportmapping.yaml
+description: Create a bng port mapping
+topology_template:
+  node_templates:
+    bngmapping:
+      type: tosca.nodes.BNGPortMapping
+      properties:
+        s_tag: "220-225"
+        switch_port: 4
diff --git a/src/fabric-crossconnect/samples/bng_mapping_single.yaml b/src/fabric-crossconnect/samples/bng_mapping_single.yaml
new file mode 100644 (file)
index 0000000..92c24d8
--- /dev/null
@@ -0,0 +1,27 @@
+# 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/bngportmapping.yaml
+description: Create a bng port mapping
+topology_template:
+  node_templates:
+    bngmapping:
+      type: tosca.nodes.BNGPortMapping
+      properties:
+        s_tag: "222"
+        switch_port: 4
diff --git a/src/fabric-crossconnect/samples/vm_crossconnect.yaml b/src/fabric-crossconnect/samples/vm_crossconnect.yaml
new file mode 100644 (file)
index 0000000..c07ccfb
--- /dev/null
@@ -0,0 +1,40 @@
+# 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/fabriccrossconnectservice.yaml
+  - custom_types/fabriccrossconnectserviceinstance.yaml
+description: Create a FabricCrossconnectServiceInstance
+topology_template:
+  node_templates:
+    service#fabric-crossconnect:
+      type: tosca.nodes.FabricCrossconnectService
+      properties:
+        name: fabric-crossconnect
+        must-exist: true
+
+    fcsi:
+      type: tosca.nodes.FabricCrossconnectServiceInstance
+      properties:
+        name: "custom_vm_crossconnect"
+        s_tag: 123
+        source_port: 3
+        switch_datapath_id: "of:0000000000000201"
+      requirements:
+        - owner:
+            node: service#fabric-crossconnect
+            relationship: tosca.relationships.BelongsToOne
diff --git a/src/fabric-crossconnect/xos/synchronizer/config.yaml b/src/fabric-crossconnect/xos/synchronizer/config.yaml
new file mode 100644 (file)
index 0000000..33a76f0
--- /dev/null
@@ -0,0 +1,36 @@
+
+# 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: fabric-crossconnect
+required_models:
+  - FabricCrossconnectService
+  - FabricCrossconnectServiceInstance
+  - ServiceInstanceLink
+steps_dir: "/opt/xos/synchronizers/fabric-crossconnect/steps"
+model_policies_dir: "/opt/xos/synchronizers/fabric-crossconnect/model_policies"
+sys_dir: "/opt/xos/synchronizers/fabric-crossconnect/sys"
+models_dir: "/opt/xos/synchronizers/fabric-crossconnect/models"
+event_steps_dir: "/opt/xos/synchronizers/fabric-crossconnect/event_steps"
+logging:
+  version: 1
+  handlers:
+    console:
+      class: logging.StreamHandler
+  loggers:
+    'multistructlog':
+      handlers:
+          - console
+      level: DEBUG
diff --git a/src/fabric-crossconnect/xos/synchronizer/event_steps/kubernetes_event.py b/src/fabric-crossconnect/xos/synchronizer/event_steps/kubernetes_event.py
new file mode 100644 (file)
index 0000000..5ff561e
--- /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 FabricCrossconnectService, FabricCrossconnectServiceInstance, 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_fabric_onos(service):
+        service = Service.objects.get(id=service.id)
+
+        # get the onos_fabric service
+        fabric_onos = [s.leaf_model for s in service.provider_services if "onos" in s.name.lower()]
+
+        if len(fabric_onos) == 0:
+            raise Exception('Cannot find ONOS service in provider_services of Fabric-Crossconnect')
+
+        return fabric_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 FabricCrossconnectService.objects.all():
+            fabric_onos = KubernetesPodDetailsEventStep.get_fabric_onos(service)
+            if (fabric_onos.name.lower() != xos_service.lower()):
+                continue
+
+            for service_instance in service.service_instances.all():
+                log.info("Dirtying FabricCrossconnectServiceInstance", 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/fabric-crossconnect/xos/synchronizer/event_steps/test_kubernetes_event.py b/src/fabric-crossconnect/xos/synchronizer/event_steps/test_kubernetes_event.py
new file mode 100644 (file)
index 0000000..5627b5d
--- /dev/null
@@ -0,0 +1,211 @@
+# 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'))
+
+        # Setting up the config module
+        from xosconfig import Config
+        config = os.path.join(test_path, "../test_fabric_crossconnect_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("fabric-crossconnect", "fabric-crossconnect.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 = FabricCrossconnectService(name="myfcservice",
+                                                   id=1112,
+                                                   backend_code=1,
+                                                   backend_status="succeeded",
+                                                   provider_services=[self.onos])
+
+        self.fcsi1 = FabricCrossconnectServiceInstance(name="myfcsi1",
+                                                       owner=self.fcservice,
+                                                       backend_code=1,
+                                                       backend_status="succeeded")
+
+        self.fcsi2 = FabricCrossconnectServiceInstance(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(FabricCrossconnectService.objects, "get_items") as fcservice_objects, \
+             patch.object(Service.objects, "get_items") as service_objects, \
+             patch.object(FabricCrossconnectServiceInstance, "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(FabricCrossconnectService.objects, "get_items") as fcservice_objects, \
+             patch.object(Service.objects, "get_items") as service_objects, \
+             patch.object(FabricCrossconnectServiceInstance, "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(FabricCrossconnectService.objects, "get_items") as fcservice_objects, \
+             patch.object(Service.objects, "get_items") as service_objects, \
+             patch.object(FabricCrossconnectServiceInstance, "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(FabricCrossconnectService.objects, "get_items") as fcservice_objects, \
+             patch.object(Service.objects, "get_items") as service_objects, \
+             patch.object(FabricCrossconnectServiceInstance, "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/fabric-crossconnect/xos/synchronizer/fabric-crossconnect-synchronizer.py b/src/fabric-crossconnect/xos/synchronizer/fabric-crossconnect-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/fabric-crossconnect/xos/synchronizer/model_policies/model_policy_fabriccrossconnectserviceinstance.py b/src/fabric-crossconnect/xos/synchronizer/model_policies/model_policy_fabriccrossconnectserviceinstance.py
new file mode 100644 (file)
index 0000000..5a61506
--- /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.
+
+
+from synchronizers.new_base.modelaccessor import FabricCrossconnectServiceInstance, ServiceInstance, model_accessor
+from synchronizers.new_base.policy import Policy
+from synchronizers.new_base.exceptions import *
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class FabricCrossconnectServiceInstancePolicy(Policy):
+    model_name = "FabricCrossconnectServiceInstance"
+
+    def handle_create(self, service_instance):
+        return self.handle_update(service_instance)
+
+    def handle_update(self, service_instance):
+        log.info("Handle_update Fabric Crossconnect Service Instance", service_instance=service_instance)
+
+        if (service_instance.link_deleted_count > 0) and (not service_instance.provided_links.exists()):
+            # If this instance has no links pointing to it, delete
+            self.handle_delete(service_instance)
+            if FabricCrossconnectServiceInstance.objects.filter(id=service_instance.id).exists():
+                service_instance.delete()
+            return
+
+    def handle_delete(self, service_instance):
+        log.info("Handle_delete Fabric-Crossconnect Service Instance", service_instance=service_instance)
+
+
diff --git a/src/fabric-crossconnect/xos/synchronizer/model_policies/test_model_policy_fabriccrossconnectserviceinstance.py b/src/fabric-crossconnect/xos/synchronizer/model_policies/test_model_policy_fabriccrossconnectserviceinstance.py
new file mode 100644 (file)
index 0000000..bccdfdc
--- /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
+
+import functools
+from mock import patch, call, Mock, PropertyMock, MagicMock
+import requests_mock
+import multistructlog
+from multistructlog import create_logger
+
+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
+
+def mock_get_westbound_service_instance_properties(props, prop):
+    return props[prop]
+
+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 TestPolicyFabricCrossconnectServiceInstance(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_fabric_crossconnect_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("fabric-crossconnect", "fabric-crossconnect.xproto")])
+        import synchronizers.new_base.modelaccessor
+
+        from mock_modelaccessor import MockObjectList
+        self.MockObjectList = MockObjectList
+
+        from model_policy_fabriccrossconnectserviceinstance import FabricCrossconnectServiceInstancePolicy, \
+            model_accessor
+
+        # import all class names to globals
+        for (k, v) in model_accessor.all_model_classes.items():
+            globals()[k] = v
+
+        self.policy_step = FabricCrossconnectServiceInstancePolicy
+        self.policy_step.log = Mock()
+
+        # mock onos-fabric
+        self.onos_fabric = Service(name = "onos-fabric",
+                              rest_hostname = "onos-fabric",
+                              rest_port = "8181",
+                              rest_username = "onos",
+                              rest_password = "rocks")
+
+        self.service = FabricCrossconnectService(name = "fcservice",
+                                                 provider_services = [self.onos_fabric])
+
+    def mock_westbound(self, fsi, s_tag, switch_datapath_id, switch_port):
+        # Mock out a ServiceInstance so the syncstep can call get_westbound_service_instance_properties on it
+        si = ServiceInstance(id=fsi.id)
+        si.get_westbound_service_instance_properties = functools.partial(
+            mock_get_westbound_service_instance_properties,
+            {"s_tag": s_tag,
+             "switch_datapath_id": switch_datapath_id,
+             "switch_port": switch_port})
+
+        fsi.provided_links=Mock(exists=Mock(return_value=True))
+
+        return si
+
+    def test_handle_update(self):
+        with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
+            patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
+
+            fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, s_tag=None, source_port=None,
+                                                    switch_datapath_id=None)
+
+            serviceinstance_objects.return_value = [fsi]
+
+            si = self.mock_westbound(fsi, s_tag=111, switch_datapath_id = "of:0000000000000201", switch_port = 3)
+            serviceinstance_objects.return_value = [si]
+
+            self.policy_step().handle_update(fsi)
+
+    def tearDown(self):
+        self.o = None
+        sys.path = self.sys_path_save
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/src/fabric-crossconnect/xos/synchronizer/models/convenience/fabric_crossconnect_service.py b/src/fabric-crossconnect/xos/synchronizer/models/convenience/fabric_crossconnect_service.py
new file mode 100644 (file)
index 0000000..c2bb8bf
--- /dev/null
@@ -0,0 +1,111 @@
+
+# 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
+
+class ORMWrapperFabricCrossconnectService(ORMWrapperService):
+
+    """ Calling convention. Assume the subscribing service does approximately (needs some checks to see
+        if the methods exist before calling them) the following in its model_policy:
+
+        if not eastbound_service.validate_links(self):
+             eastbound_service.acquire_service_instance(self)
+    """
+
+    def acquire_service_instance(self, subscriber_service_instance):
+        """ Given a subscriber_service_instance:
+              1) If there is an eligible provider_service_instance that can be used, then link to it
+              2) Otherwise, create a new provider_service_instance and link to it.
+        """
+        (s_tag, switch_datapath_id, source_port) = self._get_west_fields(subscriber_service_instance)
+
+        FabricCrossconnectServiceInstance = self.stub.FabricCrossconnectServiceInstance
+        ServiceInstanceLink = self.stub.ServiceInstanceLink
+
+        candidates = FabricCrossconnectServiceInstance.objects.filter(owner_id=self.id,
+                                                                      s_tag=s_tag,
+                                                                      switch_datapath_id=switch_datapath_id,
+                                                                      source_port=source_port)
+
+        if candidates:
+            provider_service_instance = candidates[0]
+        else:
+            provider_service_instance = FabricCrossconnectServiceInstance(owner=self,
+                                                                        s_tag=s_tag,
+                                                                        switch_datapath_id=switch_datapath_id,
+                                                                        source_port=source_port)
+            provider_service_instance.save()
+
+        # NOTE: Lack-of-atomicity vulnerability -- provider_service_instance could be deleted before we created the
+        # link.
+
+        link = ServiceInstanceLink(provider_service_instance=provider_service_instance,
+                                   subscriber_service_instance=subscriber_service_instance)
+        link.save()
+
+        return provider_service_instance
+
+    def validate_links(self, subscriber_service_instance):
+        """ Validate existing links between the provider and subscriber service instances. If a valid link exists,
+            then return it. Return [] otherwise.
+
+            As a side-effect, delete any invalid links.
+        """
+
+        # Short-cut -- if there are no subscriber links then we can skip getting all the properties.
+        if not subscriber_service_instance.subscribed_links.exists():
+            return None
+
+        (s_tag, switch_datapath_id, source_port) = self._get_west_fields(subscriber_service_instance)
+
+        matched = []
+        for link in subscriber_service_instance.subscribed_links.all():
+            if link.provider_service_instance.owner.id == self.id:
+                fcsi = link.provider_service_instance.leaf_model
+                if (fcsi.s_tag == s_tag) and (fcsi.switch_datapath_id == switch_datapath_id) and \
+                    (fcsi.source_port == source_port):
+                    matched.append(fcsi)
+                else:
+                    link.delete()
+        return matched
+
+    def _get_west_fields(self, subscriber_si):
+        """ _get_west_fields()
+
+            Helper function to inspect westbound service instance for fields that will be used inside of
+            FabricCrossconnectServiceInstance.
+        """
+
+        s_tag = subscriber_si.get_westbound_service_instance_properties("s_tag", include_self=True)
+        switch_datapath_id = subscriber_si.get_westbound_service_instance_properties("switch_datapath_id", include_self=True)
+        source_port = subscriber_si.get_westbound_service_instance_properties("switch_port", include_self=True)
+
+        if (s_tag is None):
+            raise Exception("Subscriber ServiceInstance %s s-tag is None" % subscriber_si.id)
+
+        if (not switch_datapath_id):
+            raise Exception("Subscriber ServiceInstance %s switch_datapath_id is unset" % subscriber_si.id)
+
+        if (source_port is None):
+            raise Exception("Subscriber ServiceInstance %s switch_port is None" % subscriber_si.id)
+
+        s_tag = int(s_tag)
+        source_port = int(source_port)
+
+        return (s_tag, switch_datapath_id, source_port)
+
+register_convenience_wrapper("FabricCrossconnectService", ORMWrapperFabricCrossconnectService)
diff --git a/src/fabric-crossconnect/xos/synchronizer/models/fabric-crossconnect.xproto b/src/fabric-crossconnect/xos/synchronizer/models/fabric-crossconnect.xproto
new file mode 100644 (file)
index 0000000..bbcfa66
--- /dev/null
@@ -0,0 +1,22 @@
+option name = "fabric-crossconnect";
+option app_label = "fabric-crossconnect";
+option legacy = "True";
+
+message FabricCrossconnectService (Service){
+    option verbose_name = "Fabric Crossconnect Service";
+    option description="Fabric Crossconnect implementation";
+}
+
+message FabricCrossconnectServiceInstance (ServiceInstance){
+    option verbose_name = "Fabric Crossconnect Service Instance";
+    option owner_class_name="FabricCrossconnectService";
+
+    required int32 s_tag = 1 [help_text = "s-tag"];
+    required string switch_datapath_id = 2 [help_text = "switch datapath id"];
+    required int32 source_port = 3 [help_text = "source port of fabric crossconnect"];
+}
+
+message BNGPortMapping (XOSBase) {
+    required string s_tag = 1 [help_text = "Single s-tag, range of s-tags, or ANY", null = False, db_index = False, blank = False, unique=True, tosca_key=True];
+    required int32 switch_port = 2 [help_text = "Port Number", null = False, db_index = False, blank = False];
+}
diff --git a/src/fabric-crossconnect/xos/synchronizer/models/models.py b/src/fabric-crossconnect/xos/synchronizer/models/models.py
new file mode 100644 (file)
index 0000000..8c75a2e
--- /dev/null
@@ -0,0 +1,53 @@
+# 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 xos.exceptions import XOSValidationError
+
+from models_decl import FabricCrossconnectService_decl, FabricCrossconnectServiceInstance_decl, BNGPortMapping_decl
+
+class FabricCrossconnectService(FabricCrossconnectService_decl):
+    class Meta:
+        proxy = True
+
+class FabricCrossconnectServiceInstance(FabricCrossconnectServiceInstance_decl):
+   class Meta:
+        proxy = True
+
+class BNGPortMapping(BNGPortMapping_decl):
+    class Meta:
+        proxy = True
+
+    def validate_range(self, pattern):
+        for this_range in pattern.split(","):
+            this_range = this_range.strip()
+            if "-" in this_range:
+                (first, last) = this_range.split("-")
+                try:
+                    int(first.strip())
+                    int(last.strip())
+                except ValueError:
+                    raise XOSValidationError("Malformed range %s" % pattern)
+            elif this_range.lower()=="any":
+                pass
+            else:
+                try:
+                    int(this_range)
+                except ValueError:
+                    raise XOSValidationError("Malformed range %s" % pattern)
+
+    def save(self, *args, **kwargs):
+        self.validate_range(self.s_tag)
+
+        super(BNGPortMapping, self).save(*args, **kwargs)
+
diff --git a/src/fabric-crossconnect/xos/synchronizer/models/test_models.py b/src/fabric-crossconnect/xos/synchronizer/models/test_models.py
new file mode 100755 (executable)
index 0000000..3d42609
--- /dev/null
@@ -0,0 +1,124 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import os, sys
+from mock import patch, Mock, MagicMock
+
+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")
+
+# mocking XOS exception, as they're based in Django
+class Exceptions:
+    XOSValidationError = Exception
+    XOSProgrammingError = Exception
+    XOSPermissionDenied = Exception
+
+class XOS:
+    exceptions = Exceptions
+
+class TestFabricCrossconnectModels(unittest.TestCase):
+    def setUp(self):
+
+        self.sys_path_save = sys.path
+        sys.path.append(xos_dir)
+
+        self.xos = XOS
+
+        self.models_decl = Mock()
+        self.models_decl.BNGPortMapping_decl = MagicMock
+        self.models_decl.BNGPortMapping_decl.save = Mock()
+        self.models_decl.BNGPortMapping_decl.objects = Mock()
+        self.models_decl.BNGPortMapping_decl.objects.filter.return_value = []
+
+
+        modules = {
+            'xos.exceptions': self.xos.exceptions,
+            'models_decl': self.models_decl
+        }
+
+        self.module_patcher = patch.dict('sys.modules', modules)
+        self.module_patcher.start()
+
+        self.volt = Mock()
+
+        from models import BNGPortMapping
+
+        self.BNGPortMapping = BNGPortMapping()
+
+    def tearDown(self):
+        sys.path = self.sys_path_save
+
+    def test_validate_range_single(self):
+        bpm = self.BNGPortMapping()
+        bpm.validate_range("123")
+
+    def test_validate_range_commas(self):
+        bpm = self.BNGPortMapping()
+        bpm.validate_range("123, 456")
+
+    def test_validate_range_ANY(self):
+        bpm = self.BNGPortMapping()
+        bpm.validate_range("ANY")
+        bpm.validate_range("any")
+
+    def test_validate_range_dash(self):
+        bpm = self.BNGPortMapping()
+        bpm.validate_range("123-456")
+
+    def test_validate_dash_commas(self):
+        bpm = self.BNGPortMapping()
+        bpm.validate_range("123-456, 789 - 1000")
+
+    def test_validate_range_empty(self):
+        bpm = self.BNGPortMapping()
+        with self.assertRaises(Exception) as e:
+            bpm.validate_range("")
+
+        self.assertEqual(e.exception.message, 'Malformed range ')
+
+    def test_validate_range_none(self):
+        bpm = self.BNGPortMapping()
+        with self.assertRaises(Exception) as e:
+            bpm.validate_range("")
+
+        self.assertEqual(e.exception.message, 'Malformed range ')
+
+    def test_validate_range_all(self):
+        bpm = self.BNGPortMapping()
+        with self.assertRaises(Exception) as e:
+            bpm.validate_range("badstring")
+
+        self.assertEqual(e.exception.message, 'Malformed range badstring')
+
+    def test_validate_half_range(self):
+        bpm = self.BNGPortMapping()
+        with self.assertRaises(Exception) as e:
+            bpm.validate_range("123-")
+
+        self.assertEqual(e.exception.message, 'Malformed range 123-')
+
+    def test_validate_half_comma(self):
+        bpm = self.BNGPortMapping()
+        with self.assertRaises(Exception) as e:
+            bpm.validate_range("123,")
+
+        self.assertEqual(e.exception.message, 'Malformed range 123,')
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/src/fabric-crossconnect/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py b/src/fabric-crossconnect/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py
new file mode 100644 (file)
index 0000000..5299fc1
--- /dev/null
@@ -0,0 +1,168 @@
+# 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.syncstep import SyncStep, DeferredException
+from synchronizers.new_base.modelaccessor import model_accessor, FabricCrossconnectServiceInstance, ServiceInstance, BNGPortMapping
+
+from xosconfig import Config
+from multistructlog import create_logger
+import urllib
+import requests
+from requests.auth import HTTPBasicAuth
+
+
+class SyncFabricCrossconnectServiceInstance(SyncStep):
+    provides = [FabricCrossconnectServiceInstance]
+    log = create_logger(Config().get('logging'))
+
+    observes = FabricCrossconnectServiceInstance
+
+    @staticmethod
+    def format_url(url):
+        if 'http' in url:
+            return url
+        else:
+            return 'http://%s' % url
+
+    @staticmethod
+    def get_fabric_onos_info(si):
+
+        # get the fabric-crossconnect service
+        fabric_crossconnect = si.owner
+
+        # get the onos_fabric service
+        fabric_onos = [s.leaf_model for s in fabric_crossconnect.provider_services if "onos" in s.name.lower()]
+
+        if len(fabric_onos) == 0:
+            raise Exception('Cannot find ONOS service in provider_services of Fabric-Crossconnect')
+
+        fabric_onos = fabric_onos[0]
+
+        return {
+            'url': SyncFabricCrossconnectServiceInstance.format_url("%s:%s" % (fabric_onos.rest_hostname, fabric_onos.rest_port)),
+            'user': fabric_onos.rest_username,
+            'pass': fabric_onos.rest_password
+        }
+
+    def make_handle(self, s_tag, switch_datapath_id):
+        # Generate a backend_handle that uniquely identifies the cross connect. ONOS doesn't provide us a handle, so
+        # we make up our own. This helps us to detect other FabricCrossconnectServiceInstance using the same
+        # entry, as well as to be able to extract the necessary information to delete the entry later.
+        return "%d/%s" % (s_tag, switch_datapath_id)
+
+    def extract_handle(self, backend_handle):
+        (s_tag, switch_datapath_id) = backend_handle.split("/",1)
+        s_tag = int(s_tag)
+        return (s_tag, switch_datapath_id)
+
+    def range_matches(self, value, pattern):
+        value=int(value)
+        for this_range in pattern.split(","):
+            this_range = this_range.strip()
+            if "-" in this_range:
+                (first, last) = this_range.split("-")
+                first = int(first.strip())
+                last = int(last.strip())
+                if (value>=first) and (value<=last):
+                    return True
+            elif this_range.lower()=="any":
+                return True
+            else:
+                if (value==int(this_range)):
+                    return True
+        return False
+
+    def find_bng(self, s_tag):
+        # See if there's a mapping for our s-tag directly
+        bng_mappings = BNGPortMapping.objects.filter(s_tag=str(s_tag))
+        if bng_mappings:
+            return bng_mappings[0]
+
+        # TODO(smbaker): Examine miss performance, and if necessary set a flag in the save method to allow filtering
+        # of mappings based on whether they are ranges or any.
+
+        # See if there are any ranges or "any" that match
+        for bng_mapping in BNGPortMapping.objects.all():
+            if self.range_matches(s_tag, bng_mapping.s_tag):
+                 return bng_mapping
+
+        return None
+
+    def sync_record(self, o):
+        self.log.info("Sync'ing Fabric Crossconnect Service Instance", service_instance=o)
+
+        if (o.policed is None) or (o.policed < o.updated):
+            raise DeferredException("Waiting for model_policy to run on fcsi %s" % o.id)
+
+        onos = self.get_fabric_onos_info(o)
+
+        si = ServiceInstance.objects.get(id=o.id)
+
+        if (o.s_tag is None):
+            raise Exception("Cannot sync FabricCrossconnectServiceInstance if s_tag is None on fcsi %s" % o.id)
+
+        if (o.source_port is None):
+            raise Exception("Cannot sync FabricCrossconnectServiceInstance if source_port is None on fcsi %s" % o.id)
+
+        if (not o.switch_datapath_id):
+            raise Exception("Cannot sync FabricCrossconnectServiceInstance if switch_datapath_id is unset on fcsi %s" % o.id)
+
+        bng_mapping = self.find_bng(s_tag = o.s_tag)
+        if not bng_mapping:
+            raise Exception("Unable to determine BNG port for s_tag %s" % o.s_tag)
+        east_port = bng_mapping.switch_port
+
+        data = { "deviceId": o.switch_datapath_id,
+                 "vlanId": o.s_tag,
+                 "ports": [ int(o.source_port), int(east_port) ] }
+
+        url = onos['url'] + '/onos/segmentrouting/xconnect'
+
+        self.log.info("Sending request to ONOS", url=url, body=data)
+
+        r = requests.post(url, json=data, auth=HTTPBasicAuth(onos['user'], onos['pass']))
+
+        if r.status_code != 200:
+            raise Exception("Failed to create fabric crossconnect in ONOS: %s" % r.text)
+
+        # TODO(smbaker): If the o.backend_handle changed, then someone must have changed the
+        #   FabricCrossconnectServiceInstance. If so, then we potentially need to clean up the old
+        #   entry in ONOS. Furthermore, we might want to also save the two port numbers that we used,
+        #   to detect someone changing those.
+
+        o.backend_handle = self.make_handle(o.s_tag, o.switch_datapath_id)
+        o.save(update_fields=["backend_handle"])
+
+        self.log.info("ONOS response", res=r.text)
+
+    def delete_record(self, o):
+        self.log.info("Deleting Fabric Crossconnect Service Instance", service_instance=o)
+
+        if o.backend_handle:
+            onos = self.get_fabric_onos_info(o)
+
+            # backend_handle has everything we need in it to delete this entry.
+            (s_tag, switch_datapath_id) = self.extract_handle(o.backend_handle)
+
+            data = { "deviceId": switch_datapath_id,
+                     "vlanId": s_tag }
+
+            url = onos['url'] + '/onos/segmentrouting/xconnect'
+
+            r = requests.delete(url, json=data, auth=HTTPBasicAuth(onos['user'], onos['pass']))
+
+            if r.status_code != 204:
+                raise Exception("Failed to remove fabric crossconnect in ONOS: %s" % r.text)
+
+            self.log.info("ONOS response", res=r.text)
diff --git a/src/fabric-crossconnect/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py b/src/fabric-crossconnect/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py
new file mode 100644 (file)
index 0000000..9f07687
--- /dev/null
@@ -0,0 +1,300 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+import functools
+from mock import patch, call, Mock, PropertyMock, MagicMock
+import requests_mock
+import multistructlog
+from multistructlog import create_logger
+
+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
+
+def mock_get_westbound_service_instance_properties(props, prop):
+    return props[prop]
+
+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 TestSyncFabricCrossconnectServiceInstance(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_fabric_crossconnect_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("fabric-crossconnect", "fabric-crossconnect.xproto")])
+        import synchronizers.new_base.modelaccessor
+
+        from sync_fabric_crossconnect_service_instance import SyncFabricCrossconnectServiceInstance, model_accessor, DeferredException
+
+        # import all class names to globals
+        for (k, v) in model_accessor.all_model_classes.items():
+            globals()[k] = v
+
+        self.sync_step = SyncFabricCrossconnectServiceInstance
+        self.sync_step.log = Mock()
+
+        # mock onos-fabric
+        self.onos_fabric = Service(name = "onos-fabric",
+                              rest_hostname = "onos-fabric",
+                              rest_port = "8181",
+                              rest_username = "onos",
+                              rest_password = "rocks")
+
+        self.service = FabricCrossconnectService(name = "fcservice",
+                                                 provider_services = [self.onos_fabric])
+
+    def mock_westbound(self, fsi, s_tag, switch_datapath_id, switch_port):
+        # Mock out a ServiceInstance so the syncstep can call get_westbound_service_instance_properties on it
+        si = ServiceInstance(id=fsi.id)
+        si.get_westbound_service_instance_properties = functools.partial(
+            mock_get_westbound_service_instance_properties,
+            {"s_tag": s_tag,
+             "switch_datapath_id": switch_datapath_id,
+             "switch_port": switch_port})
+        return si
+
+    def test_format_url(self):
+        url = self.sync_step().format_url("foo.com/bar")
+        self.assertEqual(url, "http://foo.com/bar")
+
+        url = self.sync_step().format_url("http://foo.com/bar")
+        self.assertEqual(url, "http://foo.com/bar")
+
+    def test_make_handle_extract_handle(self):
+        h = self.sync_step().make_handle(222, "of:0000000000000201")
+        (s_tag, switch_datapath_id) = self.sync_step().extract_handle(h)
+
+        self.assertEqual(s_tag, 222)
+        self.assertEqual(switch_datapath_id, "of:0000000000000201")
+
+    def test_get_fabric_onos_init(self):
+        fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service)
+
+        d = self.sync_step().get_fabric_onos_info(fsi)
+
+        self.assertEqual(d["url"], "http://onos-fabric:8181")
+        self.assertEqual(d["user"], "onos")
+        self.assertEqual(d["pass"], "rocks")
+
+    def test_range_matches_single(self):
+        self.assertTrue(self.sync_step().range_matches(123, "123"))
+
+    def test_range_matches_single_incorrect(self):
+        self.assertFalse(self.sync_step().range_matches(123, "456"))
+
+    def test_range_matches_range(self):
+        self.assertTrue(self.sync_step().range_matches(123, "122-124"))
+
+    def test_range_matches_range_incorrect(self):
+        self.assertFalse(self.sync_step().range_matches(123, "110-113"))
+
+    def test_range_matches_any(self):
+        self.assertTrue(self.sync_step().range_matches(123, "ANY"))
+        self.assertTrue(self.sync_step().range_matches(123, "any"))
+
+    def test_find_bng_single(self):
+        with patch.object(BNGPortMapping.objects, "get_items") as bng_objects, \
+                patch.object(self.sync_step, "range_matches") as range_matches:
+            bngmapping = BNGPortMapping(s_tag="111", switch_port=4)
+            bng_objects.return_value = [bngmapping]
+
+            # this should not be called
+            range_matches.return_value = False
+
+            found_bng = self.sync_step().find_bng(111)
+            self.assertTrue(found_bng)
+            self.assertEqual(found_bng.switch_port, 4)
+
+            range_matches.assert_not_called()
+
+    def test_find_bng_any(self):
+        with patch.object(BNGPortMapping.objects, "get_items") as bng_objects:
+            bngmapping = BNGPortMapping(s_tag="ANY", switch_port=4)
+            bng_objects.return_value = [bngmapping]
+
+            found_bng = self.sync_step().find_bng(111)
+            self.assertTrue(found_bng)
+            self.assertEqual(found_bng.switch_port, 4)
+
+    def test_find_bng_range(self):
+        with patch.object(BNGPortMapping.objects, "get_items") as bng_objects:
+            bngmapping = BNGPortMapping(s_tag="100-200", switch_port=4)
+            bng_objects.return_value = [bngmapping]
+
+            found_bng = self.sync_step().find_bng(111)
+            self.assertTrue(found_bng)
+            self.assertEqual(found_bng.switch_port, 4)
+
+    @requests_mock.Mocker()
+    def test_sync(self, m):
+        with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
+            patch.object(BNGPortMapping.objects, "get_items") as bng_objects, \
+            patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
+
+            fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, s_tag=111, source_port=3,
+                                                    switch_datapath_id="of:0000000000000201", updated=1, policed=2)
+
+            serviceinstance_objects.return_value = [fsi]
+
+            bngmapping = BNGPortMapping(s_tag="111", switch_port=4)
+            bng_objects.return_value = [bngmapping]
+
+            desired_data = {"deviceId": "of:0000000000000201",
+                    "vlanId": 111,
+                    "ports": [3, 4]}
+
+            m.post("http://onos-fabric:8181/onos/segmentrouting/xconnect",
+                   status_code=200,
+                   additional_matcher=functools.partial(match_json, desired_data))
+
+            self.sync_step().sync_record(fsi)
+            self.assertTrue(m.called)
+
+            self.assertEqual(fsi.backend_handle, "111/of:0000000000000201")
+            fcsi_save.assert_called()
+
+    def test_sync_no_bng_mapping(self):
+        with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
+            patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
+
+            fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, s_tag=111, source_port=3,
+                                                    switch_datapath_id="of:0000000000000201", updated=1, policed=2)
+
+            serviceinstance_objects.return_value = [fsi]
+
+            with self.assertRaises(Exception) as e:
+                self.sync_step().sync_record(fsi)
+
+            self.assertEqual(e.exception.message, "Unable to determine BNG port for s_tag 111")
+
+    def test_sync_not_policed(self):
+        with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
+            patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
+
+            fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, source_port=3,
+                                                    switch_datapath_id="of:0000000000000201", updated=1, policed=0)
+
+            serviceinstance_objects.return_value = [fsi]
+
+            with self.assertRaises(Exception) as e:
+                self.sync_step().sync_record(fsi)
+
+            self.assertEqual(e.exception.message, "Waiting for model_policy to run on fcsi 7777")
+
+    def test_sync_no_s_tag(self):
+        with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
+            patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
+
+            fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, source_port=3,
+                                                    switch_datapath_id="of:0000000000000201", updated=1, policed=2)
+
+            serviceinstance_objects.return_value = [fsi]
+
+            with self.assertRaises(Exception) as e:
+                self.sync_step().sync_record(fsi)
+
+            self.assertEqual(e.exception.message, "Cannot sync FabricCrossconnectServiceInstance if s_tag is None on fcsi 7777")
+
+    def test_sync_no_switch_datapath_id(self):
+        with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
+            patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
+
+            fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, source_port=3, s_tag=111,
+                                                    updated=1, policed=2)
+
+            serviceinstance_objects.return_value = [fsi]
+
+            with self.assertRaises(Exception) as e:
+                self.sync_step().sync_record(fsi)
+
+            self.assertEqual(e.exception.message, "Cannot sync FabricCrossconnectServiceInstance if switch_datapath_id is unset on fcsi 7777")
+
+    def test_sync_no_source_port(self):
+        with patch.object(ServiceInstance.objects, "get_items") as serviceinstance_objects, \
+            patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
+
+            fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service, s_tag=111,
+                                                    switch_datapath_id="of:0000000000000201", updated=1, policed=2)
+
+            serviceinstance_objects.return_value = [fsi]
+
+            with self.assertRaises(Exception) as e:
+                self.sync_step().sync_record(fsi)
+
+            self.assertEqual(e.exception.message, "Cannot sync FabricCrossconnectServiceInstance if source_port is None on fcsi 7777")
+
+    @requests_mock.Mocker()
+    def test_delete(self, m):
+        with patch.object(FabricCrossconnectServiceInstance.objects, "get_items") as fcsi_objects, \
+                patch.object(FabricCrossconnectServiceInstance, "save") as fcsi_save:
+            fsi = FabricCrossconnectServiceInstance(id=7777, owner=self.service,
+                                                    backend_handle="111/of:0000000000000201",
+                                                    enacted=True)
+
+            fcsi_objects.return_value=[fsi]
+
+            desired_data = {"deviceId": "of:0000000000000201",
+                            "vlanId": 111}
+
+            m.delete("http://onos-fabric:8181/onos/segmentrouting/xconnect",
+                   status_code=204,
+                   additional_matcher=functools.partial(match_json, desired_data))
+
+            self.sync_step().delete_record(fsi)
+            self.assertTrue(m.called)
+
+    def tearDown(self):
+        self.o = None
+        sys.path = self.sys_path_save
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/src/fabric-crossconnect/xos/synchronizer/test_fabric_crossconnect_config.yaml b/src/fabric-crossconnect/xos/synchronizer/test_fabric_crossconnect_config.yaml
new file mode 100644 (file)
index 0000000..2dede85
--- /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.
+
+name: test-fabric-crossconnect
+accessor:
+  username: xosadmin@opencord.org
+  password: "sample"
+  kind: "testframework"
+logging:
+  version: 1
+  handlers:
+    console:
+      class: logging.StreamHandler
+  loggers:
+    'multistructlog':
+      handlers:
+          - console
diff --git a/src/fabric-crossconnect/xos/unittest.cfg b/src/fabric-crossconnect/xos/unittest.cfg
new file mode 100644 (file)
index 0000000..07b76f7
--- /dev/null
@@ -0,0 +1,13 @@
+[unittest]
+plugins=nose2.plugins.junitxml
+code-directories=synchronizer
+                 model_policies
+                 steps
+                 models
+                 event_steps
+
+[coverage]
+always-on = True
+coverage = synchronizer
+coverage-report = term
+coverage-report = html
\ No newline at end of file