--- /dev/null
+.idea
\ No newline at end of file
--- /dev/null
+# 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"]
--- /dev/null
+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'
+ }
+ }
+ }
+ }
+}
--- /dev/null
+
+ 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.
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+
+# 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
--- /dev/null
+
+# 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)
--- /dev/null
+# 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()
+
+
+
--- /dev/null
+#!/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()
--- /dev/null
+
+# 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)
+
+
--- /dev/null
+# 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()
--- /dev/null
+
+# 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)
--- /dev/null
+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];
+}
--- /dev/null
+# 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)
+
--- /dev/null
+# 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()
--- /dev/null
+# 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)
--- /dev/null
+# 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()
--- /dev/null
+
+# 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
--- /dev/null
+[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