CORD-2964 Implement new openstack models and steps

Change-Id: I32ac438e799f563b721e19ad7ebc8a033796c88e
diff --git a/Dockerfile.synchronizer b/Dockerfile.synchronizer
index 4e5d0ff..d5b71a5 100644
--- a/Dockerfile.synchronizer
+++ b/Dockerfile.synchronizer
@@ -19,6 +19,10 @@
 COPY xos/synchronizer /opt/xos/synchronizers/openstack
 COPY VERSION /opt/xos/synchronizers/openstack/
 
+ENV OS_IDENTITY_API_VERSION=3
+
+ENTRYPOINT []
+
 WORKDIR "/opt/xos/synchronizers/openstack"
 
 # Label image
diff --git a/xos/examples/make_instance.xossh b/xos/examples/make_instance.xossh
new file mode 100644
index 0000000..ee3b601
--- /dev/null
+++ b/xos/examples/make_instance.xossh
@@ -0,0 +1,41 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This file is intended to be pasted as input into an `xossh` session.
+
+# Creates a TrustDomain, Principal, Slice, Image (if it does not exist), and
+# then brings up an openstack VM attached to the management network.
+
+t=TrustDomain(owner=OpenStackService.objects.first(), name="demo-trust")
+t.save()
+
+p=Principal(trust_domain=t, name="demo-user")
+p.save()
+
+s=Slice(trust_domain=t, name="demo-slice", site=Site.objects.first())
+s.save()
+
+ns=NetworkSlice(slice=s, network=Network.objects.get(name="management"))
+ns.save()
+
+img=Image.objects.filter(name="cirros-0.3.5")
+if img:
+   img=img[0]
+else:
+   img=Image(name="cirros-0.3.5", container_format="BARE", disk_format="QCOW2", path="http://download.cirros-cloud.net/0.3.5/cirros-0.3.5-x86_64-disk.img")
+   img.save()
+
+i=OpenStackServiceInstance(slice=s, image=img, name="demo-instance", owner=OpenStackService.objects.first(), flavor=Flavor.objects.get(name="m1.medium"), node=Node.objects.first())
+i.save()
diff --git a/xos/nose2-plugins/__init__.py b/xos/nose2-plugins/__init__.py
new file mode 100644
index 0000000..42722a8
--- /dev/null
+++ b/xos/nose2-plugins/__init__.py
@@ -0,0 +1,14 @@
+
+# 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/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/model_policies/model_policy_Slice.py b/xos/synchronizer/model_policies/model_policy_Slice.py
index 87a5ef4..a48f5a2 100644
--- a/xos/synchronizer/model_policies/model_policy_Slice.py
+++ b/xos/synchronizer/model_policies/model_policy_Slice.py
@@ -24,6 +24,11 @@
         return self.handle_update(slice)
 
     def handle_update(self, slice):
+        # Ignore new-style slices as we don't want to run all the old policies
+        if (slice.trust_domain != None):
+            self.logger.info("This is a new-style openstack slice, which this policy shall ignore")
+            return
+
         support_nat_net = False # Assume we're using VTN rather than nat-net
 
         # slice = Slice.get(slice_id)
@@ -109,10 +114,12 @@
 
     # TODO: This feels redundant with the reaper
     def handle_delete(self, slice):
-        public_nets = []
-        private_net = None
-        networks = Network.objects.filter(owner_id=slice.id)
+        # Ignore new-style slices as we don't want to run all the old policies
+        if (slice.trust_domain != None):
+            self.logger.info("This is a new-style openstack slice, which this policy shall ignore")
+            return
 
+        networks = Network.objects.filter(owner_id=slice.id)
         for n in networks:
             n.delete()
 
diff --git a/xos/synchronizer/models/openstack.xproto b/xos/synchronizer/models/openstack.xproto
new file mode 100644
index 0000000..9565da2
--- /dev/null
+++ b/xos/synchronizer/models/openstack.xproto
@@ -0,0 +1,22 @@
+option app_label = "openstack";
+option name = "openstack";
+
+message OpenStackService (Service){
+    option verbose_name = "OpenStack Service";
+
+     optional string auth_url = 1 [max_length = 200, content_type = "stripped", blank = True, help_text = "Auth url for the OpenStack controller", null = True, db_index = False];
+     optional string admin_user = 2 [max_length = 200, content_type = "stripped", blank = True, help_text = "Username of an admin user at this OpenStack", null = True, db_index = False];
+     optional string admin_password = 3 [max_length = 200, content_type = "stripped", blank = True, help_text = "Password of theadmin user at this OpenStack", null = True, db_index = False];
+     optional string admin_tenant = 4 [max_length = 200, content_type = "stripped", blank = True, help_text = "Name of the tenant the admin user belongs to", null = True, db_index = False];
+}
+
+message OpenStackServiceInstance (ComputeServiceInstance){
+     option verbose_name = "OpenStack Service Instance";
+
+     optional manytoone flavor->Flavor:openstackinstance = 1 [null = True, db_index = True, blank = True, help_text = "Flavor of this instance"];
+     optional manytoone node->Node:openstackinstances = 2 [db_index = True, null = True, blank = True, help_text = "Node on which to deploy this instance"];
+
+     optional string admin_password = 3 [max_length = 200, content_type = "stripped", blank = True, help_text = "Admin password for instance", null = True, db_index = False];
+}
+
+
diff --git a/xos/synchronizer/openstack_config.yaml b/xos/synchronizer/openstack_config.yaml
index 00d2c1a..5963b1c 100644
--- a/xos/synchronizer/openstack_config.yaml
+++ b/xos/synchronizer/openstack_config.yaml
@@ -20,8 +20,10 @@
   password: "@/opt/xos/services/openstack/credentials/xosadmin@opencord.org"
 dependency_graph: "/opt/xos/synchronizers/openstack/model-deps"
 steps_dir: "/opt/xos/synchronizers/openstack/steps"
