[CORD-3100] Refactoring and documenting the ONOS Service

Change-Id: Ida83935798a2a99b538f6ccdc55cc26f3148ffe3
diff --git a/.gitignore b/.gitignore
index 723ef36..1163cb2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-.idea
\ No newline at end of file
+.idea
+*.pyc
\ No newline at end of file
diff --git a/Dockerfile.synchronizer b/Dockerfile.synchronizer
index 6aa3dc3..2027b65 100644
--- a/Dockerfile.synchronizer
+++ b/Dockerfile.synchronizer
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# docker build -t xosproject/onos-synchronizer:candidate -f Dockerfile.synchronizer .
+
 # xosproject/onos-synchronizer
 
 FROM xosproject/xos-synchronizer-base:candidate
@@ -51,4 +53,3 @@
       org.opencord.component.xos.vcs-ref=$org_opencord_component_xos_vcs_ref
 
 CMD ["/usr/bin/python", "/opt/xos/synchronizers/onos/onos-synchronizer.py"]
-
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..082d1e0
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,46 @@
+# ONOS Service
+
+This service is intended to manage ONOSes.
+
+It can:
+
+- push global configuration to ONOS
+- install applications
+- push application specific configuration
+
+## Models
+
+This service is composed by two models:
+
+- `ONOSService` responsible to hold urls and authentication informations  
+- `ONOSApp` represents and ONOS application and track its dependencies (this extend the `ServiceInstance` model)
+
+This service uses `ServiceAttributes` and `ServiceInstanceAttributes`
+to hold the configuration details.
+
+For more informations about the models, please refer to the
+[xproto](https://github.com/opencord/onos-service/blob/master/xos/synchronizer/models/onos.xproto) definition
+
+## Synchronization workflow
+
+### ONOSService
+
+Anytime an `ONOSService` model is created/updated, the synchronizer checks
+for the corresponding `ServiceAttribute`s and if any are found it pushes the configuration
+to ONOS
+
+### ONOSServiceInstance
+
+Anytime an `ONOSServiceInstance` model is created/updated, the synchronizer checks
+for the corresponding `ServiceInstanceAttribute`s and if any are found:
+
+- checks for the application dependencies
+- if they are not matched defer the synchronization
+- if they are matched
+- it pushes the configuration to ONOS
+- it installs/activates the application in ONOS
+
+> ONOS Applications can be activated (if they already present in the container),
+> in that case you just need to provide the `app_id`, or they can be installed from a remote `.oar`,
+> in which case you just need to provide an `url` 
+ 
\ No newline at end of file
diff --git a/samples/apps.yaml b/samples/apps.yaml
new file mode 100644
index 0000000..d923788
--- /dev/null
+++ b/samples/apps.yaml
@@ -0,0 +1,69 @@
+# 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 @apps.yaml http://192.168.99.100:30007/run
+
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+  - custom_types/onosapp.yaml
+  - custom_types/onosservice.yaml
+  - custom_types/serviceinstanceattribute.yaml
+description: Configures fabric switches and related ports
+topology_template:
+  node_templates:
+    service#onos:
+      type: tosca.nodes.ONOSService
+      properties:
+        name: onos
+        must-exist: true
+
+
+    # Local app
+    dhcp:
+      type: tosca.nodes.ONOSApp
+      properties:
+        name: dhcp
+        app_id: org.onosproject.dhcp
+      requirements:
+        - owner:
+            node: service#onos
+            relationship: tosca.relationships.BelongsToOne
+
+    # Remote app
+    cord-config:
+      type: tosca.nodes.ONOSApp
+      properties:
+        name: cord-config
+        url: https://oss.sonatype.org/content/repositories/public/org/opencord/cord-config/1.4.0-SNAPSHOT/cord-config-1.4.0-20180604.071543-275.oar
+        version: 1.4.0.SNAPSHOT
+      requirements:
+        - owner:
+            node: service#onos
+            relationship: tosca.relationships.BelongsToOne
+
+    # CORD-Configuration
+    cord-config-attr:
+      type: tosca.nodes.ServiceInstanceAttribute
+      properties:
+        name: /onos/v1/network/configuration/apps/org.opencord.olt
+        value: >
+          {
+            "kafka" : {
+              "bootstrapServers" : "cord-kafka-kafka.default.svc.cluster.local:9092"
+            }
+          }
+      requirements:
+        - service_instance:
+            node: cord-config
+            relationship: tosca.relationships.BelongsToOne
\ No newline at end of file
diff --git a/samples/service.yaml b/samples/service.yaml
new file mode 100644
index 0000000..d610cc6
--- /dev/null
+++ b/samples/service.yaml
@@ -0,0 +1,46 @@
+# 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 @service.yaml http://192.168.99.100:30007/run
+
+tosca_definitions_version: tosca_simple_yaml_1_0
+imports:
+  - custom_types/onosservice.yaml
+  - custom_types/serviceattribute.yaml
+description: Configures fabric switches and related ports
+topology_template:
+  node_templates:
+    service#onos:
+      type: tosca.nodes.ONOSService
+      properties:
+        name: onos
+        rest_hostname: onos-fabric-ui
+        rest_port: 8181
+
+
+    # ONOS Configuration
+    driver:
+      type: tosca.nodes.ServiceAttribute
+      properties:
+        name: /onos/v1/network/configuration/devices/of:1234
+        value: >
+          {
+            "basic": {
+              "driver": "mydriver"
+            }
+          }
+      requirements:
+        - service:
+            node: service#onos
+            relationship: tosca.relationships.BelongsToOne
\ No newline at end of file
diff --git a/xos/synchronizer/scripts/dockerip.sh b/xos/nose2-plugins/__init__.py
similarity index 68%
copy from xos/synchronizer/scripts/dockerip.sh
copy to xos/nose2-plugins/__init__.py
index 3618fa6..42722a8 100644
--- a/xos/synchronizer/scripts/dockerip.sh
+++ b/xos/nose2-plugins/__init__.py
@@ -12,14 +12,3 @@
 # 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.
-
-
-#!/bin/bash
-
-MODE=`docker inspect --format '{{ .HostConfig.NetworkMode }}' $1  | tr -d '\n' | tr -d '\r'`
-if [[ "$MODE" == "host" ]]; then
-    echo -n "127.0.0.1"
-else
-    docker inspect --format '{{ .NetworkSettings.IPAddress }}' $1 | tr -d '\n' | tr -d '\r'
-fi
-
diff --git a/xos/nose2-plugins/exclude.py b/xos/nose2-plugins/exclude.py
new file mode 100644
index 0000000..241eadb
--- /dev/null
+++ b/xos/nose2-plugins/exclude.py
@@ -0,0 +1,32 @@
+
+# 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 logging
+import os
+
+from nose2.events import Plugin
+
+log = logging.getLogger('nose2.plugins.excludeignoredfiles')
+
+class ExcludeIgnoredFiles(Plugin):
+    commandLineSwitch = (None, 'exclude-ignored-files', 'Exclude that which should be excluded')
+
+    def matchPath(self, event):
+        if event.path.endswith(".py"):
+            text = open(event.path, "r").read()
+            if "test_framework: ignore" in text.lower():
+                log.info("Ignoring %s" % event.path)
+                event.handled = True
+                return False
diff --git a/xos/synchronizer/models/onos.xproto b/xos/synchronizer/models/onos.xproto
index c5cfc56..726702d 100644
--- a/xos/synchronizer/models/onos.xproto
+++ b/xos/synchronizer/models/onos.xproto
@@ -6,17 +6,17 @@
     option verbose_name="ONOS Application";
     option owner_class_name="ONOSService";
 
-    optional string install_dependencies = 1 [db_index = False, null = True, blank = True];
-    optional string dependencies = 2 [db_index = False, null = True, blank = True];
+    optional string app_id = 1 [db_index = False, null = True, blank = False];
+    required string dependencies = 2 [help_text="Comma separated list of required applications", db_index = False, null = True, blank = True];
+    optional string url = 3 [help_text="URL at which the application is available, if it needs to be downloaded", db_index = False, null = True, blank = False];
+    required string version = 4 [db_index = False, null = True, blank = False];
 }
 
 message ONOSService (Service){
     option verbose_name="ONOS Service";
 
-    optional string rest_hostname = 1 [db_index = False, max_length = 255, null = True, content_type = "stripped", blank = True];
+    required string rest_hostname = 1 [db_index = False, max_length = 255, null = False, content_type = "stripped", blank = False];
     required int32 rest_port = 2 [default = 8181, null = False, db_index = False, blank = False];
-    required bool no_container = 3 [default = False, null = False, db_index = False, blank = True];
-    optional string node_key = 4 [db_index = False, max_length = 1024, null = True, content_type = "stripped", blank = True];
-    optional string rest_username = 5 [db_index = False, max_length = 255, null = True, content_type = "stripped", blank = True, default="karaf"];
-    optional string rest_password = 6 [db_index = False, max_length = 255, null = True, content_type = "stripped", blank = True, default="karaf"];
+    required string rest_username = 3 [db_index = False, max_length = 255, null = False, content_type = "stripped", blank = False, default="karaf"];
+    required string rest_password = 4 [db_index = False, max_length = 255, null = False, content_type = "stripped", blank = False, default="karaf"];
 }
