From: xinhuili Date: Wed, 14 Aug 2019 06:03:17 +0000 (+0800) Subject: Add related patch for fabric support X-Git-Url: https://gerrit.akraino.org/r/gitweb?a=commitdiff_plain;h=d889e4412baf7831a65b4fc0e685f137a18b31b5;p=iec%2Fxconnect.git Add related patch for fabric support This patch is to enable fabric-crossconnect Signed-off-by: XINHUI LI Change-Id: I978c0479898a4f35056bbfd7e905f500ad8f919a --- diff --git a/src/fabric-crossconnect/.gitignore b/src/fabric-crossconnect/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/src/fabric-crossconnect/.gitignore @@ -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 index 0000000..005b443 --- /dev/null +++ b/src/fabric-crossconnect/Dockerfile.synchronizer @@ -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 index 0000000..a58adb4 --- /dev/null +++ b/src/fabric-crossconnect/Jenkinsfile @@ -0,0 +1,42 @@ +pipeline { + agent any + stages { + stage('Build') { + parallel { + stage('Build aarch64') { + agent { + node { + label 'aarch64' + } + + } + steps { + withDockerRegistry([ credentialsId: "fcf9c294-b8a9-4f7e-87d6-d0446f712411", url: "https://index.docker.io/v1/" ]) { + sh 'ci_scripts/push_containers.sh' + } + } + } + stage('Build x86') { + agent { + node { + label 'x86_64' + } + + } + steps { + withDockerRegistry([ credentialsId: "fcf9c294-b8a9-4f7e-87d6-d0446f712411", url: "https://index.docker.io/v1/" ]) { + sh 'ci_scripts/push_containers.sh' + } + } + } + } + } + stage('Push Manifest') { + steps { + withDockerRegistry([ credentialsId: "fcf9c294-b8a9-4f7e-87d6-d0446f712411", url: "https://index.docker.io/v1/" ]) { + sh 'ci_scripts/push_manifest.sh' + } + } + } + } +} diff --git a/src/fabric-crossconnect/LICENSE.txt b/src/fabric-crossconnect/LICENSE.txt new file mode 100644 index 0000000..d9d9d30 --- /dev/null +++ b/src/fabric-crossconnect/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016 Open Networking Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/fabric-crossconnect/VERSION b/src/fabric-crossconnect/VERSION new file mode 100644 index 0000000..65087b4 --- /dev/null +++ b/src/fabric-crossconnect/VERSION @@ -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 index 0000000..1af94ce --- /dev/null +++ b/src/fabric-crossconnect/ci_scripts/push_containers.sh @@ -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 index 0000000..b9ac357 --- /dev/null +++ b/src/fabric-crossconnect/ci_scripts/push_manifest.sh @@ -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 index 0000000..c7272e7 --- /dev/null +++ b/src/fabric-crossconnect/docs/README.md @@ -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 index 0000000..cc9c2b8 --- /dev/null +++ b/src/fabric-crossconnect/samples/bng_mapping_any.yaml @@ -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 index 0000000..5286ff5 --- /dev/null +++ b/src/fabric-crossconnect/samples/bng_mapping_range.yaml @@ -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 index 0000000..92c24d8 --- /dev/null +++ b/src/fabric-crossconnect/samples/bng_mapping_single.yaml @@ -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 index 0000000..c07ccfb --- /dev/null +++ b/src/fabric-crossconnect/samples/vm_crossconnect.yaml @@ -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 index 0000000..33a76f0 --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/config.yaml @@ -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 index 0000000..5ff561e --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/event_steps/kubernetes_event.py @@ -0,0 +1,68 @@ + +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import json +import os +import sys +from synchronizers.new_base.eventstep import EventStep +from synchronizers.new_base.modelaccessor import 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 index 0000000..5627b5d --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/event_steps/test_kubernetes_event.py @@ -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 index 0000000..337472d --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/fabric-crossconnect-synchronizer.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This imports and runs ../../xos-observer.py + +import importlib +import os +import sys +from xosconfig import Config + +base_config_file = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + '/config.yaml') +mounted_config_file = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + '/mounted_config.yaml') + +if os.path.isfile(mounted_config_file): + Config.init(base_config_file, 'synchronizer-config-schema.yaml', mounted_config_file) +else: + Config.init(base_config_file, 'synchronizer-config-schema.yaml') + +observer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../../synchronizers/new_base") +sys.path.append(observer_path) +mod = importlib.import_module("xos-synchronizer") +mod.main() diff --git a/src/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 index 0000000..5a61506 --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/model_policies/model_policy_fabriccrossconnectserviceinstance.py @@ -0,0 +1,45 @@ + +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +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 index 0000000..bccdfdc --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/model_policies/test_model_policy_fabriccrossconnectserviceinstance.py @@ -0,0 +1,131 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +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 index 0000000..c2bb8bf --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/models/convenience/fabric_crossconnect_service.py @@ -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 index 0000000..bbcfa66 --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/models/fabric-crossconnect.xproto @@ -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 index 0000000..8c75a2e --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/models/models.py @@ -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 index 0000000..3d42609 --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/models/test_models.py @@ -0,0 +1,124 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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 index 0000000..5299fc1 --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/steps/sync_fabric_crossconnect_service_instance.py @@ -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 index 0000000..9f07687 --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/steps/test_sync_fabric_crossconnect_service_instance.py @@ -0,0 +1,300 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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 index 0000000..2dede85 --- /dev/null +++ b/src/fabric-crossconnect/xos/synchronizer/test_fabric_crossconnect_config.yaml @@ -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 index 0000000..07b76f7 --- /dev/null +++ b/src/fabric-crossconnect/xos/unittest.cfg @@ -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