+pull_steps_dir: "/opt/xos/synchronizers/openstack/pull_steps"
 sys_dir: "/opt/xos/synchronizers/openstack/sys"
 model_policies_dir: "/opt/xos/synchronizers/openstack/model_policies"
+models_dir: "/opt/xos/synchronizers/openstack/models"
 images_directory: "/opt/xos/images"
 required_models:
   - ControllerImages
diff --git a/xos/synchronizer/openstacksyncstep.py b/xos/synchronizer/openstacksyncstep.py
index 3054abf..bb5fe5e 100644
--- a/xos/synchronizer/openstacksyncstep.py
+++ b/xos/synchronizer/openstacksyncstep.py
@@ -14,17 +14,15 @@
 # limitations under the License.
 
 
-import os
-import base64
 from synchronizers.new_base.syncstep import SyncStep
 
 class OpenStackSyncStep(SyncStep):
-    """ XOS Sync step for copying data to OpenStack 
-    """ 
-    
-    def __init__(self, **args):
-        SyncStep.__init__(self, **args)
-        return
+    """ XOS Sync step for copying data to OpenStack
+    """
 
+    def __init__(self, *args, **kwargs):
+        SyncStep.__init__(self, *args, **kwargs)
+
+    # TODO(smbaker): This should be explained.
     def __call__(self, **args):
         return self.call(**args)
diff --git a/xos/synchronizer/pull_steps/pull_ports.py b/xos/synchronizer/pull_steps/pull_ports.py
new file mode 100644
index 0000000..425d3eb
--- /dev/null
+++ b/xos/synchronizer/pull_steps/pull_ports.py
@@ -0,0 +1,94 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from synchronizers.new_base.pullstep import PullStep
+from synchronizers.new_base.modelaccessor import Network, Port, OpenStackService, OpenStackServiceInstance
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class OpenStackPortPullStep(PullStep):
+    def __init__(self):
+        super(OpenStackPortPullStep, self).__init__(observed_model=Port)
+
+    def connect_openstack_admin(self, service, required_version=None):
+        import openstack
+
+        if required_version:
+            if LooseVersion(openstack.version.__version__) < LooseVersion(required_version):
+                raise Exception("Insufficient OpenStack library version",
+                                installed_version=openstack.version__version__,
+                                required_version=required_version)
+
+        conn = openstack.connect(auth_url=service.auth_url,
+                                 project_name="admin",
+                                 username=service.admin_user,
+                                 password=service.admin_password,
+                                 user_domain_name="Default",
+                                 project_domain_name="Default")
+        return conn
+
+    def pull_records(self):
+        service = OpenStackService.objects.first() # TODO(smbaker): Fix, hardcoded
+        conn = self.connect_openstack_admin(service)
+
+        ports = Port.objects.all()
+        ports_by_id = {}
+        ports_by_neutron_port = {}
+        for port in ports:
+            ports_by_id[port.id] = port
+            ports_by_neutron_port[port.port_id] = port
+
+        networks = Network.objects.all()
+        networks_by_id = {}
+        for network in networks:
+            for nd in network.controllernetworks.all():
+                networks_by_id[nd.net_id] = network
+
+        os_instances = OpenStackServiceInstance.objects.all()
+        os_instances_by_handle = {}
+        for instance in os_instances:
+            if instance.backend_handle:
+                os_instances_by_handle[instance.backend_handle] = instance
+
+        os_ports = list(conn.network.ports())
+        for os_port in os_ports:
+            if os_port.id in ports_by_neutron_port:
+                # we already have it
+                continue
+            if os_port.device_id not in os_instances_by_handle:
+                # it's not one of ours
+                log.info("No instance for port", os_port=os_port)
+                continue
+            if os_port.network_id not in networks_by_id:
+                # there's no network for it
+                log.info("No network for port", os_port=os_port)
+                continue
+            if not os_port.fixed_ips:
+                # there's no ip address
+                log.info("No ip for port", os_port=os_port)
+                continue
+            network = networks_by_id[os_port.network_id]
+            instance = os_instances_by_handle[os_port.device_id]
+            ip = os_port.fixed_ips[0]["ip_address"]
+            mac = os_port.mac_address
+            port = Port(network=network,
+                        instance=None, # TODO(smbaker): link to openstack instance
+                        ip=ip,
+                        mac=mac,
+                        port_id=os_port.id)
+            port.save()
+            log.info("Created port", port=port, os_port=os_port)
diff --git a/xos/synchronizer/steps/newopenstacksyncstep.py b/xos/synchronizer/steps/newopenstacksyncstep.py
new file mode 100644
index 0000000..1e16b49
--- /dev/null
+++ b/xos/synchronizer/steps/newopenstacksyncstep.py
@@ -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.
+
+
+from distutils.version import LooseVersion
+from synchronizers.new_base.syncstep import SyncStep
+
+class NewOpenStackSyncStep(SyncStep):
+    """ XOS Sync step for copying data to OpenStack
+    """
+
+    def __init__(self, *args, **kwargs):
+        # super() does not work here...
+        SyncStep.__init__(self, *args, **kwargs)
+
+    def connect_openstack_admin(self, service, required_version=None):
+        import openstack
+
+        if required_version:
+            if LooseVersion(openstack.version.__version__) < LooseVersion(required_version):
+                raise Exception("Insufficient OpenStack library version",
+                                installed_version=openstack.version__version__,
+                                required_version=required_version)
+
+        conn = openstack.connect(auth_url=service.auth_url,
+                                 project_name="admin",
+                                 username=service.admin_user,
+                                 password=service.admin_password,
+                                 user_domain_name="Default",
+                                 project_domain_name="Default")
+        return conn
+
+    def connect_openstack_slice(self, slice, required_version=None):
+        import openstack
+
+        trust_domain = slice.trust_domain
+        service = trust_domain.owner.leaf_model
+
+        if required_version:
+            if LooseVersion(openstack.version.__version__) < LooseVersion(required_version):
+                raise Exception("Insufficient OpenStack library version",
+                                installed_version=openstack.version__version__,
+                                required_version=required_version)
+
+        # This is not working yet...
+
+        conn = openstack.connect(auth_url=service.auth_url,
+                                 project_name=slice.name,
+                                 username=service.admin_user,
+                                 password=service.admin_password,
+                                 user_domain_name="Default",
+                                 project_domain_name=trust_domain.name)
+        return conn
+
+    # TODO(smbaker): This should be explained.
+    def __call__(self, **args):
+        return self.call(**args)
diff --git a/xos/synchronizer/steps/sync_controller_sites.yaml b/xos/synchronizer/steps/sync_controller_sites.yaml
index 729bc6a..7398ae1 100644
--- a/xos/synchronizer/steps/sync_controller_sites.yaml
+++ b/xos/synchronizer/steps/sync_controller_sites.yaml
@@ -31,4 +31,4 @@
       interface: "admin"
       name: "{{ project }}"
       description: "{{ project_description }}"