diff --git a/xos/synchronizer/onos-ext-notifier-1.0-SNAPSHOT.oar b/xos/synchronizer/onos-ext-notifier-1.0-SNAPSHOT.oar
deleted file mode 100644
index 23c6fcd..0000000
--- a/xos/synchronizer/onos-ext-notifier-1.0-SNAPSHOT.oar
+++ /dev/null
Binary files differ
diff --git a/xos/synchronizer/onos-ext-volt-event-publisher-1.0-SNAPSHOT.oar b/xos/synchronizer/onos-ext-volt-event-publisher-1.0-SNAPSHOT.oar
deleted file mode 100644
index 244f589..0000000
--- a/xos/synchronizer/onos-ext-volt-event-publisher-1.0-SNAPSHOT.oar
+++ /dev/null
Binary files differ
diff --git a/xos/synchronizer/scripts/dockerip.sh b/xos/synchronizer/steps/helpers.py
similarity index 68%
rename from xos/synchronizer/scripts/dockerip.sh
rename to xos/synchronizer/steps/helpers.py
index 3618fa6..83b9458 100644
--- a/xos/synchronizer/scripts/dockerip.sh
+++ b/xos/synchronizer/steps/helpers.py
@@ -1,4 +1,3 @@
-
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,13 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from synchronizers.new_base.modelaccessor import Service
 
