SEBA-405 Convert synchronizer framework to library

Change-Id: If8562f23dc15c7d18d7a8b040b33756708b3c5ec
diff --git a/lib/xos-synchronizer/tests/steps/__init__.py b/lib/xos-synchronizer/tests/steps/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/__init__.py
@@ -0,0 +1,13 @@
+# 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.
diff --git a/lib/xos-synchronizer/tests/steps/sync_container.py b/lib/xos-synchronizer/tests/steps/sync_container.py
new file mode 100644
index 0000000..baf108f
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_container.py
@@ -0,0 +1,54 @@
+# 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 xossynchronizer.steps.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+from xossynchronizer.steps.syncstep import DeferredException
+from xossynchronizer.mock_modelaccessor import *
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+
+class SyncContainer(SyncInstanceUsingAnsible):
+    provides = [Instance]
+    observes = Instance
+    template_name = "sync_container.yaml"
+
+    def __init__(self, *args, **kwargs):
+        super(SyncContainer, self).__init__(*args, **kwargs)
+
+    def fetch_pending(self, deletion=False):
+        i = Instance()
+        i.name = "Spectacular Sponge"
+        j = Instance()
+        j.name = "Spontaneous Tent"
+        k = Instance()
+        k.name = "Embarrassed Cat"
+
+        objs = [i, j, k]
+        return objs
+
+    def sync_record(self, o):
+        pass
+
+    def delete_record(self, o):
+        pass
diff --git a/lib/xos-synchronizer/tests/steps/sync_controller_images.py b/lib/xos-synchronizer/tests/steps/sync_controller_images.py
new file mode 100644
index 0000000..84a43b1
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_controller_images.py
@@ -0,0 +1,54 @@
+# 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 os
+import base64
+from xossynchronizer.steps.syncstep import SyncStep
+from xossynchronizer.mock_modelaccessor import *
+
+class SyncControllerImages(SyncStep):
+    provides = [ControllerImages]
+    observes = ControllerImages
+    requested_interval = 0
+    playbook = "sync_controller_images.yaml"
+
+    def fetch_pending(self, deleted):
+        ci = ControllerImages()
+        i = Image()
+        i.name = "Lush Loss"
+        ci.i = i
+        return [ci]
+
+    def map_sync_inputs(self, controller_image):
+        image_fields = {
+            "endpoint": controller_image.controller.auth_url,
+            "endpoint_v3": controller_image.controller.auth_url_v3,
+            "admin_user": controller_image.controller.admin_user,
+            "admin_password": controller_image.controller.admin_password,
+            "domain": controller_image.controller.domain,
+            "name": controller_image.image.name,
+            "filepath": controller_image.image.path,
+            # name of ansible playbook
+            "ansible_tag": "%s@%s"
+            % (controller_image.image.name, controller_image.controller.name),
+        }
+
+        return image_fields
+
+    def map_sync_outputs(self, controller_image, res):
+        image_id = res[0]["id"]
+        controller_image.glance_image_id = image_id
+        controller_image.backend_status = "1 - OK"
+        controller_image.save()
diff --git a/lib/xos-synchronizer/tests/steps/sync_controller_networks.py b/lib/xos-synchronizer/tests/steps/sync_controller_networks.py
new file mode 100644
index 0000000..1133545
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_controller_networks.py
@@ -0,0 +1,60 @@
+# 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 os
+import base64
+import struct
+import socket
+from netaddr import IPAddress, IPNetwork
+from xossynchronizer.steps.syncstep import SyncStep
+from xossynchronizer.mock_modelaccessor import *
+
+class SyncControllerNetworks(SyncStep):
+    requested_interval = 0
+    provides = [Network]
+    observes = ControllerNetwork
+    external_dependencies = [User]
+    playbook = "sync_controller_networks.yaml"
+
+    def fetch_pending(self, deleted):
+        ci = ControllerNetwork()
+        i = Network()
+        i.name = "Lush Loss"
+        s = Slice()
+        s.name = "Ghastly Notebook"
+        i.owner = s
+        ci.i = i
+        return [ci]
+
+    def map_sync_outputs(self, controller_network, res):
+        network_id = res[0]["network"]["id"]
+        subnet_id = res[1]["subnet"]["id"]
+        controller_network.net_id = network_id
+        controller_network.subnet = self.cidr
+        controller_network.subnet_id = subnet_id
+        controller_network.backend_status = "1 - OK"
+        if not controller_network.segmentation_id:
+            controller_network.segmentation_id = str(
+                self.get_segmentation_id(controller_network)
+            )
+        controller_network.save()
+
+    def map_sync_inputs(self, controller_network):
+        pass
+
+    def map_delete_inputs(self, controller_network):
+        network_fields = {"endpoint": None, "delete": True}
+
+        return network_fields
diff --git a/lib/xos-synchronizer/tests/steps/sync_controller_site_privileges.py b/lib/xos-synchronizer/tests/steps/sync_controller_site_privileges.py
new file mode 100644
index 0000000..65d3985
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_controller_site_privileges.py
@@ -0,0 +1,107 @@
+# 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 os
+import base64
+import json
+from xossynchronizer.steps.syncstep import SyncStep
+from xossynchronizer.mock_modelaccessor import *
+
+class SyncControllerSitePrivileges(SyncStep):
+    provides = [SitePrivilege]
+    requested_interval = 0
+    observes = ControllerSitePrivilege
+    playbook = "sync_controller_users.yaml"
+
+    def map_sync_inputs(self, controller_site_privilege):
+        controller_register = json.loads(
+            controller_site_privilege.controller.backend_register
+        )
+        if not controller_site_privilege.controller.admin_user:
+            return
+
+        roles = [controller_site_privilege.site_privilege.role.role]
+        # setup user home site roles at controller
+        if not controller_site_privilege.site_privilege.user.site:
+            raise Exception(
+                "Siteless user %s" % controller_site_privilege.site_privilege.user.email
+            )
+        else:
+            # look up tenant id for the user's site at the controller
+            # ctrl_site_deployments = SiteDeployment.objects.filter(
+            #  site_deployment__site=controller_site_privilege.user.site,
+            #  controller=controller_site_privilege.controller)
+
+            # if ctrl_site_deployments:
+            #    # need the correct tenant id for site at the controller
+            #    tenant_id = ctrl_site_deployments[0].tenant_id
+            #    tenant_name = ctrl_site_deployments[0].site_deployment.site.login_base
+            user_fields = {
+                "endpoint": controller_site_privilege.controller.auth_url,
+                "endpoint_v3": controller_site_privilege.controller.auth_url_v3,
+                "domain": controller_site_privilege.controller.domain,
+                "name": controller_site_privilege.site_privilege.user.email,
+                "email": controller_site_privilege.site_privilege.user.email,
+                "password": controller_site_privilege.site_privilege.user.remote_password,
+                "admin_user": controller_site_privilege.controller.admin_user,
+                "admin_password": controller_site_privilege.controller.admin_password,
+                "ansible_tag": "%s@%s"
+                % (
+                    controller_site_privilege.site_privilege.user.email.replace(
+                        "@", "-at-"
+                    ),
+                    controller_site_privilege.controller.name,
+                ),
+                "admin_tenant": controller_site_privilege.controller.admin_tenant,
+                "roles": roles,
+                "tenant": controller_site_privilege.site_privilege.site.login_base,
+            }
+
+            return user_fields
+
+    def map_sync_outputs(self, controller_site_privilege, res):
+        # results is an array in which each element corresponds to an
+        # "ok" string received per operation. If we get as many oks as
+        # the number of operations we issued, that means a grand success.
+        # Otherwise, the number of oks tell us which operation failed.
+        controller_site_privilege.role_id = res[0]["id"]
+        controller_site_privilege.save()
+
+    def delete_record(self, controller_site_privilege):
+        controller_register = json.loads(
+            controller_site_privilege.controller.backend_register
+        )
+        if controller_register.get("disabled", False):
+            raise InnocuousException(
+                "Controller %s is disabled" % controller_site_privilege.controller.name
+            )
+
+        if controller_site_privilege.role_id:
+            driver = self.driver.admin_driver(
+                controller=controller_site_privilege.controller
+            )
+            user = ControllerUser.objects.get(
+                controller=controller_site_privilege.controller,
+                user=controller_site_privilege.site_privilege.user,
+            )
+            site = ControllerSite.objects.get(
+                controller=controller_site_privilege.controller,
+                user=controller_site_privilege.site_privilege.user,
+            )
+            driver.delete_user_role(
+                user.kuser_id,
+                site.tenant_id,
+                controller_site_privilege.site_prvilege.role.role,
+            )
diff --git a/lib/xos-synchronizer/tests/steps/sync_controller_sites.py b/lib/xos-synchronizer/tests/steps/sync_controller_sites.py
new file mode 100644
index 0000000..509a45c
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_controller_sites.py
@@ -0,0 +1,88 @@
+# 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 os
+import base64
+import json
+from xossynchronizer.steps.syncstep import SyncStep
+from xossynchronizer.mock_modelaccessor import *
+
+class SyncControllerSites(SyncStep):
+    requested_interval = 0
+    provides = [Site]
+    observes = ControllerSite
+    playbook = "sync_controller_sites.yaml"
+
+    def fetch_pending(self, deleted=False):
+        lobjs = super(SyncControllerSites, self).fetch_pending(deleted)
+
+        if not deleted:
+            # filter out objects with null controllers
+            lobjs = [x for x in lobjs if x.controller]
+
+        return lobjs
+
+    def map_sync_inputs(self, controller_site):
+        tenant_fields = {
+            "endpoint": controller_site.controller.auth_url,
+            "endpoint_v3": controller_site.controller.auth_url_v3,
+            "domain": controller_site.controller.domain,
+            "admin_user": controller_site.controller.admin_user,
+            "admin_password": controller_site.controller.admin_password,
+            "admin_tenant": controller_site.controller.admin_tenant,
+            # name of ansible playbook
+            "ansible_tag": "%s@%s"
+            % (controller_site.site.login_base, controller_site.controller.name),
+            "tenant": controller_site.site.login_base,
+            "tenant_description": controller_site.site.name,
+        }
+        return tenant_fields
+
+    def map_sync_outputs(self, controller_site, res):
+        controller_site.tenant_id = res[0]["id"]
+        controller_site.backend_status = "1 - OK"
+        controller_site.save()
+
+    def delete_record(self, controller_site):
+        controller_register = json.loads(controller_site.controller.backend_register)
+        if controller_register.get("disabled", False):
+            raise InnocuousException(
+                "Controller %s is disabled" % controller_site.controller.name
+            )
+
+        if controller_site.tenant_id:
+            driver = self.driver.admin_driver(controller=controller_site.controller)
+            driver.delete_tenant(controller_site.tenant_id)
+
+        """
+        Ansible does not support tenant deletion yet
+
+        import pdb
+        pdb.set_trace()
+        template = os_template_env.get_template('delete_controller_sites.yaml')
+        tenant_fields = {'endpoint':controller_site.controller.auth_url,
+                         'admin_user': controller_site.controller.admin_user,
+                         'admin_password': controller_site.controller.admin_password,
+                         'admin_tenant': 'admin',
+                         'ansible_tag': 'controller_sites/%s@%s'%(controller_site.controller_site.site.login_base,controller_site.controller_site.deployment.name), # name of ansible playbook
+                         'tenant': controller_site.controller_site.site.login_base,
+                         'delete': True}
+
+        rendered = template.render(tenant_fields)
+        res = run_template('sync_controller_sites.yaml', tenant_fields)
+
+        if (len(res)!=1):
+                raise Exception('Could not assign roles for user %s'%tenant_fields['tenant'])
+        """
diff --git a/lib/xos-synchronizer/tests/steps/sync_controller_slice_privileges.py b/lib/xos-synchronizer/tests/steps/sync_controller_slice_privileges.py
new file mode 100644
index 0000000..ec0667c
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_controller_slice_privileges.py
@@ -0,0 +1,95 @@
+# 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 os
+import base64
+import json
+from xossynchronizer.steps.syncstep import SyncStep
+from xossynchronizer.mock_modelaccessor import *
+
+class SyncControllerSlicePrivileges(SyncStep):
+    provides = [SlicePrivilege]
+    requested_interval = 0
+    observes = ControllerSlicePrivilege
+    playbook = "sync_controller_users.yaml"
+
+    def map_sync_inputs(self, controller_slice_privilege):
+        if not controller_slice_privilege.controller.admin_user:
+            return
+
+        template = os_template_env.get_template("sync_controller_users.yaml")
+        roles = [controller_slice_privilege.slice_privilege.role.role]
+        # setup user home slice roles at controller
+        if not controller_slice_privilege.slice_privilege.user.site:
+            raise Exception(
+                "Sliceless user %s"
+                % controller_slice_privilege.slice_privilege.user.email
+            )
+        else:
+            user_fields = {
+                "endpoint": controller_slice_privilege.controller.auth_url,
+                "endpoint_v3": controller_slice_privilege.controller.auth_url_v3,
+                "domain": controller_slice_privilege.controller.domain,
+                "name": controller_slice_privilege.slice_privilege.user.email,
+                "email": controller_slice_privilege.slice_privilege.user.email,
+                "password": controller_slice_privilege.slice_privilege.user.remote_password,
+                "admin_user": controller_slice_privilege.controller.admin_user,
+                "admin_password": controller_slice_privilege.controller.admin_password,
+                "ansible_tag": "%s@%s@%s"
+                % (
+                    controller_slice_privilege.slice_privilege.user.email.replace(
+                        "@", "-at-"
+                    ),
+                    controller_slice_privilege.slice_privilege.slice.name,
+                    controller_slice_privilege.controller.name,
+                ),
+                "admin_tenant": controller_slice_privilege.controller.admin_tenant,
+                "roles": roles,
+                "tenant": controller_slice_privilege.slice_privilege.slice.name,
+            }
+            return user_fields
+
+    def map_sync_outputs(self, controller_slice_privilege, res):
+        controller_slice_privilege.role_id = res[0]["id"]
+        controller_slice_privilege.save()
+
+    def delete_record(self, controller_slice_privilege):
+        controller_register = json.loads(
+            controller_slice_privilege.controller.backend_register
+        )
+        if controller_register.get("disabled", False):
+            raise InnocuousException(
+                "Controller %s is disabled" % controller_slice_privilege.controller.name
+            )
+
+        if controller_slice_privilege.role_id:
+            driver = self.driver.admin_driver(
+                controller=controller_slice_privilege.controller
+            )
+            user = ControllerUser.objects.filter(
+                controller_id=controller_slice_privilege.controller.id,
+                user_id=controller_slice_privilege.slice_privilege.user.id,
+            )
+            user = user[0]
+            slice = ControllerSlice.objects.filter(
+                controller_id=controller_slice_privilege.controller.id,
+                user_id=controller_slice_privilege.slice_privilege.user.id,
+            )
+            slice = slice[0]
+            driver.delete_user_role(
+                user.kuser_id,
+                slice.tenant_id,
+                controller_slice_privilege.slice_prvilege.role.role,
+            )
diff --git a/lib/xos-synchronizer/tests/steps/sync_controller_slices.py b/lib/xos-synchronizer/tests/steps/sync_controller_slices.py
new file mode 100644
index 0000000..0f43bad
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_controller_slices.py
@@ -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.
+
+
+import os
+import base64
+from xossynchronizer.steps.syncstep import SyncStep, DeferredException
+from xossynchronizer.mock_modelaccessor import *
+
+class SyncControllerSlices(SyncStep):
+    provides = [Slice]
+    requested_interval = 0
+    observes = ControllerSlice
+    playbook = "sync_controller_slices.yaml"
+
+    def map_sync_inputs(self, controller_slice):
+        if getattr(controller_slice, "force_fail", None):
+            raise Exception("Forced failure")
+        elif getattr(controller_slice, "force_defer", None):
+            raise DeferredException("Forced defer")
+
+        tenant_fields = {"endpoint": "endpoint", "name": "Flagrant Haircut"}
+
+        return tenant_fields
+
+    def map_sync_outputs(self, controller_slice, res):
+        controller_slice.save()
+
+    def map_delete_inputs(self, controller_slice):
+        tenant_fields = {
+            "endpoint": "endpoint",
+            "name": "Conscientious Plastic",
+            "delete": True,
+        }
+        return tenant_fields
diff --git a/lib/xos-synchronizer/tests/steps/sync_controller_users.py b/lib/xos-synchronizer/tests/steps/sync_controller_users.py
new file mode 100644
index 0000000..881e78a
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_controller_users.py
@@ -0,0 +1,72 @@
+# 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 os
+import base64
+from xossynchronizer.steps.syncstep import SyncStep
+from xossynchronizer.mock_modelaccessor import *
+
+
+class SyncControllerUsers(SyncStep):
+    provides = [User]
+    requested_interval = 0
+    observes = ControllerUser
+    playbook = "sync_controller_users.yaml"
+
+    def map_sync_inputs(self, controller_user):
+        if not controller_user.controller.admin_user:
+            return
+
+        # All users will have at least the 'user' role at their home site/tenant.
+        # We must also check if the user should have the admin role
+
+        roles = ["user"]
+        if controller_user.user.is_admin:
+            driver = self.driver.admin_driver(controller=controller_user.controller)
+            roles.append(driver.get_admin_role().name)
+
+        # setup user home site roles at controller
+        if not controller_user.user.site:
+            raise Exception("Siteless user %s" % controller_user.user.email)
+        else:
+            user_fields = {
+                "endpoint": controller_user.controller.auth_url,
+                "endpoint_v3": controller_user.controller.auth_url_v3,
+                "domain": controller_user.controller.domain,
+                "name": controller_user.user.email,
+                "email": controller_user.user.email,
+                "password": controller_user.user.remote_password,
+                "admin_user": controller_user.controller.admin_user,
+                "admin_password": controller_user.controller.admin_password,
+                "ansible_tag": "%s@%s"
+                % (
+                    controller_user.user.email.replace("@", "-at-"),
+                    controller_user.controller.name,
+                ),
+                "admin_project": controller_user.controller.admin_tenant,
+                "roles": roles,
+                "project": controller_user.user.site.login_base,
+            }
+            return user_fields
+
+    def map_sync_outputs(self, controller_user, res):
+        controller_user.kuser_id = res[0]["user"]["id"]
+        controller_user.backend_status = "1 - OK"
+        controller_user.save()
+
+    def delete_record(self, controller_user):
+        if controller_user.kuser_id:
+            driver = self.driver.admin_driver(controller=controller_user.controller)
+            driver.delete_user(controller_user.kuser_id)
diff --git a/lib/xos-synchronizer/tests/steps/sync_images.py b/lib/xos-synchronizer/tests/steps/sync_images.py
new file mode 100644
index 0000000..2284ed2
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_images.py
@@ -0,0 +1,28 @@
+# 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 os
+import base64
+from xossynchronizer.steps.syncstep import SyncStep
+from xossynchronizer.mock_modelaccessor import *
+
+class SyncImages(SyncStep):
+    provides = [Image]
+    requested_interval = 0
+    observes = [Image]
+
+    def sync_record(self, role):
+        # do nothing
+        pass
diff --git a/lib/xos-synchronizer/tests/steps/sync_instances.py b/lib/xos-synchronizer/tests/steps/sync_instances.py
new file mode 100644
index 0000000..479b87d
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_instances.py
@@ -0,0 +1,65 @@
+# 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 os
+import base64
+import socket
+from xossynchronizer.steps import syncstep
+from xossynchronizer.mock_modelaccessor import *
+
+RESTAPI_HOSTNAME = socket.gethostname()
+RESTAPI_PORT = "8000"
+
+
+def escape(s):
+    s = s.replace("\n", r"\n").replace('"', r"\"")
+    return s
+
+
+class SyncInstances(syncstep.SyncStep):
+    provides = [Instance]
+    requested_interval = 0
+    observes = Instance
+    playbook = "sync_instances.yaml"
+
+    def fetch_pending(self, deletion=False):
+        objs = super(SyncInstances, self).fetch_pending(deletion)
+        objs = [x for x in objs if x.isolation == "vm"]
+        return objs
+
+    def map_sync_inputs(self, instance):
+        inputs = {}
+        metadata_update = {}
+
+        fields = {"name": instance.name, "delete": False}
+        return fields
+
+    def map_sync_outputs(self, instance, res):
+        instance.save()
+
+    def map_delete_inputs(self, instance):
+        input = {
+            "endpoint": "endpoint",
+            "admin_user": "admin_user",
+            "admin_password": "admin_password",
+            "project_name": "project_name",
+            "tenant": "tenant",
+            "tenant_description": "tenant_description",
+            "name": instance.name,
+            "ansible_tag": "ansible_tag",
+            "delete": True,
+        }
+
+        return input
diff --git a/lib/xos-synchronizer/tests/steps/sync_ports.py b/lib/xos-synchronizer/tests/steps/sync_ports.py
new file mode 100644
index 0000000..77209a5
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_ports.py
@@ -0,0 +1,37 @@
+# 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 os
+import base64
+from xossynchronizer.steps.syncstep import SyncStep
+from xossynchronizer.mock_modelaccessor import *
+
+
+class SyncPort(SyncStep):
+    requested_interval = 0  # 3600
+    provides = [Port]
+    observes = Port
+
+    def call(self, failed=[], deletion=False):
+        if deletion:
+            self.delete_ports()
+        else:
+            self.sync_ports()
+
+    def sync_ports(self):
+        open("/tmp/sync_ports", "w").write("Sync successful")
+
+    def delete_ports(self):
+        open("/tmp/delete_ports", "w").write("Delete successful")
diff --git a/lib/xos-synchronizer/tests/steps/sync_roles.py b/lib/xos-synchronizer/tests/steps/sync_roles.py
new file mode 100644
index 0000000..e8b1364
--- /dev/null
+++ b/lib/xos-synchronizer/tests/steps/sync_roles.py
@@ -0,0 +1,33 @@
+# 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 os
+import base64
+from xossynchronizer.steps.syncstep import SyncStep
+from xossynchronizer.mock_modelaccessor import *
+
+
+class SyncRoles(SyncStep):
+    provides = [Role]
+    requested_interval = 0
+    observes = [SiteRole, SliceRole, ControllerRole]
+
+    def sync_record(self, role):
+        if not role.enacted:
+            controllers = Controller.objects.all()
+            for controller in controllers:
+                driver = self.driver.admin_driver(controller=controller)
+                driver.create_role(role.role)
+            role.save()