-
+      domain: "{{ domain }}"
diff --git a/xos/synchronizer/steps/sync_controller_slices.yaml b/xos/synchronizer/steps/sync_controller_slices.yaml
index d224d5f..150179d 100644
--- a/xos/synchronizer/steps/sync_controller_slices.yaml
+++ b/xos/synchronizer/steps/sync_controller_slices.yaml
@@ -34,6 +34,7 @@
       state: absent
 {% else %}
       description: "{{ project_description }}"
+      domain: "{{ domain }}"
 
 {% for role in roles %}
   - name: Create role "{{ role }}"
diff --git a/xos/synchronizer/steps/sync_controller_users.yaml b/xos/synchronizer/steps/sync_controller_users.yaml
index 17db144..853a585 100644
--- a/xos/synchronizer/steps/sync_controller_users.yaml
+++ b/xos/synchronizer/steps/sync_controller_users.yaml
@@ -32,6 +32,7 @@
       name: "{{ name }}"
       email: "{{ email }}"
       password: "{{ password }}"
+      domain: "{{ domain }}"
 
   - name: Create project for "{{ project }}"
     os_project:
@@ -46,6 +47,7 @@
         {%- endif %}
       interface: "admin"
       name: "{{ project }}"
+      domain: "{{ domain }}"
 
 {% for role in roles %}
   - name: Create role "{{ role }}"