-#!/bin/bash
-
-MODE=`docker inspect --format '{{ .HostConfig.NetworkMode }}' $1  | tr -d '\n' | tr -d '\r'`
-if [[ "$MODE" == "host" ]]; then
-    echo -n "127.0.0.1"
-else
-    docker inspect --format '{{ .NetworkSettings.IPAddress }}' $1 | tr -d '\n' | tr -d '\r'
-fi
-
+class Helpers():
+    @staticmethod
+    def format_url(url):
+        if 'http' in url:
+            return url
+        else:
+            return 'http://%s' % url
\ No newline at end of file
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)
diff --git a/xos/synchronizer/steps/sync_onos_service.py b/xos/synchronizer/steps/sync_onos_service.py
new file mode 100644
index 0000000..010fc33
--- /dev/null
+++ b/xos/synchronizer/steps/sync_onos_service.py
@@ -0,0 +1,89 @@
+
+# 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, model_accessor
+from synchronizers.new_base.modelaccessor import ONOSService, Service, ServiceAttribute
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+from helpers import Helpers
+
+log = create_logger(Config().get('logging'))
+
+class SyncONOSService(SyncStep):
+    provides = [ONOSService]
+    observes = [ONOSService, ServiceAttribute]
+
+    def get_service_attribute(self, o):
+        # NOTE this method is defined in the core convenience methods for services
+        svc = Service.objects.get(id=o.id)
+        return svc.serviceattribute_dict
+
+    def sync_record(self, o):
+        if hasattr(o, 'service'):
+            # this is a ServiceAttribute model
+            if 'ONOSService' in o.service.leaf_model.class_names:
+                print "sync ONOSService Attribute", o.service.leaf_model
+                return self.sync_record(o.service.leaf_model)
+            return # if it's not related to an ONOSService do nothing
+
+        onos_url = "%s:%s" % (Helpers.format_url(o.rest_hostname), o.rest_port)
+        onos_basic_auth = HTTPBasicAuth(o.rest_username, o.rest_password)
+
+        configs = self.get_service_attribute(o)
+        for url, value in configs.iteritems():
+
+            if url[0] == "/":
+                # strip initial /
+                url = url[1:]
+
+            url = '%s/%s' % (onos_url, url)
+            value = json.loads(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" % url)
+
+    def delete_record(self, o):
+
+        if hasattr(o, 'service'):
+            # this is a ServiceAttribute model
+            if 'ONOSService' in o.service.leaf_model.class_names:
+                print "sync ONOSService Attribute", o.service.leaf_model
+
+                log.info("Deleting config %s" % o.name)
+                # getting onos url and auth
+                onos_service = o.service.leaf_model
+                onos_url = "%s:%s" % (
+                Helpers.format_url(onos_service.rest_hostname), onos_service.rest_port)
+                onos_basic_auth = HTTPBasicAuth(onos_service.rest_username,
+                                                onos_service.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))
diff --git a/xos/synchronizer/steps/sync_onosapp.py b/xos/synchronizer/steps/sync_onosapp.py
deleted file mode 100644
index 759dc67..0000000
--- a/xos/synchronizer/steps/sync_onosapp.py
+++ /dev/null
@@ -1,255 +0,0 @@
-
-# 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 hashlib
-import os
-import socket
-import sys
-import base64
-import time
-import re
-import json
-from collections import OrderedDict
-from xosconfig import Config
-from synchronizers.new_base.syncstep import DeferredException
-from synchronizers.new_base.ansible_helper import run_template
-from synchronizers.new_base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
-from synchronizers.new_base.modelaccessor import *
-from xos.logger import Logger, logging
-
-logger = Logger(level=logging.INFO)
-
-
-class SyncONOSApp(SyncInstanceUsingAnsible):
-    provides=[ONOSApp]
-    observes=ONOSApp
-    requested_interval=0
-    template_name = "sync_onosapp.yaml"
-
-    def __init__(self, *args, **kwargs):
-        super(SyncONOSApp, self).__init__(*args, **kwargs)
-
-    def get_instance(self, o):
-        # We assume the ONOS service owns a slice, so pick one of the instances
-        # inside that slice to sync to.
-
-        serv = self.get_onos_service(o)
-
-        if serv.no_container:
-            raise Exception("get_instance() was called on a service that was marked no_container")
-
-        if serv.slices.exists():
-            slice = serv.slices.all()[0]
-            if slice.instances.exists():
-                return slice.instances.all()[0]
-
-        return None
-
-    def get_onos_service(self, o):
-        return o.owner.leaf_model
-
-    def is_no_container(self, o):
-        return self.get_onos_service(o).no_container
-
-    def skip_ansible_fields(self, o):
-        return self.is_no_container(o)
-
-    def get_files_dir(self, o):
-        step_dir = Config.get("steps_dir")
-
-        return os.path.join(step_dir, "..", "files", str(self.get_onos_service(o).id), o.name)
-
-    def get_cluster_configuration(self, o):
-        instance = self.get_instance(o)
-        if not instance:
-           raise Exception("No instance for ONOS App")
-        node_ips = [socket.gethostbyname(instance.node.name)]
-
-        ipPrefix = ".".join(node_ips[0].split(".")[:3]) + ".*"
-        result = '{ "nodes": ['
-        result = result + ",".join(['{ "ip": "%s"}' % ip for ip in node_ips])
-        result = result + '], "ipPrefix": "%s"}' % ipPrefix
-        return result
-
-    def get_dynamic_parameter_value(self, o, param):
-        instance = self.get_instance(o)
-        if not instance:
-           raise Exception("No instance for ONOS App")
-        if param == 'rabbit_host':
-            return instance.controller.rabbit_host
-        if param == 'rabbit_user':
-            return instance.controller.rabbit_user
-        if param == 'rabbit_password':
-            return instance.controller.rabbit_password
-        if param == 'keystone_tenant_id':
-            cslice = ControllerSlice.objects.get(slice=instance.slice)
-            if not cslice:
-                raise Exception("Controller slice object for %s does not exist" % instance.slice.name)
-            return cslice.tenant_id
-        if param == 'keystone_user_id':
-            cuser = ControllerUser.objects.get(user=instance.creator)
-            if not cuser:
-                raise Exception("Controller user object for %s does not exist" % instance.creator)
-            return cuser.kuser_id
-
-    def write_configs(self, o):
-        if hasattr(o, "create_attr"):
-            # new API doesn't let us setattr for things that don't already exist
-            o.create_attr("config_fns")
-            o.create_attr("rest_configs")
-            o.create_attr("component_configs")
-            o.create_attr("files_dir")
-            o.create_attr("node_key_fn")
-            o.create_attr("early_rest_configs")
-
-        o.config_fns = []
-        o.rest_configs = []
-        o.component_configs = []
-        o.files_dir = self.get_files_dir(o)
-
-        if not os.path.exists(o.files_dir):
-            os.makedirs(o.files_dir)
-
-        # Combine the service attributes with the tenant attributes. Tenant
-        # attribute can override service attributes.
-        attrs = o.owner.serviceattribute_dict
-        attrs.update(o.tenantattribute_dict)
-
-        # Check to see if we're waiting on autoconfig
-        if (attrs.get("autogenerate") in ["vtn-network-cfg",]) and \
-           (not attrs.get("rest_onos/v1/network/configuration/")):
-            raise DeferredException("Network configuration is not populated yet")
-
-        ordered_attrs = attrs.keys()
-
-        onos = self.get_onos_service(o)
-        if onos.node_key:
-            file(os.path.join(o.files_dir, "node_key"),"w").write(onos.node_key)
-            o.node_key_fn="node_key"
-        else:
-            o.node_key_fn=None
-
-        o.early_rest_configs=[]
-        if ("cordvtn" in o.dependencies) and (not self.is_no_container(o)):
-            # For VTN, since it's running in a docker host container, we need
-            # to make sure it configures the cluster using the right ip addresses.
-            # NOTE: rest_onos/v1/cluster/configuration/ will reboot the cluster and
-            #   must go first.
-            name="rest_onos/v1/cluster/configuration/"
-            value= self.get_cluster_configuration(o)
-            fn = name[5:].replace("/","_")
-            endpoint = name[5:]
-            file(os.path.join(o.files_dir, fn),"w").write(" " +value)
-            o.early_rest_configs.append( {"endpoint": endpoint, "fn": fn} )
-
-        for name in attrs.keys():
-            value = attrs[name]
-            if name.startswith("config_"):
-                fn = name[7:] # .replace("_json",".json")
-                o.config_fns.append(fn)
-                file(os.path.join(o.files_dir, fn),"w").write(value)
-            if name.startswith("rest_"):
-                fn = name[5:].replace("/","_")
-                endpoint = name[5:]
-                # Ansible goes out of it's way to make our life difficult. If
-                # 'lookup' sees a file that it thinks contains json, then it'll
-                # insist on parsing and return a json object. We just want
-                # a string, so prepend a space and then strip the space off
-                # later.
-                file(os.path.join(o.files_dir, fn),"w").write(" " +value)
-                o.rest_configs.append( {"endpoint": endpoint, "fn": fn} )
-            if name.startswith("component_config"):
-                components = json.loads(value,object_pairs_hook=OrderedDict)
-                for component in components.keys():
-                    config = components[component]
-                    for key in config.keys():
-                         config_val = config[key]
-                         found = re.findall('<(.+?)>',config_val)
-                         for x in found:
-                            #Get value corresponding to that string
-                            val = self.get_dynamic_parameter_value(o, x)
-                            if val:
-	                       config_val = re.sub('<'+x+'>', val, config_val)
-                            #TODO: else raise an exception?
-	                 o.component_configs.append( {"component": component, "config_params": "'{\""+key+"\":\""+config_val+"\"}'"} )
-
-    def prepare_record(self, o):
-        self.write_configs(o)
-
-    def get_extra_attributes_common(self, o):
-        fields = {}
-
-        # These are attributes that are not dependent on Instance. For example,
-        # REST API stuff.
-
-        onos = self.get_onos_service(o)
-
-        fields["files_dir"] = o.files_dir
-        fields["appname"] = o.name
-        fields["rest_configs"] = o.rest_configs
-        fields["component_configs"] = o.component_configs
-        fields["rest_hostname"] = onos.rest_hostname
-        fields["rest_username"] = onos.rest_username
-        fields["rest_password"] = onos.rest_password
-        fields["rest_port"] = onos.rest_port
-
-        if o.dependencies:
-            fields["dependencies"] = [x.strip() for x in o.dependencies.split(",")]
-        else:
-            fields["dependencies"] = []
-
-        if o.install_dependencies:
-            fields["install_dependencies"] = [x.strip() for x in o.install_dependencies.split(",")]
-        else:
-            fields["install_dependencies"] = []
-
-        return fields
-
-    def get_extra_attributes_full(self, o):
-        instance = self.get_instance(o)
-
-        fields = self.get_extra_attributes_common(o)
-
-        fields["config_fns"] = o.config_fns
-        fields["early_rest_configs"] = o.early_rest_configs
-        fields["node_key_fn"] = o.node_key_fn
-
-        if (instance.isolation=="container"):
-            fields["ONOS_container"] = "%s-%s" % (instance.slice.name, str(instance.id))
-        else:
-            fields["ONOS_container"] = "ONOS"
-        return fields
-
-    def get_extra_attributes(self, o):
-        if self.is_no_container(o):
-            return self.get_extra_attributes_common(o)
-        else:
-            return self.get_extra_attributes_full(o)
-
-    def sync_fields(self, o, fields):
-        # the super causes the playbook to be run
-        super(SyncONOSApp, self).sync_fields(o, fields)
-
-    def run_playbook(self, o, fields):
-        if self.is_no_container(o):
-            # There is no machine to SSH to, so use the synchronizer's
-            # run_template method directly.
-            run_template("sync_onosapp_nocontainer.yaml", fields, object=o)
-        else:
-            super(SyncONOSApp, self).run_playbook(o, fields)
-
-    def delete_record(self, m):
-        pass
diff --git a/xos/synchronizer/steps/sync_onosapp.yaml b/xos/synchronizer/steps/sync_onosapp.yaml
deleted file mode 100644
index e732814..0000000
--- a/xos/synchronizer/steps/sync_onosapp.yaml
+++ /dev/null
@@ -1,188 +0,0 @@
-
-# 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.
-
-
----
-- hosts: {{ instance_name }}
-  gather_facts: False
-  connection: ssh
-  user: {{ username }}
-  become: yes
-  vars:
-    appname: {{ appname }}
-    dependencies: {{ dependencies }}
-{% if component_configs %}
-    component_configs:
-{% for component_config in component_configs %}
-       - component: {{ component_config.component }}
-         config_params: {{  component_config.config_params }}
-{% endfor %}
-{% endif %}
-{% if rest_configs %}
-    var_rest_configs:
-{% for rest_config in rest_configs %}
-       - endpoint: {{ rest_config.endpoint }}
-         body: "{{ '{{' }} lookup('file', '{{ files_dir }}/{{ rest_config.fn }}') {{ '}}' }}"
-{% endfor %}
-{% endif %}
-{% if early_rest_configs %}
-    var_early_rest_configs:
-{% for early_rest_config in early_rest_configs %}
-       - endpoint: {{ early_rest_config.endpoint }}
-         body: "{{ '{{' }} lookup('file', '{{ files_dir }}/{{ early_rest_config.fn }}') {{ '}}' }}"
-{% endfor %}
-{% endif %}
-
-  tasks:
-
-  - name: Get Docker IP
-    script: /opt/xos/synchronizers/onos/scripts/dockerip.sh {{ ONOS_container }}
-    register: onosaddr
-
-  - name: Wait for ONOS to come up
-    wait_for:
-      host={{ '{{' }} onosaddr.stdout {{ '}}' }}
-      port={{ '{{' }} item {{ '}}' }}
-      state=present
-    with_items:
-    - 8101
-    - 8181
-    - 9876
-
-  - name: Config file directory
-    file:
-      path=/home/ubuntu/{{ appname }}/
-      state=directory
-
-{% if node_key_fn %}
-  - name: Copy over key
-    copy:
-      src={{ files_dir }}/{{ node_key_fn }}
-      dest=/home/ubuntu/node_key
-
-  - name: Copy node key into container
-    shell: docker cp /home/ubuntu/node_key {{ ONOS_container }}:/root/node_key
-{% endif %}
-
-{% if config_fns %}
-  - name: Copy over configuration files
-    copy:
-      src={{ files_dir }}/{{ '{{' }} item {{ '}}' }}
-      dest=/home/ubuntu/{{ appname }}/{{ '{{' }} item {{ '}}' }}
-    with_items:
-        {% for config_fn in config_fns %}
-        - {{ config_fn }}
-        {% endfor %}
-
-  - name: Make sure config directory exists
-    shell: docker exec {{ ONOS_container }} mkdir -p /root/onos/config/
-    sudo: yes
-
-  - name: Copy config files into container
-    shell: docker cp {{ appname }}/{{ '{{' }} item {{ '}}' }} {{ ONOS_container }}:/root/onos/config/
-    sudo: yes
-    with_items:
-        {% for config_fn in config_fns %}
-        - {{ config_fn }}
-        {% endfor %}
-{% endif %}
-
-  # Don't know how to check for this condition, just wait
-  - name: Wait for ONOS to install the apps
-    wait_for: timeout=15
-
-{% if early_rest_configs %}
-  - name: Add ONOS early configuration values
-    uri:
-      url: http://{{ '{{' }} onosaddr.stdout {{ '}}' }}:8181/{{ '{{' }} item.endpoint {{ '}}' }}
-      body: "{{ '{{' }} item.body {{ '}}' }}"
-      body_format: json
-      method: POST
-      user: karaf
-      password: karaf
-    with_items: "var_early_rest_configs"
-
-  # Don't know how to check for this condition, just wait
-  - name: Wait for ONOS to restart
-    wait_for: timeout=15
-{% endif %}
-
-{% if install_dependencies %}
-  - name: Install app file directory
-    file:
-      path=/home/ubuntu/{{ appname }}/apps/
-      state=directory
-
-  - name: Copy over app install files to ONOS host
-    copy:
-      src=/opt/xos/synchronizers/onos/{{ '{{' }} item {{ '}}' }}
-      dest=/home/ubuntu/{{ appname }}/apps/{{ '{{' }} item {{ '}}' }}
-    with_items:
-        {% for install_app in install_dependencies %}
-        - {{ install_app }}
-        {% endfor %}
-
-  - name: POST onos-app install command
-    command: >
-        curl -XPOST -HContent-Type:application/octet-stream -u karaf:karaf --data-binary @/home/ubuntu/{{ appname }}/apps/{{ '{{' }} item {{ '}}' }} http://{{ '{{' }} onosaddr.stdout  {{ '}}' }}:8181/onos/v1/applications
-    with_items:
-        {% for dependency in install_dependencies %}
-        - {{ dependency }}
-        {% endfor %}
-{% endif %}
-
-{% if dependencies %}
-  - name: Add dependencies to ONOS
-    uri:
-      url: http://{{ '{{' }} onosaddr.stdout {{ '}}' }}:8181/onos/v1/applications/{{ '{{' }} item {{ '}}' }}/active
-      method: POST
-      user: karaf
-      password: karaf
-    with_items:
-        {% for dependency in dependencies %}
-        - {{ dependency }}
-        {% endfor %}
-{% endif %}
-
-{% if component_configs %}
-  - name: Add ONOS component configuration values
-    command: >
-        curl -XPOST -HContent-Type:application/json -u karaf:karaf -d {{ '{{' }} item.config_params | to_json {{ '}}' }} http://{{ '{{' }} onosaddr.stdout  {{ '}}' }}:8181/onos/v1/configuration/{{
- '{{' }} item.component {{ '}}' }}
-    with_items: "component_configs"
-
-#    uri:
-#      url: http://{{ '{{' }} onosaddr.stdout {{ '}}' }}:8181/onos/v1/configuration/{{ '{{' }} item.component {{ '}}' }} #http://localhost:8181/onos/v1/configuration/
-#      body: "{{ '{{' }} item.config_params | to_json {{ '}}' }}"
-#      body_format: json
-#      method: POST
-#      user: karaf
-#      password: karaf
-#    with_items: "component_configs"
-{% endif %}
-
-{% if rest_configs %}
-# Do this after services have been activated, or it will cause an exception.
-# vOLT will re-read its net config; vbng may not.
-  - name: Add ONOS configuration values
-    uri:
-      url: http://{{ '{{' }} onosaddr.stdout {{ '}}' }}:8181/{{ '{{' }} item.endpoint {{ '}}' }} #http://localhost:8181/onos/v1/network/configuration/
-      body: "{{ '{{' }} item.body {{ '}}' }}"
-      body_format: json
-      method: POST
-      user: karaf
-      password: karaf
-    with_items: "var_rest_configs"
-{% endif %}
diff --git a/xos/synchronizer/steps/sync_onosapp_nocontainer.yaml b/xos/synchronizer/steps/sync_onosapp_nocontainer.yaml
deleted file mode 100644
index 3c572d7..0000000
--- a/xos/synchronizer/steps/sync_onosapp_nocontainer.yaml
+++ /dev/null
@@ -1,97 +0,0 @@
-
-# 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.
-
-
----
-- hosts: 127.0.0.1
-  connection: local
-  vars:
-    appname: {{ appname }}
-    dependencies: {{ dependencies }}
-{% if component_configs %}
-    component_configs:
-{% for component_config in component_configs %}
-       - component: {{ component_config.component }}
-         config_params: {{  component_config.config_params }}
-{% endfor %}
-{% endif %}
-{% if rest_configs %}
-    var_rest_configs:
-{% for rest_config in rest_configs %}
-       - endpoint: {{ rest_config.endpoint }}
-         body: "{{ '{{' }} lookup('file', '{{ files_dir }}/{{ rest_config.fn }}') {{ '}}' }}"
-{% endfor %}
-{% endif %}
-    rest_hostname: {{ rest_hostname }}
-    rest_port: {{ rest_port }}
-
-  tasks:
-{% if install_dependencies %}
-  - name: Install ONOS apps by URL
-    uri:
-        url: "http://{{ rest_hostname }}:{{ rest_port }}/{{ '{{' }}  item.endpoint {{ '}}' }}"
-        body: "{{ '{{' }} item.body | to_json {{ '}}' }}"
-        user: {{ rest_username }}
-        password: {{ rest_password }}
-        body_format: json
-        method: POST
-        status_code: 200, 409
-    with_items:
-       {% for install_dependency in install_dependencies %}
-       - endpoint: "onos/v1/applications"
-         body:
-           url: "{{ install_dependency }}"
-           activate: "true"
-       {% endfor %}
-{% endif %}
-
-{% if dependencies %}
-  - name: Add dependencies to ONOS
-    uri:
-      url: http://{{ rest_hostname }}:{{ rest_port }}/onos/v1/applications/{{ '{{' }} item {{ '}}' }}/active
-      method: POST
-      user: karaf
-      password: karaf
-    with_items:
-        {% for dependency in dependencies %}
-        - {{ dependency }}
-        {% endfor %}
-{% endif %}
-
-{% if component_configs %}
-  - name: Add ONOS component configuration values
-    uri:
-      url: http://{{ rest_hostname }}:{{ rest_port }}/onos/v1/configuration/{{ '{{' }} item.component {{ '}}' }} #http://localhost:8181/onos/v1/configuration/
-      body: "{{ '{{' }} item.config_params {{ '}}' }}"
-      body_format: json
-      method: POST
-      user: karaf
-      password: karaf
-    with_items: "{{ '{{' }} component_configs {{ '}}' }}"
-{% endif %}
-
-{% if rest_configs %}
-# Do this after services have been activated, or it will cause an exception.
-# vOLT will re-read its net config; vbng may not.
-  - name: Add ONOS configuration values
-    uri:
-      url: http://{{ rest_hostname }}:{{ rest_port }}/{{ '{{' }} item.endpoint {{ '}}' }} #http://localhost:8181/onos/v1/network/configuration/
-      body: "{{ '{{' }} item.body {{ '}}' }}"
-      body_format: json
-      method: POST
-      user: karaf
-      password: karaf
-    with_items: "{{ '{{' }} var_rest_configs {{ '}}' }}"
-{% endif %}
diff --git a/xos/synchronizer/steps/sync_onosservice.py b/xos/synchronizer/steps/sync_onosservice.py
deleted file mode 100644
index 35b34b2..0000000
--- a/xos/synchronizer/steps/sync_onosservice.py
+++ /dev/null
@@ -1,78 +0,0 @@
-
-# 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 hashlib
-import os
-import socket
-import sys
-import base64
-import time
-from synchronizers.new_base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
-from synchronizers.new_base.modelaccessor import *
-from xos.logger import Logger, logging
-
-logger = Logger(level=logging.INFO)
-
-class SyncONOSService(SyncInstanceUsingAnsible):
-    provides=[ONOSService]
-    observes=ONOSService
-    requested_interval=0
-    template_name = "sync_onosservice.yaml"
-
-    def __init__(self, *args, **kwargs):
-        super(SyncONOSService, self).__init__(*args, **kwargs)
-
-    def get_instance(self, o):
-        # We assume the ONOS service owns a slice, so pick one of the instances
-        # inside that slice to sync to.
-
-        serv = o
-
-        if serv.slices.exists():
-            slice = serv.slices.all()[0]
-            if slice.instances.exists():
-                return slice.instances.all()[0]
-
-        return None
-
-    def get_extra_attributes(self, o):
-        fields={}
-        fields["instance_hostname"] = self.get_instance(o).instance_name.replace("_","-")
-        fields["appname"] = o.name
-        fields["ONOS_container"] = "ONOS"
-        return fields
-
-    def sync_record(self, o):
-        if o.no_container:
-            logger.info("no work to do for onos service, because o.no_container is set",extra=o.tologdict())
-            o.save()
-        else:
-            super(SyncONOSService, self).sync_record(o)
-
-    def sync_fields(self, o, fields):
-        # the super causes the playbook to be run
-        super(SyncONOSService, self).sync_fields(o, fields)
-
-    def run_playbook(self, o, fields):
-        instance = self.get_instance(o)
-        if (instance.isolation=="container"):
-            # If the instance is already a container, then we don't need to
-            # install ONOS.
-            return
-        super(SyncONOSService, self).run_playbook(o, fields)
-
-    def delete_record(self, m):
-        pass
diff --git a/xos/synchronizer/steps/sync_onosservice.yaml b/xos/synchronizer/steps/sync_onosservice.yaml
deleted file mode 100644
index 9a098a0..0000000
--- a/xos/synchronizer/steps/sync_onosservice.yaml
+++ /dev/null
@@ -1,82 +0,0 @@
-
-# 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.
-
-
----
-- hosts: {{ instance_name }}
-  gather_facts: False
-  connection: ssh
-  user: ubuntu
-  become: yes
-
-  tasks:
-
-  - name: Fix /etc/hosts
-    lineinfile:
-      dest=/etc/hosts
-      regexp="127.0.0.1 localhost"
-      line="127.0.0.1 localhost {{ instance_hostname }}"
-
-  - name: Add repo key
-    apt_key:
-      keyserver=hkp://pgp.mit.edu:80
-      id=58118E89F3A912897C070ADBF76221572C52609D
-
-  - name: Install Docker repo
-    apt_repository:
-      repo="deb https://apt.dockerproject.org/repo ubuntu-trusty main"
-      state=present
-
-  - name: Install Docker
-    apt:
-      name={{ '{{' }} item {{ '}}' }}
-      state=latest
-      update_cache=yes
-    with_items:
-    - docker-engine
-    - python-pip
-    - python-httplib2
-
-  - name: Install docker-py
-    pip:
-      name=docker-py
-      state=latest
-
-  - name: Start ONOS container
-    docker:
-      docker_api_version: "1.18"
-      name: {{ ONOS_container }}
-      # was: reloaded
-      state: running
-      image: onosproject/onos
-      ports:
-      - "6653:6653"
-      - "8101:8101"
-      - "8181:8181"
-      - "9876:9876"
-
-  - name: Get Docker IP
-    script: /opt/xos/synchronizers/onos/scripts/dockerip.sh {{ ONOS_container }}
-    register: dockerip
-
-  - name: Wait for ONOS to come up
-    wait_for:
-      host={{ '{{' }} dockerip.stdout {{ '}}' }}
-      port={{ '{{' }} item {{ '}}' }}
-      state=present
-    with_items:
-    - 8101
-    - 8181
-    - 9876
diff --git a/xos/synchronizer/steps/test_sync_onos_app.py b/xos/synchronizer/steps/test_sync_onos_app.py
new file mode 100644
index 0000000..4d64783
--- /dev/null
+++ b/xos/synchronizer/steps/test_sync_onos_app.py
@@ -0,0 +1,343 @@
+# 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
+
+def match_none(req):
+    return req.text == None
+
+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 TestSyncOnosApp(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_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("onos-service", "onos.xproto")
+        ])
+        import synchronizers.new_base.modelaccessor
+
+        from sync_onos_app import SyncONOSApp, DeferredException, model_accessor
+
+        # import all class names to globals
+        for (k, v) in model_accessor.all_model_classes.items():
+            globals()[k] = v
+
+
+        self.sync_step = SyncONOSApp
+
+        onos = ONOSService()
+        onos.rest_hostname = "onos-url"
+        onos.rest_port = "8181"
+        onos.rest_username = "karaf"
+        onos.rest_password = "karaf"
+
+        self.onos_app = Mock(spec=[
+            'id',
+            'name',
+            'app_id',
+            'dependencies',
+            'owner',
+            'url',
+            'backend_code',
+            'version',
+            'tologdict'
+        ])
+        self.onos_app.id = 1
+        self.onos_app.name = "vrouter"
+        self.onos_app.app_id = "org.onosproject.vrouter"
+        self.onos_app.dependencies = ""
+        self.onos_app.owner.leaf_model = onos
+        self.onos_app.url = None
+        self.onos_app.class_names = "ONOSApp"
+        self.onos_app.tologdict.return_value = ""
+
+        self.si = Mock()
+        self.si.id = 1
+        self.si.leaf_model = self.onos_app
+
+        self.vrouter_app_response = {
+            "name": "org.onosproject.vrouter",
+            "version": "1.13.1",
+        }
+
+        self.onos_app_attribute = Mock(spec=[
+            'id',
+            'service_instance',
+            'name',
+            'value'
+        ])
+        self.onos_app_attribute.id = 1
+        self.onos_app_attribute.service_instance = self.si
+        self.onos_app_attribute.name = "/onos/v1/network/configuration/apps/org.opencord.olt"
+        self.onos_app_attribute.value = {
+            "kafka" : {
+                "bootstrapServers" : "cord-kafka-kafka.default.svc.cluster.local:9092"
+            }
+        }
+
+    def tearDown(self):
+        self.onos = None
+        sys.path = self.sys_path_save
+
+    @requests_mock.Mocker()
+    def test_defer_app_sync(self, m):
+        self.onos_app.dependencies = "org.onosproject.segmentrouting, org.onosproject.openflow"
+
+        segment_routing = Mock()
+        segment_routing.app_id = "org.onosproject.segmentrouting"
+        segment_routing.backend_code = 1
+
+        openflow = Mock()
+        openflow.app_id = "org.onosproject.openflow"
+        openflow.backend_code = 0
+
+        with patch.object(ONOSApp.objects, "get_items") as app_get, \
+            patch.object(ServiceInstance.objects, "get_items") as mock_si, \
+            self.assertRaises(DeferredException) as e:
+
+            app_get.return_value = [segment_routing, openflow]
+            mock_si.return_value = [self.si]
+            self.sync_step().sync_record(self.onos_app)
+
+        self.assertEqual(e.exception.message, 'Deferring installation of ONOSApp with id 1 as dependencies are not met')
+        self.assertFalse(m.called)
+
+    @requests_mock.Mocker()
+    def test_app_sync_local_app_no_config(self, m):
+        """
+        Activate an application that is already installed in ONOS
+        """
+
+        m.post("http://onos-url:8181/onos/v1/applications/org.onosproject.vrouter/active",
+               status_code=200,
+               additional_matcher=match_none)
+
+        m.get("http://onos-url:8181/onos/v1/applications/org.onosproject.vrouter",
+               status_code=200,
+               json=self.vrouter_app_response)
+
+        self.si.serviceinstanceattribute_dict = {}
+
+        with patch.object(ServiceInstance.objects, "get_items") as mock_si:
+            mock_si.return_value = [self.si]
+            self.sync_step().sync_record(self.onos_app)
+
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 2)
+        self.assertEqual(self.onos_app.version, self.vrouter_app_response["version"])
+
+    @requests_mock.Mocker()
+    def test_app_sync_local_app_with_config(self, m):
+
+        m.post("http://onos-url:8181/onos/v1/applications/org.onosproject.vrouter/active",
+               status_code=200,
+               additional_matcher=match_none)
+
+        m.get("http://onos-url:8181/onos/v1/applications/org.onosproject.vrouter",
+              status_code=200,
+              json=self.vrouter_app_response)
+
+        with patch.object(ServiceInstance.objects, "get_items") as mock_si:
+            mock_si.return_value = [self.si]
+            self.sync_step().sync_record(self.onos_app)
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 2)
+        self.assertEqual(self.onos_app.version, self.vrouter_app_response["version"])
+
+    @requests_mock.Mocker()
+    def test_app_install_remote_app_no_config(self, m):
+        """
+        Install an application that has to be downloaded from a remote source
+        """
+
+        self.onos_app.url = 'http://onf.org/maven/...'
+        self.onos_app.version = "1.13.1"
+        self.onos_app.app_id = None
+
+        expected = {
+            'activate': True,
+            'url': self.onos_app.url
+        }
+
+        m.post("/onos/v1/applications",
+               status_code=200,
+               additional_matcher=functools.partial(match_json, expected),
+               json=self.vrouter_app_response)
+
+        m.get("http://onos-url:8181/onos/v1/applications/org.onosproject.vrouter",
+              status_code=200,
+              json=self.vrouter_app_response)
+
+        self.si.serviceinstanceattribute_dict = {}
+
+        with patch.object(ServiceInstance.objects, "get_items") as mock_si:
+            mock_si.return_value = [self.si]
+            self.sync_step().sync_record(self.onos_app)
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 2)
+        self.assertEqual(self.onos_app.app_id, self.vrouter_app_response["name"])
+
+    @requests_mock.Mocker()
+    def test_update_remote_app(self, m):
+        self.onos_app.url = 'http://onf.org/maven/...'
+        self.onos_app.version = "1.14.1"
+
+        expected = {
+            'activate': True,
+            'url': self.onos_app.url
+        }
+
+        self.vrouter_app_response_updated = self.vrouter_app_response.copy()
+        self.vrouter_app_response_updated["version"] = "1.14.1"
+
+        m.post("/onos/v1/applications",
+               status_code=200,
+               additional_matcher=functools.partial(match_json, expected),
+               json=self.vrouter_app_response)
+
+
+        m.get("http://onos-url:8181/onos/v1/applications/org.onosproject.vrouter",
+              [
+                  {"json": self.vrouter_app_response, "status_code": 200},
+                  {"json": self.vrouter_app_response_updated, "status_code": 200}
+              ]
+        )
+
+        m.delete("http://onos-url:8181/onos/v1/applications/org.onosproject.vrouter",
+                 status_code=204)
+
+        self.si.serviceinstanceattribute_dict = {}
+
+        with patch.object(ServiceInstance.objects, "get_items") as mock_si:
+            mock_si.return_value = [self.si]
+            self.sync_step().sync_record(self.onos_app)
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 4)
+        self.assertEqual(self.onos_app.app_id, self.vrouter_app_response_updated["name"])
+
+    @requests_mock.Mocker()
+    def test_app_sync_remote_app_no_config_fail_version(self, m):
+        """
+        Activate an application that has to be downloaded from a remote source
+        """
+
+        self.onos_app.url = 'http://onf.org/maven/...'
+        self.onos_app.version = "1.14.2"
+        self.onos_app.app_id = None
+
+        expected = {
+            'activate': True,
+            'url': self.onos_app.url
+        }
+
+        m.post("/onos/v1/applications",
+               status_code=200,
+               additional_matcher=functools.partial(match_json, expected),
+               json=self.vrouter_app_response)
+
+        m.get("http://onos-url:8181/onos/v1/applications/org.onosproject.vrouter",
+              status_code=200,
+              json=self.vrouter_app_response)
+
+        self.si.serviceinstanceattribute_dict = {}
+
+        with patch.object(ServiceInstance.objects, "get_items") as mock_si, \
+            self.assertRaises(Exception) as e:
+            mock_si.return_value = [self.si]
+            self.sync_step().sync_record(self.onos_app)
+
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 2)
+        self.assertEqual(self.onos_app.app_id, self.vrouter_app_response["name"])
+        self.assertEqual(e.exception.message, "The version of org.onosproject.vrouter you installed (1.13.1) is not the same you requested (1.14.2)")
+
+    @requests_mock.Mocker()
+    def test_config_delete(self, m):
+        m.delete("http://onos-url:8181%s" % self.onos_app_attribute.name,
+               status_code=204)
+
+        self.sync_step().delete_record(self.onos_app_attribute)
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 1)
+
+    @requests_mock.Mocker()
+    def test_app_deactivate(self, m):
+        m.delete("http://onos-url:8181/onos/v1/applications/org.onosproject.vrouter/active",
+               status_code=204)
+
+        self.sync_step().delete_record(self.onos_app)
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 1)
+
+    @requests_mock.Mocker()
+    def test_app_uninstall(self, m):
+        self.onos_app.url = 'http://onf.org/maven/...'
+        self.onos_app.version = "1.14.2"
+        self.onos_app.backend_code = 1
+
+        m.delete("http://onos-url:8181/onos/v1/applications/org.onosproject.vrouter",
+                 status_code=204)
+
+        self.sync_step().delete_record(self.onos_app)
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 1)
+
+
+
diff --git a/xos/synchronizer/steps/test_sync_onos_service.py b/xos/synchronizer/steps/test_sync_onos_service.py
new file mode 100644
index 0000000..314a0c1
--- /dev/null
+++ b/xos/synchronizer/steps/test_sync_onos_service.py
@@ -0,0 +1,198 @@
+# 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
+
+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 TestSyncOnosService(unittest.TestCase):
+
+    def setUp(self):
+
+        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_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("onos-service", "onos.xproto")
+        ])
+        import synchronizers.new_base.modelaccessor
+
+        from sync_onos_service import SyncONOSService, model_accessor
+
+        # import all class names to globals
+        for (k, v) in model_accessor.all_model_classes.items():
+            globals()[k] = v
+
+
+        self.sync_step = SyncONOSService
+
+        self.onos = Mock(spec=[
+            'id',
+            'name',
+            "rest_hostname",
+            "rest_port",
+            "rest_username",
+            "rest_password",
+            "class_names"
+        ])
+        self.onos.id = 1
+        self.onos.name = "onos"
+        self.onos.rest_hostname = "onos-url"
+        self.onos.rest_port = "8181"
+        self.onos.rest_username = "karaf"
+        self.onos.rest_password = "karaf"
+        self.onos.class_names = "ONOSService"
+
+        self.service = Mock()
+        self.service.id = 1
+        self.service.serviceattribute_dict = {}
+        self.service.leaf_model = self.onos
+
+        self.onos_service_attribute = Mock(spec=[
+            'id',
+            'service',
+            'name',
+            'value'
+        ])
+        self.onos_service_attribute.service = self.service
+        self.onos_service_attribute.name = "/onos/v1/network/configuration/apps/org.opencord.olt"
+        self.onos_service_attribute.value = {
+            "kafka": {
+                "bootstrapServers": "cord-kafka-kafka.default.svc.cluster.local:9092"
+            }
+        }
+
+    def tearDown(self):
+        self.onos = None
+        sys.path = self.sys_path_save
+
+    @requests_mock.Mocker()
+    def test_sync_no_service_attributes(self, m):
+        with patch.object(Service.objects, "get_items") as service_mock:
+            service_mock.return_value = [self.service]
+            self.sync_step().sync_record(self.onos)
+        self.assertFalse(m.called)
+
+    @requests_mock.Mocker()
+    def test_sync_service_attributes_from_service(self, m):
+        expected_conf = '{"foo": "bar"}'
+
+        self.service.serviceattribute_dict = {
+            '/onos/v1/network/configuration/apps/org.onosproject.olt': expected_conf,
+            '/onos/v1/network/configuration/apps/org.onosproject.dhcp': expected_conf
+        }
+
+        m.post("http://onos-url:8181/onos/v1/network/configuration/apps/org.onosproject.olt",
+               status_code=200,
+               additional_matcher=functools.partial(match_json, json.loads(expected_conf)))
+
+        m.post("http://onos-url:8181/onos/v1/network/configuration/apps/org.onosproject.dhcp",
+               status_code=200,
+               additional_matcher=functools.partial(match_json, json.loads(expected_conf)))
+
+        with patch.object(Service.objects, "get_items") as service_mock:
+            service_mock.return_value = [self.service]
+            self.sync_step().sync_record(self.onos)
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 2)
+
+    @requests_mock.Mocker()
+    def test_sync_service_attributes_from_attribute(self, m):
+        expected_conf = '{"foo": "bar"}'
+        self.service.serviceattribute_dict = {
+            '/onos/v1/network/configuration/apps/org.onosproject.olt': expected_conf,
+        }
+        m.post("http://onos-url:8181/onos/v1/network/configuration/apps/org.onosproject.olt",
+               status_code=200,
+               additional_matcher=functools.partial(match_json, json.loads(expected_conf)))
+
+        with patch.object(Service.objects, "get_items") as service_mock:
+            service_mock.return_value = [self.service]
+            self.sync_step().sync_record(self.onos_service_attribute)
+
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 1)
+
+    @requests_mock.Mocker()
+    def test_sync_service_attributes_err(self, m):
+        expected_conf = '{"foo": "bar"}'
+
+        self.service.serviceattribute_dict = {
+            '/onos/v1/network/configuration/apps/org.onosproject.olt': expected_conf,
+        }
+
+        m.post("http://onos-url:8181/onos/v1/network/configuration/apps/org.onosproject.olt",
+               status_code=500,
+               text="Mock Error",
+               additional_matcher=functools.partial(match_json, json.loads(expected_conf)))
+
+        with self.assertRaises(Exception) as e, \
+            patch.object(Service.objects, "get_items") as service_mock:
+
+            service_mock.return_value = [self.service]
+            self.sync_step().sync_record(self.onos)
+
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 1)
+        self.assertEqual(e.exception.message, "Failed to add config http://onos-url:8181/onos/v1/network/configuration/apps/org.onosproject.olt in ONOS")
+
+    @requests_mock.Mocker()
+    def test_delete(self, m):
+        m.delete("http://onos-url:8181%s" % self.onos_service_attribute.name,
+                 status_code=204)
+
+        self.sync_step().delete_record(self.onos_service_attribute)
+        self.assertTrue(m.called)
+        self.assertEqual(m.call_count, 1)
\ No newline at end of file
diff --git a/xos/synchronizer/supervisor/onos-observer.conf b/xos/synchronizer/supervisor/onos-observer.conf
deleted file mode 100644
index 995644e..0000000
--- a/xos/synchronizer/supervisor/onos-observer.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-[supervisord]
-logfile=/var/log/supervisord.log ; (main log file;default $CWD/supervisord.log)
-pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
-nodaemon=true
-
-[program:synchronizer]
-command=python /opt/xos/synchronizers/onos/onos-synchronizer.py -C /opt/xos/synchronizers/onos/onos_synchronizer_config
-stderr_logfile=/var/log/supervisor/synchronizer.err.log
-stdout_logfile=/var/log/supervisor/synchronizer.out.log
diff --git a/xos/synchronizer/scripts/dockerip.sh b/xos/synchronizer/test_config.yaml
similarity index 67%
copy from xos/synchronizer/scripts/dockerip.sh
copy to xos/synchronizer/test_config.yaml
index 3618fa6..61e06f5 100644
--- a/xos/synchronizer/scripts/dockerip.sh
+++ b/xos/synchronizer/test_config.yaml
@@ -13,13 +13,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
-#!/bin/bash
-
-MODE=`docker inspect --format '{{ .HostConfig.NetworkMode }}' $1  | tr -d '\n' | tr -d '\r'`
-if [[ "$MODE" == "host" ]]; then
-    echo -n "127.0.0.1"
-else
-    docker inspect --format '{{ .NetworkSettings.IPAddress }}' $1 | tr -d '\n' | tr -d '\r'
-fi
-
+name: onos-service
+accessor:
+  username: xosadmin@opencord.org
+  password: "sample"
+  kind: "testframework"
+logging:
+  version: 1
+  handlers:
+    console:
+      class: logging.StreamHandler
+  loggers:
+    'multistructlog':
+      handlers:
+        - console
+      level: ERROR
diff --git a/xos/unittest.cfg b/xos/unittest.cfg
new file mode 100644
index 0000000..48ea867
--- /dev/null
+++ b/xos/unittest.cfg
@@ -0,0 +1,4 @@
+[unittest]
+plugins=nose2-plugins.exclude
+code-directories=synchronizer
+                 steps