[CORD-3100] Refactoring and documenting the ONOS Service

Change-Id: Ida83935798a2a99b538f6ccdc55cc26f3148ffe3
diff --git a/xos/synchronizer/steps/sync_onos_app.py b/xos/synchronizer/steps/sync_onos_app.py
new file mode 100644
index 0000000..6288243
--- /dev/null
+++ b/xos/synchronizer/steps/sync_onos_app.py
@@ -0,0 +1,230 @@
+
+# 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 requests
+from requests.auth import HTTPBasicAuth
+from synchronizers.new_base.syncstep import SyncStep, DeferredException, model_accessor
+from synchronizers.new_base.modelaccessor import ONOSApp, ServiceInstance, ServiceInstanceAttribute
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+from helpers import Helpers
+
+log = create_logger(Config().get('logging'))
+log.info("config file", file=Config().get_config_file())
+
+class SyncONOSApp(SyncStep):
+    provides = [ONOSApp]
+    observes = [ONOSApp, ServiceInstanceAttribute]
+
+    def get_service_instance_attribute(self, o):
+        # NOTE this method is defined in the core convenience methods for service_instances
+        svc = ServiceInstance.objects.get(id=o.id)
+        return svc.serviceinstanceattribute_dict
+
+    def check_app_dependencies(self, deps):
+        """
+        Check if all the dependencies required by this application are installed
+        :param deps: comma separated list of application names
+        :return: bool
+        """
+        for dep in [x.strip() for x in str(deps).split(',') if x is not ""]:
+            try:
+                app = ONOSApp.objects.get(app_id=dep)
+                if not app.backend_code == 1:
+                    # backend_code == 1 means that the app has been pushed
+                    return False
+            except IndexError, e:
+                return False
+        return True
+
+    def add_config(self, o):
+        log.info("Adding config %s" % o.name, model=o.tologdict())
+        # getting onos url and auth
+        onos_url = "%s:%s" % (Helpers.format_url(o.service_instance.leaf_model.owner.leaf_model.rest_hostname), o.service_instance.leaf_model.owner.leaf_model.rest_port)
+        onos_basic_auth = HTTPBasicAuth(o.service_instance.leaf_model.owner.leaf_model.rest_username, o.service_instance.leaf_model.owner.leaf_model.rest_password)
+
+        # push configs (if any)
+        url = o.name
+        if url[0] == "/":
+            # strip initial /
+            url = url[1:]
+
+        url = '%s/%s' % (onos_url, url)
+        value = json.loads(o.value)
+        request = requests.post(url, json=value, auth=onos_basic_auth)
+
+        if request.status_code != 200:
+            log.error("Request failed", response=request.text)
+            raise Exception("Failed to add config %s in ONOS:  %s" % (url, request.text))
+
+    def activate_app(self, o, onos_url, onos_basic_auth):
+        log.info("Activating app %s" % o.app_id)
+        url = '%s/onos/v1/applications/%s/active' % (onos_url, o.app_id)
+        request = requests.post(url, auth=onos_basic_auth)
+
+        if request.status_code != 200:
+            log.error("Request failed", response=request.text)
+            raise Exception("Failed to add application %s to ONOS: %s" % (url, request.text))
+
+        url = '%s/onos/v1/applications/%s' % (onos_url, o.app_id)
+        request = requests.get(url, auth=onos_basic_auth)
+
+        if request.status_code != 200:
+            log.error("Request failed", response=request.text)
+            raise Exception("Failed to read application %s from ONOS: %s" % (url, request.text))
+        else:
+            o.version = request.json()["version"]
+
+    def check_app_installed(self, o, onos_url, onos_basic_auth):
+        url = '%s/onos/v1/applications/%s' % (onos_url, o.app_id)
+        request = requests.get(url, auth=onos_basic_auth)
+
+        if request.status_code == 200:
+            if "version" in request.json() and o.version == request.json()["version"]:
+                return True
+            else:
+                # uninstall the application
+                self.uninstall_app(o, onos_url, onos_basic_auth)
+                return False
+        if request.status_code == 404:
+            # app is not installed at all
+            return False
+        else:
+            log.error("Request failed", response=request.text)
+            raise Exception("Failed to read application %s from ONOS: %s" % (url, request.text))
+
+    def install_app(self, o, onos_url, onos_basic_auth):
+        log.info("Installing app from url %s" % o.url)
+
+        # check is the already installed app is the correct version (if it has no app_id is not installed)
+        is_installed = False
+        if o.app_id and o.app_id is not None:
+            is_installed = self.check_app_installed(o, onos_url, onos_basic_auth)
+
+        if is_installed:
+            # if the app is already installed we don't need to do anything
+            return
+
+        if not o.version or o.version is None:
+            # TODO move this validation in the model.py (if the url is there version must there and app_id must not)
+            raise Exception('You need to specify a version')
+        data = {
+            'activate': True,
+            'url': o.url
+        }
+        url = '%s/onos/v1/applications' % onos_url
+        request = requests.post(url, json=data, auth=onos_basic_auth)
+
+        if request.status_code != 200:
+            log.error("Request failed", response=request.text)
+            raise Exception("Failed to add application %s to ONOS: %s" % (url, request.text))
+
+        o.app_id = request.json()["name"]
+
+        url = '%s/onos/v1/applications/%s' % (onos_url, o.app_id)
+        request = requests.get(url, auth=onos_basic_auth)
+
+        if request.status_code != 200:
+            log.error("Request failed", response=request.text)
+            raise Exception("Failed to read application %s from ONOS: %s" % (url, request.text))
+        else:
+            if o.version != request.json()["version"]:
+                raise Exception("The version of %s you installed (%s) is not the same you requested (%s)" % (o.app_id, request.json()["version"], o.version))
+
+    def sync_record(self, o):
+        log.info("Sync'ing", model=o.tologdict())
+        if hasattr(o, 'service_instance'):
+            # this is a ServiceInstanceAttribute model just push the config
+            if 'ONOSApp' in o.service_instance.leaf_model.class_names:
+                return self.add_config(o)
+            return # if it's not an ONOSApp do nothing
+
+        if not self.check_app_dependencies(o.dependencies):
+            raise DeferredException('Deferring installation of ONOSApp with id %s as dependencies are not met' % o.id)
+
+        # getting onos url and auth
+        onos_url = "%s:%s" % (Helpers.format_url(o.owner.leaf_model.rest_hostname), o.owner.leaf_model.rest_port)
+        onos_basic_auth = HTTPBasicAuth(o.owner.leaf_model.rest_username, o.owner.leaf_model.rest_password)
+
+        # activate app (bundled in onos)
+        if not o.url or o.url is None:
+            self.activate_app(o, onos_url, onos_basic_auth)
+        # install an app from a remote source
+        if o.url and o.url is not None:
+            self.install_app(o, onos_url, onos_basic_auth)
+
+    def delete_config(self, o):
+        log.info("Deleting config %s" % o.name)
+        # getting onos url and auth
+        onos_app = o.service_instance.leaf_model
+        onos_url = "%s:%s" % (Helpers.format_url(onos_app.owner.leaf_model.rest_hostname), onos_app.owner.leaf_model.rest_port)
+        onos_basic_auth = HTTPBasicAuth(onos_app.owner.leaf_model.rest_username, onos_app.owner.leaf_model.rest_password)
+
+        url = o.name
+        if url[0] == "/":
+            # strip initial /
+            url = url[1:]
+
+        url = '%s/%s' % (onos_url, url)
+        request = requests.delete(url, auth=onos_basic_auth)
+
+        if request.status_code != 204:
+            log.error("Request failed", response=request.text)
+            raise Exception("Failed to remove config %s from ONOS:  %s" % (url, request.text))
+
+    def uninstall_app(self,o, onos_url, onos_basic_auth):
+        log.info("Uninstalling app %s" % o.app_id)
+        url = '%s/onos/v1/applications/%s' % (onos_url, o.app_id)
+
+        request = requests.delete(url, auth=onos_basic_auth)
+
+        if request.status_code != 204:
+            log.error("Request failed", response=request.text)
+            raise Exception("Failed to delete application %s from ONOS: %s" % (url, request.text))
+
+    def deactivate_app(self, o, onos_url, onos_basic_auth):
+        log.info("Deactivating app %s" % o.app_id)
+        url = '%s/onos/v1/applications/%s/active' % (onos_url, o.app_id)
+
+        request = requests.delete(url, auth=onos_basic_auth)
+
+        if request.status_code != 204:
+            log.error("Request failed", response=request.text)
+            raise Exception("Failed to deactivate application %s from ONOS: %s" % (url, request.text))
+
+    def delete_record(self, o):
+
+        if hasattr(o, 'service_instance'):
+            # this is a ServiceInstanceAttribute model
+            if 'ONOSApp' in o.service_instance.leaf_model.class_names:
+                return self.delete_config(o)
+            return # if it's not related to an ONOSApp do nothing
+
+        # NOTE if it is an ONOSApp we don't care about the ServiceInstanceAttribute
+        # as the reaper will delete it
+
+        # getting onos url and auth
+        onos_url = "%s:%s" % (Helpers.format_url(o.owner.leaf_model.rest_hostname), o.owner.leaf_model.rest_port)
+        onos_basic_auth = HTTPBasicAuth(o.owner.leaf_model.rest_username, o.owner.leaf_model.rest_password)
+
+        # deactivate an app (bundled in onos)
+        if not o.url or o.url is None:
+            self.deactivate_app(o, onos_url, onos_basic_auth)
+        # uninstall an app from a remote source, only if it has been activated before
+        if o.url and o.url is not None:
+            self.uninstall_app(o, onos_url, onos_basic_auth)