diff --git a/xos/synchronizer/steps/sync_openstack_service.py b/xos/synchronizer/steps/sync_openstack_service.py
new file mode 100644
index 0000000..79e2df8
--- /dev/null
+++ b/xos/synchronizer/steps/sync_openstack_service.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.
+
+
+from synchronizers.new_base.modelaccessor import OpenStackService
+from newopenstacksyncstep import NewOpenStackSyncStep
+
+class SyncOpenStackService(NewOpenStackSyncStep):
+    provides=[OpenStackService]
+    requested_interval=0
+    observes=OpenStackService
+
+    def sync_record(self, service):
+        # nothing to do
+        pass
+
+    def delete_record(self, service):
+        # nothing to do
+        pass
+
diff --git a/xos/synchronizer/steps/sync_openstackserviceinstance.py b/xos/synchronizer/steps/sync_openstackserviceinstance.py
new file mode 100644
index 0000000..6ae91a1
--- /dev/null
+++ b/xos/synchronizer/steps/sync_openstackserviceinstance.py
@@ -0,0 +1,134 @@
+
+# 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 base64
+import random
+import string
+
+from synchronizers.new_base.modelaccessor import OpenStackServiceInstance, Node, NetworkSlice, Flavor
+from newopenstacksyncstep import NewOpenStackSyncStep
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class SyncOpenStackServiceInstance(NewOpenStackSyncStep):
+    provides=[OpenStackServiceInstance]
+    requested_interval=0
+    observes=OpenStackServiceInstance
+
+    def get_connected_networks(self, instance):
+        xos_networks = [ns.network for ns in NetworkSlice.objects.filter(slice_id=instance.slice.id)]
+        return xos_networks
+
+    def get_user_data(self, instance):
+        pubkeys=[]
+
+        if instance.slice.creator and instance.slice.creator.public_key:
+            pubkeys.add(instance.slice.creator.public_key)
+
+        if instance.slice.service and instance.slice.service.public_key:
+            pubkeys.add(instance.slice.service.public_key)
+
+        userdata = '#cloud-config\n\n'
+#        userdata += 'opencloud:\n   slicename: "%s"\n   hostname: "%s"\n   restapi_hostname: "%s"\n   restapi_port: "%s"\n' % (
+#        instance.slice.name, instance.node.name, RESTAPI_HOSTNAME, str(RESTAPI_PORT))
+        userdata += 'ssh_authorized_keys:\n'
+        for key in pubkeys:
+            userdata += '  - %s\n' % key
+
+        log.info("generated userdata", userdata=userdata)
+
+        return userdata
+
+    def sync_record(self, instance):
+        slice = instance.slice
+        if not slice.trust_domain:
+            raise Exception("Instance's slice has no trust domain")
+
+        service = instance.slice.trust_domain.owner.leaf_model
+        #conn = self.connect_openstack_slice(slice)
+        conn = self.connect_openstack_admin(service)
+
+        os_domain = conn.identity.find_domain(slice.trust_domain.name)
+        os_project = conn.identity.find_project(slice.name, domain_id=os_domain.id)
+
+        os_instances = list(conn.compute.servers(name=instance.name, project_id=os_project.id))
+        if os_instances:
+            os_instance=os_instances[0]
+            log.info("Instance already exists in openstack", instance=instance)
+        else:
+            image_name = instance.image.name
+            image_id = conn.compute.find_image(image_name).id
+
+            if instance.flavor:
+                flavor_name = instance.flavor.name
+            else:
+                # pick a sensible default
+                flavor_name = "m1.small"
+            flavor_id = conn.compute.find_flavor(flavor_name).id
+
+            xos_networks = self.get_connected_networks(instance)
+            networks = []
+            for xos_network in xos_networks:
+                networks.append({"uuid": conn.network.find_network(xos_network.name).id})
+
+            # TODO(smbaker): No ssh keys specified
+
+            availability_zone="nova:%s" % instance.node.name
+
+            log.info("Creating Instance", instance=instance, image_id=image_id, flavor_id=flavor_id,
+                     availability_zone=availability_zone,
+                     networks=networks)
+
+            if not instance.admin_password:
+                instance.admin_password = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8))
+                instance.save(update_fields=["admin_password"])
+
+            user_data = self.get_user_data(instance)
+
+            os_instance = conn.compute.create_server(name=instance.name,
+                                                     image_id=image_id,
+                                                     flavor_id=flavor_id,
+                                                     project_domain_id=os_project.id,
+                                                     availability_zone=availability_zone,
+                                                     networks=networks,
+                                                     config_drive=True,
+                                                     user_data=base64.b64encode(user_data),
+                                                     admin_password=instance.admin_password)
+
+        if os_instance.id != instance.backend_handle:
+            instance.backend_handle = os_instance.id
+            instance.save(update_fields=["backend_handle"])
+
+    def delete_record(self, instance):
+        slice = instance.slice
+        if not slice.trust_domain:
+            raise Exception("Instance's slice has no trust domain")
+
+        service = slice.trust_domain.owner.leaf_model
+        conn = self.connect_openstack_admin(service)
+
+        os_domain = conn.identity.find_domain(slice.trust_domain.name)
+        os_project = conn.identity.find_project(slice.name, domain_id=os_domain.id)
+
+        os_instances = list(conn.compute.servers(name=instance.name, project_id=os_project.id))
+        if (not os_instances):
+            log.info("Instance already does not exist in openstack", instance=instance)
+        else:
+            os_instance=os_instances[0]
+            log.info("Deleting Instance", instance=instance, os_id=os_instance.id)
+            conn.compute.delete_server(os_instance.id)
diff --git a/xos/synchronizer/steps/sync_principal.py b/xos/synchronizer/steps/sync_principal.py
new file mode 100644
index 0000000..9b401a9
--- /dev/null
+++ b/xos/synchronizer/steps/sync_principal.py
@@ -0,0 +1,75 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from synchronizers.new_base.modelaccessor import TrustDomain, Principal
+from newopenstacksyncstep import NewOpenStackSyncStep
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class SyncPrincipal(NewOpenStackSyncStep):
+    provides=[Principal]
+    requested_interval=0
+    observes=Principal
+
+    def fetch_pending(self, deleted):
+        """ Figure out which Principals are interesting to the OpenStack synchronizer. It's necessary to filter as we're
+            synchronizing a core model, and we only want to synchronize trust domains that will exist within
+            OpenStack.
+        """
+        objs = super(SyncPrincipal, self).fetch_pending(deleted)
+        for obj in objs[:]:
+            # If the Principal isn't in a TrustDomain, then the OpenStack synchronizer can't do anything with it
+            if not obj.trust_domain:
+                objs.remove(obj)
+                continue
+
+            # If the TrustDomain isn't part of the OpenStack service, then it's someone else's trust domain
+            if "OpenStackService" not in obj.trust_domain.owner.leaf_model.class_names:
+                objs.remove(obj)
+        return objs
+
+    def sync_record(self, principal):
+        service = principal.trust_domain.owner.leaf_model
+        conn = self.connect_openstack_admin(service)
+
+        os_domain = conn.identity.find_domain(principal.trust_domain.name)
+
+        os_user = conn.identity.find_user(principal.name, domain_id=os_domain.id)
+        if (os_user):
+            log.info("Principal already exists in openstack", principal=principal)
+        else:
+            log.info("Creating Principal", principal=principal)
+            os_user = conn.identity.create_user(name=principal.name, domain_id=os_domain.id)
+
+        if os_user.id != principal.backend_handle:
+            principal.backend_handle = os_user.id
+            principal.save(update_fields=["backend_handle"])
+
+    def delete_record(self, principal):
+        service = principal.trust_domain.owner.leaf_model
+        conn = self.connect_openstack_admin(service)
+
+        os_domain = conn.identity.find_domain(principal.trust_domain.name)
+
+        os_user = conn.identity.find_user(principal.name, domain_id=os_domain.id)
+        if (not os_user):
+            log.info("Principal already does not exist in openstack", principal=principal)
+        else:
+            log.info("Deleting Principal", principal=principal, os_id=os_domain.id)
+            conn.identity.delete_user(os_user.id)
diff --git a/xos/synchronizer/steps/sync_slice.py b/xos/synchronizer/steps/sync_slice.py
new file mode 100644
index 0000000..81ca6dc
--- /dev/null
+++ b/xos/synchronizer/steps/sync_slice.py
@@ -0,0 +1,76 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from synchronizers.new_base.ansible_helper import *
+from synchronizers.new_base.modelaccessor import Slice
+from newopenstacksyncstep import NewOpenStackSyncStep
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class SyncSlice(NewOpenStackSyncStep):
+    provides=[Slice]
+    requested_interval=0
+    observes=Slice
+
+    def fetch_pending(self, deleted):
+        """ Figure out which Principals are interesting to the OpenStack synchronizer. It's necessary to filter as we're
+            synchronizing a core model, and we only want to synchronize trust domains that will exist within
+            OpenStack.
+        """
+        objs = super(SyncSlice, self).fetch_pending(deleted)
+        for obj in objs[:]:
+            # If the Slice isn't in a TrustDomain, then the OpenStack synchronizer can't do anything with it
+            if not obj.trust_domain:
+                objs.remove(obj)
+                continue
+
+            # If the TrustDomain isn't part of the OpenStack service, then it's someone else's trust domain
+            if "OpenStackService" not in obj.trust_domain.owner.leaf_model.class_names:
+                objs.remove(obj)
+        return objs
+
+    def sync_record(self, slice):
+        service = slice.trust_domain.owner.leaf_model
+        conn = self.connect_openstack_admin(service)
+
+        os_domain = conn.identity.find_domain(slice.trust_domain.name)
+
+        os_slice = conn.identity.find_project(slice.name, domain_id=os_domain.id)
+        if os_slice:
+            log.info("Slice already exists in openstack", slice=slice)
+        else:
+            log.info("Creating Slice", slice=slice)
+            os_slice = conn.identity.create_project(name=slice.name, domain_id=os_domain.id)
+
+        if os_slice.id != slice.backend_handle:
+            slice.backend_handle = os_slice.id
+            slice.save(update_fields=["backend_handle"])
+
+    def delete_record(self, slice):
+        service = slice.trust_domain.owner.leaf_model
+        conn = self.connect_openstack_admin(service)
+
+        os_domain = conn.identity.find_domain(slice.trust_domain.name)
+
+        os_slice = conn.identity.find_project(slice.name, domain_id=os_domain.id)
+        if (not os_slice):
+            log.info("Slice already does not exist in openstack", slice=slice)
+        else:
+            log.info("Deleting Slice", slice=slice, os_id=os_slice.id)
+            conn.identity.delete_project(os_slice.id)
diff --git a/xos/synchronizer/steps/sync_trustdomain.py b/xos/synchronizer/steps/sync_trustdomain.py
new file mode 100644
index 0000000..67bf752
--- /dev/null
+++ b/xos/synchronizer/steps/sync_trustdomain.py
@@ -0,0 +1,70 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from synchronizers.new_base.ansible_helper import *
+from synchronizers.new_base.modelaccessor import TrustDomain
+from newopenstacksyncstep import NewOpenStackSyncStep
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class SyncTrustDomain(NewOpenStackSyncStep):
+    provides=[TrustDomain]
+    requested_interval=0
+    observes=TrustDomain
+
+    def fetch_pending(self, deleted):
+        """ Figure out which TrustDomains are interesting to the OpenStack synchronizer. It's necessary to filter as
+            we're synchronizing a core model, and we only want to synchronize trust domains that will exist within
+            OpenStack.
+        """
+        objs = super(SyncTrustDomain, self).fetch_pending(deleted)
+        for obj in objs[:]:
+            # If the TrustDomain isn't part of the OpenStack service, then it's someone else's trust domain
+            if "OpenStackService" not in obj.owner.leaf_model.class_names:
+                objs.remove(obj)
+        return objs
+
+    def sync_record(self, trust_domain):
+        service = trust_domain.owner.leaf_model
+        conn = self.connect_openstack_admin(service)
+
+        os_domain = conn.identity.find_domain(trust_domain.name)
+        if (os_domain):
+            log.info("Trust Domain already exists in openstack", trust_domain=trust_domain)
+        else:
+            log.info("Creating Trust Domain", trust_domain=trust_domain)
+            os_domain = conn.identity.create_domain(name=trust_domain.name)
+
+        if os_domain.id != trust_domain.backend_handle:
+            trust_domain.backend_handle = os_domain.id
+            trust_domain.save(update_fields=["backend_handle"])
+
+    def delete_record(self, trust_domain):
+        service = trust_domain.owner.leaf_model
+        conn = self.connect_openstack_admin(service)
+
+        os_domain = conn.identity.find_domain(trust_domain.name)
+        if (not os_domain):
+            log.info("Trust Domain already does not exist in openstack", trust_domain=trust_domain)
+        else:
+            if os_domain.is_enabled:
+                log.info("Disabling Trust Domain", trust_domain=trust_domain, os_id=os_domain.id)
+                os_domain=conn.identity.update_domain(os_domain.id, enabled=False)
+            log.info("Deleting Trust Domain", trust_domain=trust_domain, os_id=os_domain.id)
+            conn.identity.delete_domain(os_domain.id)
diff --git a/xos/synchronizer/tests/test_config.yaml b/xos/synchronizer/tests/test_config.yaml
new file mode 100644
index 0000000..acd2ba7
--- /dev/null
+++ b/xos/synchronizer/tests/test_config.yaml
@@ -0,0 +1,29 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+name: test-model-policies
+accessor:
+  username: xosadmin@opencord.org
+  password: "sample"
+  kind: "testframework"
+logging:
+  version: 1
+  handlers:
+    console:
+      class: logging.StreamHandler
+  loggers:
+    'multistructlog':
+      handlers:
+          - console
diff --git a/xos/synchronizer/tests/test_pull_ports.py b/xos/synchronizer/tests/test_pull_ports.py
new file mode 100644
index 0000000..7bfe160
--- /dev/null
+++ b/xos/synchronizer/tests/test_pull_ports.py
@@ -0,0 +1,90 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import sys
+import unittest
+from mock import patch, PropertyMock, ANY, MagicMock
+from unit_test_common import setup_sync_unit_test
+
+def fake_connect_openstack_admin(self, service, required_version=None):
+    return MagicMock()
+
+class TestPullPorts(unittest.TestCase):
+
+    def setUp(self):
+        self.unittest_setup = setup_sync_unit_test(os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
+                                                   globals(),
+                                                   [("openstack", "openstack.xproto")] )
+
+        sys.path.append(os.path.join(os.path.abspath(os.path.dirname(os.path.realpath(__file__))), "../pull_steps"))
+
+        from pull_ports import OpenStackPortPullStep
+        self.step_class = OpenStackPortPullStep
+
+        self.service = OpenStackService()
+        self.site = Site(name="test-site")
+        self.trust_domain = TrustDomain(owner=self.service, name="test-trust")
+        self.flavor = Flavor(name="test-flavor", backend_handle=1114)
+        self.node = Node(name="test-node", backend_handle=1113)
+        self.slice = Slice(name="test-slice", trust_domain=self.trust_domain, backend_handle=1112)
+        self.image = Image(name="test-image", backend_handle=1111)
+        self.net_management = Network(name="management", backend_handle=1115)
+
+    def tearDown(self):
+        sys.path = self.unittest_setup["sys_path_save"]
+
+    def test_pull_records(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin, \
+             patch.object(Port.objects, "get_items") as port_objects, \
+             patch.object(Network.objects, "get_items") as network_objects, \
+             patch.object(OpenStackServiceInstance.objects, "get_items") as ossi_objects, \
+             patch.object(OpenStackService.objects, "get_items") as osi_objects, \
+             patch.object(Port, "save", autospec=True) as port_save:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            xos_instance = OpenStackServiceInstance(name="test-instance", slice=self.slice, image=self.image,
+                                                    node=self.node, flavor=self.flavor, backend_handle=2112)
+
+            port_objects.return_value = []
+            network_objects.return_value = [self.net_management]
+            ossi_objects.return_value = [xos_instance]
+            osi_objects.return_value = [self.service]
+
+            cn = ControllerNetwork(net_id=self.net_management.backend_handle)
+            self.net_management.controllernetworks = self.unittest_setup["MockObjectList"]([cn])
+
+            fakeconn.network.ports.return_value = [MagicMock(id=2111, device_id=xos_instance.backend_handle,
+                                                             network_id=self.net_management.backend_handle,
+                                                             fixed_ips=[{"ip_address": "1.2.3.4"}],
+                                                             mac_address="11:22:33:44:55:66")]
+
+            step = self.step_class()
+            step.pull_records()
+
+            self.assertEqual(port_save.call_count, 1)
+            saved_port = port_save.call_args[0][0]
+
+            self.assertEqual(saved_port.network, self.net_management)
+            self.assertEqual(saved_port.ip, "1.2.3.4")
+            self.assertEqual(saved_port.mac, "11:22:33:44:55:66")
+            self.assertEqual(saved_port.port_id, 2111)
+
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/xos/synchronizer/tests/test_sync_openstackserviceinstance.py b/xos/synchronizer/tests/test_sync_openstackserviceinstance.py
new file mode 100644
index 0000000..c39bd0b
--- /dev/null
+++ b/xos/synchronizer/tests/test_sync_openstackserviceinstance.py
@@ -0,0 +1,120 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import sys
+import unittest
+from mock import patch, PropertyMock, ANY, MagicMock
+from unit_test_common import setup_sync_unit_test
+
+def fake_connect_openstack_admin(self, service, required_version=None):
+    return MagicMock()
+
+class TestSyncOpenStackServiceInstance(unittest.TestCase):
+
+    def setUp(self):
+        self.unittest_setup = setup_sync_unit_test(os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
+                                                   globals(),
+                                                   [("openstack", "openstack.xproto")] )
+
+        sys.path.append(os.path.join(os.path.abspath(os.path.dirname(os.path.realpath(__file__))), "../steps"))
+
+        from sync_openstackserviceinstance import SyncOpenStackServiceInstance
+        self.step_class = SyncOpenStackServiceInstance
+
+        self.service = OpenStackService()
+        self.site = Site(name="test-site")
+        self.trust_domain = TrustDomain(owner=self.service, name="test-trust")
+        self.flavor = Flavor(name="test-flavor", backend_handle=1114)
+        self.node = Node(name="test-node", backend_handle=1113)
+        self.slice = Slice(name="test-slice", trust_domain=self.trust_domain, backend_handle=1112)
+        self.image = Image(name="test-image", backend_handle=1111)
+
+    def tearDown(self):
+        sys.path = self.unittest_setup["sys_path_save"]
+
+    def test_sync_record_create_noexist(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            xos_instance = OpenStackServiceInstance(name="test-instance", slice=self.slice, image=self.image,
+                                                    node=self.node, flavor=self.flavor)
+
+            step = self.step_class()
+            fakeconn.compute.servers.return_value = []
+            fakeconn.identity.find_project.return_value = MagicMock(id=self.slice.backend_handle)
+            fakeconn.identity.find_domain.return_value = MagicMock(id=self.trust_domain.backend_handle)
+            fakeconn.compute.find_image.return_value = MagicMock(id=self.image.backend_handle)
+            fakeconn.compute.find_flavor.return_value = MagicMock(id=self.flavor.backend_handle)
+
+            os_instance = MagicMock()
+            os_instance.id = "1234"
+            fakeconn.compute.create_server.return_value = os_instance
+
+            step.sync_record(xos_instance)
+
+            fakeconn.compute.create_server.assert_called_with(admin_password=ANY,
+                                                              availability_zone="nova:test-node",
+                                                              config_drive=True,
+                                                              flavor_id=self.flavor.backend_handle,
+                                                              image_id=self.image.backend_handle,
+                                                              name=xos_instance.name,
+                                                              networks=[],
+                                                              project_domain_id=self.slice.backend_handle,
+                                                              user_data=ANY)
+            self.assertEqual(xos_instance.backend_handle, "1234")
+
+    def test_sync_record_create_exists(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            xos_instance = OpenStackServiceInstance(name="test-instance", slice=self.slice, image=self.image,
+                                                    node=self.node, flavor=self.flavor)
+
+            os_instance = MagicMock()
+            os_instance.id = "1234"
+
+            step = self.step_class()
+            fakeconn.identity.find_project.return_value = os_instance
+            fakeconn.compute.create_server.return_value = None
+            fakeconn.compute.servers.return_value = [os_instance]
+
+            step.sync_record(xos_instance)
+
+            fakeconn.compute.create_server.assert_not_called()
+            self.assertEqual(xos_instance.backend_handle, "1234")
+
+    def test_delete_record(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            xos_instance = OpenStackServiceInstance(name="test-instance", slice=self.slice, image=self.image,
+                                                    node=self.node, flavor=self.flavor)
+
+            step = self.step_class()
+            os_instance = MagicMock()
+            os_instance.id = "1234"
+            fakeconn.compute.servers.return_value = [os_instance]
+            fakeconn.compute.delete_server.return_value = None
+
+            step.delete_record(xos_instance)
+            fakeconn.compute.delete_server.assert_called_with("1234")
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/xos/synchronizer/tests/test_sync_principal.py b/xos/synchronizer/tests/test_sync_principal.py
new file mode 100644
index 0000000..f06ec48
--- /dev/null
+++ b/xos/synchronizer/tests/test_sync_principal.py
@@ -0,0 +1,102 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import sys
+import unittest
+from mock import patch, PropertyMock, ANY, MagicMock
+from unit_test_common import setup_sync_unit_test
+
+def fake_connect_openstack_admin(self, service, required_version=None):
+    return MagicMock()
+
+class TestSyncPrincipal(unittest.TestCase):
+
+    def setUp(self):
+        self.unittest_setup = setup_sync_unit_test(os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
+                                                   globals(),
+                                                   [("openstack", "openstack.xproto")] )
+
+        sys.path.append(os.path.join(os.path.abspath(os.path.dirname(os.path.realpath(__file__))), "../steps"))
+
+        from sync_principal import SyncPrincipal
+        self.step_class = SyncPrincipal
+
+        self.service = OpenStackService()
+        self.trust_domain = TrustDomain(owner=self.service, name="test-trust")
+
+    def tearDown(self):
+        sys.path = self.unittest_setup["sys_path_save"]
+
+    def test_sync_record_create_noexist(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            trust_domain_id = 5678
+
+            xos_principal = Principal(name="test-principal", trust_domain=self.trust_domain)
+
+            step = self.step_class()
+            fakeconn.identity.find_user.return_value = None
+            fakeconn.identity.find_domain.return_value = MagicMock(id=trust_domain_id)
+
+            os_user = MagicMock()
+            os_user.id = "1234"
+            fakeconn.identity.create_user.return_value = os_user
+
+            step.sync_record(xos_principal)
+
+            fakeconn.identity.create_user.assert_called_with(name=xos_principal.name, domain_id=trust_domain_id)
+            self.assertEqual(xos_principal.backend_handle, "1234")
+
+    def test_sync_record_create_exists(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            xos_principal = Principal(name="test-principal", trust_domain=self.trust_domain)
+
+            os_user = MagicMock()
+            os_user.id = "1234"
+
+            step = self.step_class()
+            fakeconn.identity.find_user.return_value = os_user
+            fakeconn.identity.create_user.return_value = None
+
+            step.sync_record(xos_principal)
+
+            fakeconn.identity.create_user.assert_not_called()
+            self.assertEqual(xos_principal.backend_handle, "1234")
+
+    def test_delete_record(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            xos_principal = Principal(name="test-principal", trust_domain=self.trust_domain)
+
+            step = self.step_class()
+            os_user = MagicMock()
+            os_user.id = "1234"
+            fakeconn.identity.find_user.return_value = os_user
+            fakeconn.identity.delete_user.return_value = None
+
+            step.delete_record(xos_principal)
+            fakeconn.identity.delete_user.assert_called_with("1234")
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/xos/synchronizer/tests/test_sync_slice.py b/xos/synchronizer/tests/test_sync_slice.py
new file mode 100644
index 0000000..384641f
--- /dev/null
+++ b/xos/synchronizer/tests/test_sync_slice.py
@@ -0,0 +1,103 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import sys
+import unittest
+from mock import patch, PropertyMock, ANY, MagicMock
+from unit_test_common import setup_sync_unit_test
+
+def fake_connect_openstack_admin(self, service, required_version=None):
+    return MagicMock()
+
+class TestSyncSlice(unittest.TestCase):
+
+    def setUp(self):
+        self.unittest_setup = setup_sync_unit_test(os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
+                                                   globals(),
+                                                   [("openstack", "openstack.xproto")] )
+
+        sys.path.append(os.path.join(os.path.abspath(os.path.dirname(os.path.realpath(__file__))), "../steps"))
+
+        from sync_slice import SyncSlice
+        self.step_class = SyncSlice
+
+        self.service = OpenStackService()
+        self.site = Site(name="test-site")
+        self.trust_domain = TrustDomain(owner=self.service, name="test-trust")
+
+    def tearDown(self):
+        sys.path = self.unittest_setup["sys_path_save"]
+
+    def test_sync_record_create_noexist(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            trust_domain_id = 5678
+
+            xos_slice = Slice(name="test-slice", trust_domain=self.trust_domain, site=self.site)
+
+            step = self.step_class()
+            fakeconn.identity.find_project.return_value = None
+            fakeconn.identity.find_domain.return_value = MagicMock(id=trust_domain_id)
+
+            os_slice = MagicMock()
+            os_slice.id = "1234"
+            fakeconn.identity.create_project.return_value = os_slice
+
+            step.sync_record(xos_slice)
+
+            fakeconn.identity.create_project.assert_called_with(name=xos_slice.name, domain_id=trust_domain_id)
+            self.assertEqual(xos_slice.backend_handle, "1234")
+
+    def test_sync_record_create_exists(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            xos_slice = Slice(name="test-slice", trust_domain=self.trust_domain, site=self.site)
+
+            os_slice = MagicMock()
+            os_slice.id = "1234"
+
+            step = self.step_class()
+            fakeconn.identity.find_project.return_value = os_slice
+            fakeconn.identity.create_user.return_value = None
+
+            step.sync_record(xos_slice)
+
+            fakeconn.identity.create_slice.assert_not_called()
+            self.assertEqual(xos_slice.backend_handle, "1234")
+
+    def test_delete_record(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            xos_slice = Slice(name="test-slice", trust_domain=self.trust_domain, site=self.site)
+
+            step = self.step_class()
+            os_slice = MagicMock()
+            os_slice.id = "1234"
+            fakeconn.identity.find_project.return_value = os_slice
+            fakeconn.identity.delete_project.return_value = None
+
+            step.delete_record(xos_slice)
+            fakeconn.identity.delete_project.assert_called_with("1234")
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/xos/synchronizer/tests/test_sync_trustdomain.py b/xos/synchronizer/tests/test_sync_trustdomain.py
new file mode 100644
index 0000000..b9f8a74
--- /dev/null
+++ b/xos/synchronizer/tests/test_sync_trustdomain.py
@@ -0,0 +1,104 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import sys
+import unittest
+from mock import patch, PropertyMock, ANY, MagicMock
+from unit_test_common import setup_sync_unit_test
+
+def fake_connect_openstack_admin(self, service, required_version=None):
+    return MagicMock()
+
+class TestSyncTrustDomain(unittest.TestCase):
+
+    def setUp(self):
+        self.unittest_setup = setup_sync_unit_test(os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
+                                                   globals(),
+                                                   [("openstack", "openstack.xproto")] )
+
+        sys.path.append(os.path.join(os.path.abspath(os.path.dirname(os.path.realpath(__file__))), "../steps"))
+
+        from sync_trustdomain import SyncTrustDomain
+        self.step_class = SyncTrustDomain
+
+        self.service = OpenStackService()
+
+    def tearDown(self):
+        sys.path = self.unittest_setup["sys_path_save"]
+
+    def test_sync_record_create_noexist(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            xos_trust_domain = TrustDomain(name="test-trust", owner=self.service)
+
+            step = self.step_class()
+            fakeconn.identity.find_domain.return_value = None
+
+            os_domain = MagicMock()
+            os_domain.id = "1234"
+            fakeconn.identity.create_domain.return_value = os_domain
+
+            step.sync_record(xos_trust_domain)
+
+            fakeconn.identity.create_domain.assert_called_with(name=xos_trust_domain.name)
+            self.assertEqual(xos_trust_domain.backend_handle, "1234")
+
+    def test_sync_record_create_exists(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            xos_trust_domain = TrustDomain(name="test-trust", owner=self.service)
+
+            step = self.step_class()
+            os_domain = MagicMock()
+            os_domain.id = "1234"
+            fakeconn.identity.find_domain.return_value = os_domain
+
+            fakeconn.identity.create_domain.return_value = None
+
+            step.sync_record(xos_trust_domain)
+
+            fakeconn.identity.create_domain.assert_not_called()
+            self.assertEqual(xos_trust_domain.backend_handle, "1234")
+
+    def test_delete_record(self):
+        fakeconn = MagicMock()
+        with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+            fake_connect_openstack_admin.return_value = fakeconn
+
+            xos_trust_domain = TrustDomain(name="test-trust", owner=self.service)
+
+            step = self.step_class()
+            os_domain = MagicMock()
+            os_domain.id = "1234"
+            os_domain.enabled = True
+            fakeconn.identity.find_domain.return_value = os_domain
+            os_domain2 = MagicMock()
+            os_domain2.id = "1234"
+            os_domain2.enabled = False
+            fakeconn.identity.update_domain.return_value = os_domain2
+            fakeconn.identity.delete_domain.return_value = None
+
+            step.delete_record(xos_trust_domain)
+            fakeconn.identity.update_domain.assert_called_with("1234", enabled=False)
+            fakeconn.identity.delete_domain.assert_called_with("1234")
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/xos/synchronizer/tests/unit_test_common.py b/xos/synchronizer/tests/unit_test_common.py
new file mode 100644
index 0000000..68f6743
--- /dev/null
+++ b/xos/synchronizer/tests/unit_test_common.py
@@ -0,0 +1,84 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+
+def setup_sync_unit_test(test_path, globals_dict, models, config_fn="test_config.yaml"):
+    """ Perform the common steps associated with setting up a synchronizer unit test.
+           1) Add synchronizers/new_base to sys.path
+           2) Import xosconfig.Config and set it up to test_config.yaml in the current dir
+           3) Build the mock modelaccessor and import it
+           4) Import all model accessor classes into global space
+
+        Arguments:
+            test_path - path to the test case that is being run
+            globals_dict - a dictionary to add global models to
+            models - a list of pairs (service_name, xproto_name,
+            config_fn - filename of config file)
+
+        Returns:
+            Dictionary containing the following:
+                sys_path_save: the original sys.path
+                model_accessor: model accessor class
+                Config: the Config object
+                xos_dir: xos directory
+                services_dir: services directory
+    """
+    def get_models_fn(services_dir, 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))
+
+    sys_path_save = sys.path
+
+    xos_dir = os.path.join(test_path, "../../..")
+    if not os.path.exists(os.path.join(test_path, "new_base")):
+        xos_dir = os.path.join(test_path, "../../../../../../orchestration/xos/xos")
+        services_dir = os.path.join(xos_dir, "../../xos_services")
+    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, config_fn)
+    Config.clear()
+    Config.init(config, "synchronizer-config-schema.yaml")
+
+    xprotos = []
+    for (service_name, xproto_name) in models:
+        xprotos.append(get_models_fn(services_dir, service_name, xproto_name))
+
+    from synchronizers.new_base.mock_modelaccessor_build import build_mock_modelaccessor
+    build_mock_modelaccessor(xos_dir, services_dir, xprotos)
+    import synchronizers.new_base.modelaccessor
+    from synchronizers.new_base.modelaccessor import model_accessor
+    from mock_modelaccessor import MockObjectList
+
+    # import all class names to globals
+    for (k, v) in model_accessor.all_model_classes.items():
+        globals_dict[k] = v
+
+    return {"sys_path_save": sys_path_save,
+            "model_accessor": model_accessor,
+            "Config": Config,
+            "xos_dir": xos_dir,
+            "services_dir": services_dir,
+            "MockObjectList": MockObjectList}
diff --git a/xos/unittest.cfg b/xos/unittest.cfg
new file mode 100644
index 0000000..44c6ea4
--- /dev/null
+++ b/xos/unittest.cfg
@@ -0,0 +1,6 @@
+[unittest]
+plugins=nose2-plugins.exclude
+code-directories=synchronizer
+                 model_policies
+                 steps
+                 pull_steps