[CORD-2998] Create sync step to configure fabric switches and ports

Change-Id: I69a9c0ba568a008d998d83b0a4d3118d62fafbbc
diff --git a/samples/fabric.yaml b/samples/fabric.yaml
new file mode 100644
index 0000000..838c800
--- /dev/null
+++ b/samples/fabric.yaml
@@ -0,0 +1,56 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X POST --data-binary @fabric.yaml http://192.168.99.100:30007/run
+
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+  - custom_types/switch.yaml
+  - custom_types/switchport.yaml
+  - custom_types/portinterface.yaml
+description: Configures fabric switches and related ports
+topology_template:
+  node_templates:
+    switch#leaf1:
+      type: tosca.nodes.Switch
+      properties:
+        driver: ofdpa3
+        ipv4Loopback: 192.168.0.201
+        ipv4NodeSid: 17
+        isEdgeRouter: True
+        name: leaf1
+        ofId: of:0000000000000001
+        routerMac: 00:00:02:01:06:01
+        
+
+    # Setup a fabric switch port
+    port#port1:
+      type: tosca.nodes.SwitchPort
+      properties:
+        portId: 1
+      requirements:
+        - switch:
+            node: switch#leaf1
+            relationship: tosca.relationships.BelongsToOne
+
+    # Setup a fabric switch port interface
+    interface#interface1:
+      type: tosca.nodes.PortInterface
+      properties:
+        ips: 192.168.0.254/24
+        name: port1
+      requirements:
+        - port:
+            node: port#port1
+            relationship: tosca.relationships.BelongsToOne
diff --git a/xos/synchronizer/fabric_config.yaml b/xos/synchronizer/fabric_config.yaml
index 5dccbfc..84a2ea8 100644
--- a/xos/synchronizer/fabric_config.yaml
+++ b/xos/synchronizer/fabric_config.yaml
@@ -22,6 +22,8 @@
   - Instance
   - Tag
   - FabricService
+  - Switch
+  - SwitchPort
 dependency_graph: "/opt/xos/synchronizers/fabric/model-deps"
 steps_dir: "/opt/xos/synchronizers/fabric/steps"
 sys_dir: "/opt/xos/synchronizers/fabric/sys"
diff --git a/xos/synchronizer/models/fabric.xproto b/xos/synchronizer/models/fabric.xproto
index 8ecf614..ac617be 100644
--- a/xos/synchronizer/models/fabric.xproto
+++ b/xos/synchronizer/models/fabric.xproto
@@ -1,8 +1,36 @@
 option app_label = "fabric";
 option name = "fabric";
 
-message FabricService(Service){
+message FabricService(Service) {
     option verbose_name = "Fabric Service";
     
     optional bool autoconfig = 1 [default = True, help_text="Autoconfigure the fabric", null=False];
 }
+
+message Switch(XOSBase) {
+    option verbose_name = "Fabric Switch";
+
+    required string ofId = 2 [help_text = "The unique OpenFlow ID of the fabric switch", max_length = 19, null = False, db_index = False, blank = False];
+    required string name = 2 [help_text = "The unique name of the fabric switch", max_length = 254, null = False, db_index = False, blank = False];
+    required string driver = 3 [help_text = "The driver used by the SDN controller", max_length = 254, null = False, db_index = False, blank = False, default = "ofdpa3"];
+    required int32 ipv4NodeSid = 4 [help_text = "The MPLS label used by the switch [17 - 1048576]", null = False, db_index = False, blank = False];
+    required string ipv4Loopback = 5 [help_text = "Fabric loopback interface", max_length = 17, null = False, db_index = False, blank = False];
+    required string routerMac = 6 [help_text = "MAC address of the fabric switch used for all interfaces", max_length = 17, null = False, db_index = False, blank = False];
+    required bool isEdgeRouter = 7 [default = True, help_text="Whether the fabric switch is a leaf or a spine", null=False, blank = False];
+}
+
+message SwitchPort(XOSBase) {
+    option verbose_name = "Fabric Switch Port";
+
+    required manytoone switch->Switch:ports = 1 [help_text = "The fabric switch the port belongs to", db_index = True, null = False, blank = False, tosca_key=True];
+    required int32 portId = 2 [help_text = "The unique port OpenFlow port ID", null = False, db_index = False, blank = False, tosca_key=True];
+}
+
+message PortInterface(XOSBase) {
+    option verbose_name = "Fabric Port Interface";
+
+    required manytoone port->SwitchPort:interfaces = 1 [help_text = "The fabric switch port the interface belongs to", db_index = True, null = False, blank = False];
+    required string name = 2 [help_text = "The unique name of the fabric switch port", max_length = 254, null = False, db_index = False, blank = False];
+    required string ips = 3 [help_text = "The interface IP address (xxx.yyy.www.zzz/nm)", max_length = 20, null = False, db_index = False, blank = False];
+    optional int32 vlanUntagged = 4 [help_text = "The optional untagged VLAN ID for the interface", max_length = 20, null = True, db_index = False, blank = True];
+}
\ No newline at end of file
diff --git a/xos/synchronizer/steps/sync_fabric_port.py b/xos/synchronizer/steps/sync_fabric_port.py
new file mode 100644
index 0000000..0615b0f
--- /dev/null
+++ b/xos/synchronizer/steps/sync_fabric_port.py
@@ -0,0 +1,83 @@
+
+# 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 requests
+from requests.auth import HTTPBasicAuth
+from synchronizers.new_base.syncstep import SyncStep, DeferredException
+from synchronizers.new_base.modelaccessor import FabricService, SwitchPort
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class SyncFabricPort(SyncStep):
+    provides = [SwitchPort]
+    observes = SwitchPort
+
+    def get_fabric_onos_service_internal(self):
+        # There will be a ServiceInstanceLink from the FabricService to the Fabric ONOS App
+        fs = FabricService.objects.first()
+        for link in fs.subscribed_links.all():
+            if link.provider_service_instance:
+                # Cast from ServiceInstance to ONOSApp
+                service_instance = link.provider_service_instance.leaf_model
+                # Cast from Service to ONOSService
+                return service_instance.owner.leaf_model
+
+        return None
+
+    def get_fabric_onos_service(self):
+        fos = self.get_fabric_onos_service_internal()
+        if not fos:
+            raise Exception("Fabric ONOS service not found")
+        return fos
+
+    def sync_record(self, model):
+        interfaces = []
+        for intf in model.interfaces.all():
+            i = {
+                "name" : intf.name,
+                "ips" : [ intf.ips ]
+            }
+            if intf.vlanUntagged:
+                i["vlan-untagged"] = intf.vlanUntagged
+            interfaces.append(i)
+
+        # Send port config to onos-fabric netcfg
+        data = {
+          "ports": {
+            "%s/%s" % (model.switch.ofId, model.portId) : {
+              "interfaces" : interfaces
+            }
+          }
+        }
+
+        onos = self.get_fabric_onos_service()
+
+        url = 'http://%s:%s/onos/v1/network/configuration/' % (onos.rest_hostname, onos.rest_port)
+        r = requests.post(url, json=data, auth=HTTPBasicAuth(onos.rest_username, onos.rest_password))
+
+        if r.status_code != 200:
+            log.error(r.text)
+            raise Exception("Failed to add port %s into ONOS" % model.name)
+        else:
+            try:
+                print r.json()
+            except Exception:
+                print r.text
+
+    def delete_record(self, switch):
+        pass
diff --git a/xos/synchronizer/steps/sync_fabric_switch.py b/xos/synchronizer/steps/sync_fabric_switch.py
new file mode 100644
index 0000000..b29ec91
--- /dev/null
+++ b/xos/synchronizer/steps/sync_fabric_switch.py
@@ -0,0 +1,84 @@
+
+# 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 requests
+from requests.auth import HTTPBasicAuth
+from synchronizers.new_base.syncstep import SyncStep, DeferredException
+from synchronizers.new_base.modelaccessor import FabricService, Switch
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class SyncFabricSwitch(SyncStep):
+    provides = [Switch]
+    observes = Switch
+
+    def get_fabric_onos_service_internal(self):
+        # There will be a ServiceInstanceLink from the FabricService to the Fabric ONOS App
+        fs = FabricService.objects.first()
+        for link in fs.subscribed_links.all():
+            if link.provider_service_instance:
+                # cast from ServiceInstance to ONOSApp
+                service_instance = link.provider_service_instance.leaf_model
+                # cast from Service to ONOSService
+                return service_instance.owner.leaf_model
+
+        return None
+
+    def get_fabric_onos_service(self):
+        fos = self.get_fabric_onos_service_internal()
+        if not fos:
+            raise Exception("Fabric ONOS service not found")
+        return fos
+
+    def sync_record(self, model):
+        # Send device info to onos-fabric netcfg
+        data = {
+          "devices": {
+            model.ofId: {
+              "basic": {
+                "name": model.name,
+                "driver": model.driver
+              },
+              "segmentrouting" : {
+                "name" : model.name,
+                "ipv4NodeSid" : model.ipv4NodeSid,
+                "ipv4Loopback" : model.ipv4Loopback,
+                "routerMac" : model.routerMac,
+                "isEdgeRouter" : model.isEdgeRouter,
+                "adjacencySids" : []
+              }
+            }
+          }
+        }
+
+        onos = self.get_fabric_onos_service()
+
+        url = 'http://%s:%s/onos/v1/network/configuration/' % (onos.rest_hostname, onos.rest_port)
+        r = requests.post(url, json=data, auth=HTTPBasicAuth(onos.rest_username, onos.rest_password))
+
+        if r.status_code != 200:
+            log.error(r.text)
+            raise Exception("Failed to add device %s into ONOS" % model.name)
+        else:
+            try:
+                print r.json()
+            except Exception:
+                print r.text
+
+    def delete_record(self, switch):
+        pass