SEBA-557 implement xos test service

Change-Id: I21ab4c9139a3be7e68cfff8ef506100f8fe3a2c3
diff --git a/VERSION b/VERSION
index eca690e..448fac8 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.0.5
+3.0.6-dev
diff --git a/testservice/Dockerfile.synchronizer b/testservice/Dockerfile.synchronizer
new file mode 100644
index 0000000..f3a2d69
--- /dev/null
+++ b/testservice/Dockerfile.synchronizer
@@ -0,0 +1,58 @@
+# Copyright 2018-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.
+
+# docker build -t smbaker/testservice-synchronizer:test -f Dockerfile.synchronizer .
+
+# xosproject/testservice
+
+FROM xosproject/xos-synchronizer-base:candidate
+
+COPY xos/synchronizer /opt/xos/synchronizers/testservice
+COPY VERSION /opt/xos/synchronizers/testservice/
+
+ENTRYPOINT []
+
+WORKDIR "/opt/xos/synchronizers/testservice"
+
+# Label image
+ARG org_label_schema_schema_version=1.0
+ARG org_label_schema_name=testservice
+ARG org_label_schema_version=unknown
+ARG org_label_schema_vcs_url=unknown
+ARG org_label_schema_vcs_ref=unknown
+ARG org_label_schema_build_date=unknown
+ARG org_opencord_vcs_commit_date=unknown
+ARG org_opencord_component_chameleon_version=unknown
+ARG org_opencord_component_chameleon_vcs_url=unknown
+ARG org_opencord_component_chameleon_vcs_ref=unknown
+ARG org_opencord_component_xos_version=unknown
+ARG org_opencord_component_xos_vcs_url=unknown
+ARG org_opencord_component_xos_vcs_ref=unknown
+
+LABEL org.label-schema.schema-version=$org_label_schema_schema_version \
+      org.label-schema.name=$org_label_schema_name \
+      org.label-schema.version=$org_label_schema_version \
+      org.label-schema.vcs-url=$org_label_schema_vcs_url \
+      org.label-schema.vcs-ref=$org_label_schema_vcs_ref \
+      org.label-schema.build-date=$org_label_schema_build_date \
+      org.opencord.vcs-commit-date=$org_opencord_vcs_commit_date \
+      org.opencord.component.chameleon.version=$org_opencord_component_chameleon_version \
+      org.opencord.component.chameleon.vcs-url=$org_opencord_component_chameleon_vcs_url \
+      org.opencord.component.chameleon.vcs-ref=$org_opencord_component_chameleon_vcs_ref \
+      org.opencord.component.xos.version=$org_opencord_component_xos_version \
+      org.opencord.component.xos.vcs-url=$org_opencord_component_xos_vcs_url \
+      org.opencord.component.xos.vcs-ref=$org_opencord_component_xos_vcs_ref
+
+#CMD ["/usr/bin/python", "/opt/xos/synchronizers/testservice/testservice-synchronizer.py"]
+CMD ["/bin/bash", "-c", "echo test && python /opt/xos/synchronizers/testservice/testservice-synchronizer.py"]
diff --git a/testservice/README.md b/testservice/README.md
new file mode 100644
index 0000000..c74e09a
--- /dev/null
+++ b/testservice/README.md
@@ -0,0 +1 @@
+# Testservice Service
diff --git a/testservice/VERSION b/testservice/VERSION
new file mode 100644
index 0000000..8acdd82
--- /dev/null
+++ b/testservice/VERSION
@@ -0,0 +1 @@
+0.0.1
diff --git a/testservice/helm-charts/testservice-devel.yaml b/testservice/helm-charts/testservice-devel.yaml
new file mode 100644
index 0000000..3f487a9
--- /dev/null
+++ b/testservice/helm-charts/testservice-devel.yaml
@@ -0,0 +1,16 @@
+# 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.
+
+imagePullPolicy: 'IfNotPresent'
+testservice_synchronizerImage: 'xosproject/testservice-synchronizer:candidate'
diff --git a/testservice/helm-charts/testservice/Chart.yaml b/testservice/helm-charts/testservice/Chart.yaml
new file mode 100644
index 0000000..3076353
--- /dev/null
+++ b/testservice/helm-charts/testservice/Chart.yaml
@@ -0,0 +1,17 @@
+---
+# Copyright 2018-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: testservice
+version: 0.0.1
diff --git a/testservice/helm-charts/testservice/templates/_helpers.tpl b/testservice/helm-charts/testservice/templates/_helpers.tpl
new file mode 100644
index 0000000..3e260de
--- /dev/null
+++ b/testservice/helm-charts/testservice/templates/_helpers.tpl
@@ -0,0 +1,71 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Copyright 2018-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.
+*/}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "testservice.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "testservice.fullname" -}}
+{{- if .Values.fullnameOverride -}}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- if contains $name .Release.Name -}}
+{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "testservice.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{- define "testservice.serviceConfig" -}}
+name: testservice
+accessor:
+  username: {{ .Values.xosAdminUser | quote }}
+  password: {{ .Values.xosAdminPassword | quote }}
+  endpoint: xos-core:50051
+logging:
+  version: 1
+  handlers:
+    console:
+      class: logging.StreamHandler
+    file:
+      class: logging.handlers.RotatingFileHandler
+      filename: /var/log/xos.log
+      maxBytes: 10485760
+      backupCount: 5
+  loggers:
+    'multistructlog':
+      handlers:
+          - console
+          - file
+      level: DEBUG
+{{- end -}}
diff --git a/testservice/helm-charts/testservice/templates/_tosca.tpl b/testservice/helm-charts/testservice/templates/_tosca.tpl
new file mode 100644
index 0000000..a33348b
--- /dev/null
+++ b/testservice/helm-charts/testservice/templates/_tosca.tpl
@@ -0,0 +1,30 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Copyright 2018-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.
+*/}}
+{{- define "testservice.serviceTosca" -}}
+tosca_definitions_version: tosca_simple_yaml_1_0
+description: Set up testservice service
+imports:
+  - custom_types/testserviceservice.yaml
+
+topology_template:
+  node_templates:
+    service#testservice:
+      type: tosca.nodes.TestserviceService
+      properties:
+        name: testservice
+        kind: Testservice
+{{- end -}}
diff --git a/testservice/helm-charts/testservice/templates/configmap.yaml b/testservice/helm-charts/testservice/templates/configmap.yaml
new file mode 100644
index 0000000..26cca16
--- /dev/null
+++ b/testservice/helm-charts/testservice/templates/configmap.yaml
@@ -0,0 +1,22 @@
+---
+# Copyright 2018-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.
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: testservice
+data:
+  serviceConfig: |
+{{ include "testservice.serviceConfig" . | indent 4 }}
diff --git a/testservice/helm-charts/testservice/templates/deployment.yaml b/testservice/helm-charts/testservice/templates/deployment.yaml
new file mode 100644
index 0000000..ad82915
--- /dev/null
+++ b/testservice/helm-charts/testservice/templates/deployment.yaml
@@ -0,0 +1,76 @@
+---
+# Copyright 2018-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.
+
+apiVersion: apps/v1beta2
+kind: Deployment
+metadata:
+  name: {{ template "testservice.fullname" . }}
+  labels:
+    app: {{ template "testservice.name" . }}
+    chart: {{ template "testservice.chart" . }}
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      app: {{ template "testservice.name" . }}
+      release: {{ .Release.Name }}
+  template:
+    metadata:
+      labels:
+        app: {{ template "testservice.name" . }}
+        release: {{ .Release.Name }}
+      annotations:
+        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
+    spec:
+      containers:
+        - name: {{ .Chart.Name }}
+          image: {{ tpl .Values.testservice_synchronizerImage . | quote }}
+          imagePullPolicy: {{ .Values.imagePullPolicy }}
+          resources:
+{{ toYaml .Values.resources | indent 12 }}
+          volumeMounts:
+            - name: testservice-config
+              mountPath: /opt/xos/synchronizers/testservice/mounted_config.yaml
+              subPath: mounted_config.yaml
+            - name: certchain-volume
+              mountPath: /usr/local/share/ca-certificates/local_certs.crt
+              subPath: config/ca_cert_chain.pem
+      volumes:
+        - name: testservice-config
+          configMap:
+            name: testservice
+            items:
+              - key: serviceConfig
+                path: mounted_config.yaml
+        - name: certchain-volume
+          configMap:
+            name: ca-certificates
+            items:
+              - key: chain
+                path: config/ca_cert_chain.pem
+    {{- with .Values.nodeSelector }}
+      nodeSelector:
+{{ toYaml . | indent 8 }}
+    {{- end }}
+    {{- with .Values.affinity }}
+      affinity:
+{{ toYaml . | indent 8 }}
+    {{- end }}
+    {{- with .Values.tolerations }}
+      tolerations:
+{{ toYaml . | indent 8 }}
+    {{- end }}
diff --git a/testservice/helm-charts/testservice/values.yaml b/testservice/helm-charts/testservice/values.yaml
new file mode 100644
index 0000000..567f213
--- /dev/null
+++ b/testservice/helm-charts/testservice/values.yaml
@@ -0,0 +1,30 @@
+---
+# Copyright 2018-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.
+
+nameOverride: ""
+fullnameOverride: ""
+
+testservice_synchronizerImage: "xosproject/testservice-synchronizer:{{ .Chart.Version }}"
+
+imagePullPolicy: 'IfNotPresent'
+
+xosAdminUser: "admin@opencord.org"
+xosAdminPassword: "letmein"
+
+affinity: {}
+nodeSelector: {}
+replicaCount: 1
+resources: {}
+tolerations: []
diff --git a/testservice/scripts/cleanup.sh b/testservice/scripts/cleanup.sh
new file mode 100755
index 0000000..ad39431
--- /dev/null
+++ b/testservice/scripts/cleanup.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# 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.
+
+TOSCA_URL=http://`hostname`:30007
+RECIPE=../tosca/test_no_constraints.yaml
+curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X POST --data-binary @$RECIPE $TOSCA_URL/delete
diff --git a/testservice/scripts/cycle.sh b/testservice/scripts/cycle.sh
new file mode 100755
index 0000000..7426b42
--- /dev/null
+++ b/testservice/scripts/cycle.sh
@@ -0,0 +1,21 @@
+#! /bin/bash
+
+# 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.
+
+pushd ~/cord/orchestration/xos/testservice/scripts
+helm del --purge testservice
+./rebuild.sh
+./deploy.sh
+popd
diff --git a/testservice/scripts/deploy.sh b/testservice/scripts/deploy.sh
new file mode 100755
index 0000000..7909a3c
--- /dev/null
+++ b/testservice/scripts/deploy.sh
@@ -0,0 +1,20 @@
+#! /bin/bash
+
+# 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.
+
+pushd ~/cord/orchestration/xos/testservice/helm-charts
+#helm dep update testservice
+helm install testservice -n testservice -f testservice-devel.yaml
+popd
diff --git a/testservice/scripts/rebuild.sh b/testservice/scripts/rebuild.sh
new file mode 100755
index 0000000..6f6d38b
--- /dev/null
+++ b/testservice/scripts/rebuild.sh
@@ -0,0 +1,19 @@
+#! /bin/bash
+
+# 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.
+
+pushd ~/cord/orchestration/xos/testservice
+sudo docker build -t xosproject/testservice-synchronizer:candidate -f Dockerfile.synchronizer .
+popd
diff --git a/testservice/scripts/test_no_constraints.sh b/testservice/scripts/test_no_constraints.sh
new file mode 100755
index 0000000..dd58a85
--- /dev/null
+++ b/testservice/scripts/test_no_constraints.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# 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.
+
+set -e
+
+CHAMELEON_URL=http://`hostname`:30006/xosapi/v1/testservice/testserviceserviceinstances
+TOSCA_URL=http://`hostname`:30007
+TEST_POD=`kubectl get pods | grep -i testservice | cut -f 1 -d " "`
+RECIPE=../tosca/test_no_constraints.yaml
+
+curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X post --data-binary @$RECIPE $TOSCA_URL/run
+
+echo "done tosca"
+
+cat <<EOF > wait_for.txt
+TEST:SYNC_DONE                 id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-no-constraints' some_integer=0 some_other_integer=0
+TEST:POLICY_DONE               id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-no-constraints' some_integer=0 some_other_integer=0
+EOF
+kubectl logs -f --since=30s $TEST_POD | ansi2txt | python wait_for_lines.py wait_for.txt
+
+python ./verify_model.py $CHAMELEON_URL name=test-no-constraints "updated>=0" "enacted>=@updated" "policed>=@updated"
diff --git a/testservice/scripts/test_policy_after_sync.sh b/testservice/scripts/test_policy_after_sync.sh
new file mode 100755
index 0000000..090b373
--- /dev/null
+++ b/testservice/scripts/test_policy_after_sync.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# 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.
+
+set -e
+
+CHAMELEON_URL=http://`hostname`:30006/xosapi/v1/testservice/testserviceserviceinstances
+TOSCA_URL=http://`hostname`:30007
+TEST_POD=`kubectl get pods | grep -i testservice | cut -f 1 -d " "`
+RECIPE=../tosca/test_policy_after_sync.yaml
+
+curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X post --data-binary @$RECIPE $TOSCA_URL/run
+
+echo "done tosca"
+
+cat <<EOF > wait_for.txt
+TEST:SYNC_DONE                 id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-policy-after-sync' some_integer=0 some_other_integer=0
+TEST:POLICY_DONE               id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-policy-after-sync' some_integer=0 some_other_integer=0
+EOF
+kubectl logs -f --since=30s $TEST_POD | ansi2txt | python wait_for_lines.py wait_for.txt
+
+python ./verify_model.py $CHAMELEON_URL name=test-policy-after-sync "updated>=0" "enacted>=@updated" "policed>=@updated"
diff --git a/testservice/scripts/test_policy_after_sync_update.sh b/testservice/scripts/test_policy_after_sync_update.sh
new file mode 100755
index 0000000..b3cbbfa
--- /dev/null
+++ b/testservice/scripts/test_policy_after_sync_update.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+# 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.
+
+set -e
+
+CHAMELEON_URL=http://`hostname`:30006/xosapi/v1/testservice/testserviceserviceinstances
+TOSCA_URL=http://`hostname`:30007
+TEST_POD=`kubectl get pods | grep -i testservice | cut -f 1 -d " "`
+RECIPE=../tosca/test_policy_after_sync_update.yaml
+
+curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X post --data-binary @$RECIPE $TOSCA_URL/run
+
+echo "done tosca"
+
+cat <<EOF > wait_for.txt
+TEST:SYNC_DONE                 id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-policy-after-sync-update' some_integer=0 some_other_integer=0
+TEST:SYNC_DONE                 id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-policy-after-sync-update' some_integer=0 some_other_integer=1
+TEST:POLICY_DONE               id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-policy-after-sync-update' some_integer=0 some_other_integer=1
+EOF
+kubectl logs -f --since=30s $TEST_POD | ansi2txt | python wait_for_lines.py wait_for.txt
+
+python ./verify_model.py $CHAMELEON_URL name=test-policy-after-sync-update "updated>=0" "policed>=@updated" "enacted>=@policed" "some_other_integer=1"
diff --git a/testservice/scripts/test_sync_after_policy.sh b/testservice/scripts/test_sync_after_policy.sh
new file mode 100755
index 0000000..2daf7ef
--- /dev/null
+++ b/testservice/scripts/test_sync_after_policy.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# 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.
+
+set -e
+
+CHAMELEON_URL=http://`hostname`:30006/xosapi/v1/testservice/testserviceserviceinstances
+TOSCA_URL=http://`hostname`:30007
+TEST_POD=`kubectl get pods | grep -i testservice | cut -f 1 -d " "`
+RECIPE=../tosca/test_sync_after_policy.yaml
+
+curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X post --data-binary @$RECIPE $TOSCA_URL/run
+
+echo "done tosca"
+
+cat <<EOF > wait_for.txt
+TEST:SYNC_DONE                 id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-sync-after-policy' some_integer=0 some_other_integer=0
+TEST:POLICY_DONE               id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-sync-after-policy' some_integer=0 some_other_integer=0
+EOF
+kubectl logs -f --since=30s $TEST_POD | ansi2txt | python wait_for_lines.py wait_for.txt
+
+python ./verify_model.py $CHAMELEON_URL name=test-sync-after-policy "updated>=0" "enacted>=@updated" "policed>=@updated"
diff --git a/testservice/scripts/test_sync_after_policy_update.sh b/testservice/scripts/test_sync_after_policy_update.sh
new file mode 100755
index 0000000..e7f2112
--- /dev/null
+++ b/testservice/scripts/test_sync_after_policy_update.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+# 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.
+
+set -e
+
+CHAMELEON_URL=http://`hostname`:30006/xosapi/v1/testservice/testserviceserviceinstances
+TOSCA_URL=http://`hostname`:30007
+TEST_POD=`kubectl get pods | grep -i testservice | cut -f 1 -d " "`
+RECIPE=../tosca/test_sync_after_policy_update.yaml
+
+curl -H "xos-username: admin@opencord.org" -H "xos-password: letmein" -X post --data-binary @$RECIPE $TOSCA_URL/run
+
+echo "done tosca"
+
+cat <<EOF > wait_for.txt
+TEST:SYNC_DONE                 id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-sync-after-policy-update' some_integer=1 some_other_integer=0
+TEST:POLICY_DONE               id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-sync-after-policy-update' some_integer=0 some_other_integer=0
+TEST:POLICY_DONE               id=[0-9]+ model_class=TestserviceServiceInstance model_name=u'test-sync-after-policy-update' some_integer=1 some_other_integer=0
+EOF
+kubectl logs -f --since=30s $TEST_POD | ansi2txt | python wait_for_lines.py wait_for.txt
+
+python ./verify_model.py $CHAMELEON_URL name=test-sync-after-policy-update "updated>=0" "enacted>=@updated" "policed>=@enacted" "some_integer=1"
diff --git a/testservice/scripts/verify_model.py b/testservice/scripts/verify_model.py
new file mode 100755
index 0000000..a895940
--- /dev/null
+++ b/testservice/scripts/verify_model.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+
+# 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 __future__ import absolute_import, print_function
+
+import requests
+import sys
+
+
+def matches(test, item):
+    if ">=" in test:
+        delim = ">="
+    elif ">" in test:
+        delim = ">"
+    elif "=" in test:
+        delim = "="
+    (name, value) = test.split(delim, 1)
+    if value.startswith("@"):
+        value = item[value[1:]]
+
+    if delim == ">":
+        if float(item[name]) > float(value):
+            return True
+
+    if delim == ">=":
+        if float(item[name]) >= float(value):
+            return True
+
+    if delim == "=":
+        if str(item[name]) == str(value):
+            return True
+    return False
+
+
+def main():
+    url = sys.argv[1]
+    tests = sys.argv[2:]
+
+    r = requests.get(url, auth=("admin@opencord.org", "letmein"))
+    for item in r.json()["items"]:
+        okay = True
+        for test in tests:
+            if not matches(test, item):
+                okay = False
+        if okay:
+            print("matched")
+            sys.exit(0)
+
+    print("Not Matched")
+    sys.exit(-1)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/testservice/scripts/wait_for_lines.py b/testservice/scripts/wait_for_lines.py
new file mode 100755
index 0000000..089978f
--- /dev/null
+++ b/testservice/scripts/wait_for_lines.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+# 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 __future__ import absolute_import, print_function
+
+import re
+import signal
+import sys
+
+
+def handler(signum, frame):
+    print "Timed Out"
+    sys.exit(-1)
+
+
+signal.signal(signal.SIGALRM, handler)
+signal.alarm(60)  # timeout in 60 seconds
+
+waitlines = open(sys.argv[1]).readlines()
+waitlines = [x.strip() for x in waitlines]
+
+while True:
+    if not waitlines:
+        print("all lines matched")
+        break
+    line = sys.stdin.readline()
+    line = line.strip()
+    for pattern in waitlines[:]:
+        if re.match(pattern, line):
+            print("matched", pattern)
+            waitlines.remove(pattern)
diff --git a/testservice/tosca/test_create_dup.yaml b/testservice/tosca/test_create_dup.yaml
new file mode 100644
index 0000000..7b3c7dd
--- /dev/null
+++ b/testservice/tosca/test_create_dup.yaml
@@ -0,0 +1,35 @@
+# 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.
+
+tosca_definitions_version: tosca_simple_yaml_1_0
+description: Set up testservice service
+imports:
+  - custom_types/testserviceservice.yaml
+  - custom_types/testserviceserviceinstance.yaml
+
+topology_template:
+  node_templates:
+    service#testservice:
+      type: tosca.nodes.TestserviceService
+      properties:
+        name: testservice
+        kind: generic
+
+    serviceinstance:
+      type: tosca.nodes.TestserviceServiceInstance
+      properties:
+          name: test-create-dup
+          create_duplicate: true
+          some_integer: 1234
+          some_other_integer: 5678
diff --git a/testservice/tosca/test_no_constraints.yaml b/testservice/tosca/test_no_constraints.yaml
new file mode 100644
index 0000000..abaf163
--- /dev/null
+++ b/testservice/tosca/test_no_constraints.yaml
@@ -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.
+
+tosca_definitions_version: tosca_simple_yaml_1_0
+description: Set up testservice service
+imports:
+  - custom_types/testserviceservice.yaml
+  - custom_types/testserviceserviceinstance.yaml
+
+topology_template:
+  node_templates:
+    service#testservice:
+      type: tosca.nodes.TestserviceService
+      properties:
+        name: testservice
+        kind: generic
+
+    serviceinstance:
+      type: tosca.nodes.TestserviceServiceInstance
+      properties:
+          name: test-no-constraints
diff --git a/testservice/tosca/test_policy_after_sync.yaml b/testservice/tosca/test_policy_after_sync.yaml
new file mode 100644
index 0000000..f697190
--- /dev/null
+++ b/testservice/tosca/test_policy_after_sync.yaml
@@ -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.
+
+tosca_definitions_version: tosca_simple_yaml_1_0
+description: Set up testservice service
+imports:
+  - custom_types/testserviceservice.yaml
+  - custom_types/testserviceserviceinstance.yaml
+
+topology_template:
+  node_templates:
+    service#testservice:
+      type: tosca.nodes.TestserviceService
+      properties:
+        name: testservice
+        kind: generic
+
+    serviceinstance:
+      type: tosca.nodes.TestserviceServiceInstance
+      properties:
+          name: test-policy-after-sync
+          policy_after_sync: true
diff --git a/testservice/tosca/test_policy_after_sync_update.yaml b/testservice/tosca/test_policy_after_sync_update.yaml
new file mode 100644
index 0000000..3101d69
--- /dev/null
+++ b/testservice/tosca/test_policy_after_sync_update.yaml
@@ -0,0 +1,34 @@
+# 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.
+
+tosca_definitions_version: tosca_simple_yaml_1_0
+description: Set up testservice service
+imports:
+  - custom_types/testserviceservice.yaml
+  - custom_types/testserviceserviceinstance.yaml
+
+topology_template:
+  node_templates:
+    service#testservice:
+      type: tosca.nodes.TestserviceService
+      properties:
+        name: testservice
+        kind: generic
+
+    serviceinstance:
+      type: tosca.nodes.TestserviceServiceInstance
+      properties:
+          name: test-policy-after-sync-update
+          policy_after_sync: true
+          update_during_policy: true
diff --git a/testservice/tosca/test_sync_after_policy.yaml b/testservice/tosca/test_sync_after_policy.yaml
new file mode 100644
index 0000000..4a219c0
--- /dev/null
+++ b/testservice/tosca/test_sync_after_policy.yaml
@@ -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.
+
+tosca_definitions_version: tosca_simple_yaml_1_0
+description: Set up testservice service
+imports:
+  - custom_types/testserviceservice.yaml
+  - custom_types/testserviceserviceinstance.yaml
+
+topology_template:
+  node_templates:
+    service#testservice:
+      type: tosca.nodes.TestserviceService
+      properties:
+        name: testservice
+        kind: generic
+
+    serviceinstance:
+      type: tosca.nodes.TestserviceServiceInstance
+      properties:
+          name: test-sync-after-policy
+          sync_after_policy: true
diff --git a/testservice/tosca/test_sync_after_policy_update.yaml b/testservice/tosca/test_sync_after_policy_update.yaml
new file mode 100644
index 0000000..c2b3f54
--- /dev/null
+++ b/testservice/tosca/test_sync_after_policy_update.yaml
@@ -0,0 +1,34 @@
+# 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.
+
+tosca_definitions_version: tosca_simple_yaml_1_0
+description: Set up testservice service
+imports:
+  - custom_types/testserviceservice.yaml
+  - custom_types/testserviceserviceinstance.yaml
+
+topology_template:
+  node_templates:
+    service#testservice:
+      type: tosca.nodes.TestserviceService
+      properties:
+        name: testservice
+        kind: generic
+
+    serviceinstance:
+      type: tosca.nodes.TestserviceServiceInstance
+      properties:
+          name: test-sync-after-policy-update
+          sync_after_policy: true
+          update_during_sync: true
diff --git a/testservice/xos/synchronizer/config.yaml b/testservice/xos/synchronizer/config.yaml
new file mode 100644
index 0000000..a1399b5
--- /dev/null
+++ b/testservice/xos/synchronizer/config.yaml
@@ -0,0 +1,23 @@
+# Copyright 2018-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: testservice
+core_version: ">=2.2.1"
+required_models:
+  - TestserviceService
+  - TestserviceServiceInstance
+dependency_graph: "/opt/xos/synchronizers/testservice/model-deps"
+model_policies_dir: "/opt/xos/synchronizers/testservice/model_policies"
+models_dir: "/opt/xos/synchronizers/testservice/models"
+steps_dir: "/opt/xos/synchronizers/testservice/steps"
diff --git a/testservice/xos/synchronizer/helpers.py b/testservice/xos/synchronizer/helpers.py
new file mode 100644
index 0000000..20f4ca8
--- /dev/null
+++ b/testservice/xos/synchronizer/helpers.py
@@ -0,0 +1,37 @@
+# Copyright 2018-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 __future__ import absolute_import
+
+import os
+import time
+
+
+def get_signal_fn(model, name):
+    return os.path.join("/tmp", "signal_%s_%s_%s" % (model.model_name, model.id, name))
+
+
+def put_signal(log, model, name):
+    open(get_signal_fn(model, name), "a").write(str(time.time()) + "\n")
+
+
+def wait_for_signal(log, model, name, timeout=30):
+    fn = get_signal_fn(model, name)
+    count = 0
+    while not os.path.exists(fn):
+        count += 1
+        if (count > 30):
+            log.error("TEST:FAIL - Timeout waiting for %s" % fn, model=model)
+            raise Exception("timeout")
+        time.sleep(1)
diff --git a/testservice/xos/synchronizer/migrations/0001_initial.py b/testservice/xos/synchronizer/migrations/0001_initial.py
new file mode 100644
index 0000000..e4a87ae
--- /dev/null
+++ b/testservice/xos/synchronizer/migrations/0001_initial.py
@@ -0,0 +1,140 @@
+# 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.
+
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-03-20 23:53
+from __future__ import unicode_literals
+
+import core.models.xosbase_header
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('core', '0009_auto_20190313_1442'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='TestserviceDuplicateServiceInstance',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('created', models.DateTimeField(auto_now_add=True, help_text=b'Time this model was created')),
+                ('updated', models.DateTimeField(default=django.utils.timezone.now, help_text=b'Time this model was changed by a non-synchronizer')),
+                ('enacted', models.DateTimeField(blank=True, default=None, help_text=b'When synced, set to the timestamp of the data that was synced', null=True)),
+                ('policed', models.DateTimeField(blank=True, default=None, help_text=b'When policed, set to the timestamp of the data that was policed', null=True)),
+                ('backend_register', models.CharField(blank=True, default=b'{}', max_length=1024, null=True)),
+                ('backend_need_delete', models.BooleanField(default=False)),
+                ('backend_need_reap', models.BooleanField(default=False)),
+                ('backend_status', models.CharField(default=b'Provisioning in progress', max_length=1024)),
+                ('backend_code', models.IntegerField(default=0)),
+                ('deleted', models.BooleanField(default=False)),
+                ('write_protect', models.BooleanField(default=False)),
+                ('lazy_blocked', models.BooleanField(default=False)),
+                ('no_sync', models.BooleanField(default=False)),
+                ('no_policy', models.BooleanField(default=False)),
+                ('policy_status', models.CharField(blank=True, default=b'Policy in process', max_length=1024, null=True)),
+                ('policy_code', models.IntegerField(blank=True, default=0, null=True)),
+                ('leaf_model_name', models.CharField(help_text=b'The most specialized model in this chain of inheritance, often defined by a service developer', max_length=1024)),
+                ('backend_need_delete_policy', models.BooleanField(default=False, help_text=b'True if delete model_policy must be run before object can be reaped')),
+                ('xos_managed', models.BooleanField(default=True, help_text=b'True if xos is responsible for creating/deleting this object')),
+                ('backend_handle', models.CharField(blank=True, help_text=b'Handle used by the backend to track this object', max_length=1024, null=True)),
+                ('changed_by_step', models.DateTimeField(blank=True, default=None, help_text=b'Time this model was changed by a sync step', null=True)),
+                ('changed_by_policy', models.DateTimeField(blank=True, default=None, help_text=b'Time this model was changed by a model policy', null=True)),
+                ('name', models.CharField(help_text=b'Named copied From serviceInstance', max_length=256)),
+                ('some_integer', models.IntegerField(default=0)),
+                ('some_other_integer', models.IntegerField(default=0)),
+                ('optional_string', models.TextField(blank=True, null=True)),
+                ('optional_string_with_default', models.TextField(blank=True, default=b'some_default', null=True)),
+                ('optional_string_with_choices', models.TextField(blank=True, choices=[(b'one', b'one'), (b'two', b'two')], null=True)),
+                ('optional_string_max_length', models.CharField(blank=True, max_length=32, null=True)),
+                ('optional_string_stripped', core.models.xosbase_header.StrippedCharField(blank=True, max_length=32, null=True)),
+                ('optional_string_date', models.DateTimeField(blank=True, null=True)),
+                ('optional_string_ip', models.GenericIPAddressField(blank=True, null=True)),
+                ('optional_string_indexed', models.TextField(blank=True, db_index=True, null=True)),
+                ('required_string', models.TextField(default=b'some_default')),
+                ('required_bool_default_false', models.BooleanField(default=False)),
+                ('required_bool_default_true', models.BooleanField(default=True)),
+                ('optional_int', models.IntegerField(blank=True, null=True)),
+                ('optional_int_with_default', models.IntegerField(blank=True, default=123, null=True)),
+                ('optional_int_with_min', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(100)])),
+                ('optional_int_with_max', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(199)])),
+                ('required_int_with_default', models.IntegerField(default=456)),
+                ('optional_float', models.FloatField(blank=True, null=True)),
+                ('optional_float_with_default', models.FloatField(blank=True, default=3.3, null=True)),
+            ],
+            options={
+                'verbose_name': 'Testservice Duplicate Service Instance',
+            },
+            bases=(models.Model, core.models.xosbase_header.PlModelMixIn),
+        ),
+        migrations.CreateModel(
+            name='TestserviceService',
+            fields=[
+                ('service_decl_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.Service_decl')),
+            ],
+            options={
+                'verbose_name': 'Testservice Service',
+            },
+            bases=('core.service',),
+        ),
+        migrations.CreateModel(
+            name='TestserviceServiceInstance',
+            fields=[
+                ('serviceinstance_decl_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.ServiceInstance_decl')),
+                ('sync_after_policy', models.BooleanField(default=False)),
+                ('sync_during_policy', models.BooleanField(default=False)),
+                ('policy_after_sync', models.BooleanField(default=False)),
+                ('policy_during_sync', models.BooleanField(default=False)),
+                ('update_during_sync', models.BooleanField(default=False)),
+                ('update_during_policy', models.BooleanField(default=False)),
+                ('create_duplicate', models.BooleanField(default=False)),
+                ('some_integer', models.IntegerField(default=0)),
+                ('some_other_integer', models.IntegerField(default=0)),
+                ('optional_string', models.TextField(blank=True, null=True)),
+                ('optional_string_with_default', models.TextField(blank=True, default=b'some_default', null=True)),
+                ('optional_string_with_choices', models.TextField(blank=True, choices=[(b'one', b'one'), (b'two', b'two')], null=True)),
+                ('optional_string_max_length', models.CharField(blank=True, max_length=32, null=True)),
+                ('optional_string_stripped', core.models.xosbase_header.StrippedCharField(blank=True, max_length=32, null=True)),
+                ('optional_string_date', models.DateTimeField(blank=True, null=True)),
+                ('optional_string_ip', models.GenericIPAddressField(blank=True, null=True)),
+                ('optional_string_indexed', models.TextField(blank=True, db_index=True, null=True)),
+                ('required_string', models.TextField(default=b'some_default')),
+                ('required_bool_default_false', models.BooleanField(default=False)),
+                ('required_bool_default_true', models.BooleanField(default=True)),
+                ('optional_int', models.IntegerField(blank=True, null=True)),
+                ('optional_int_with_default', models.IntegerField(blank=True, default=123, null=True)),
+                ('optional_int_with_min', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(100)])),
+                ('optional_int_with_max', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(199)])),
+                ('required_int_with_default', models.IntegerField(default=456)),
+                ('optional_float', models.FloatField(blank=True, null=True)),
+                ('optional_float_with_default', models.FloatField(blank=True, default=3.3, null=True)),
+            ],
+            options={
+                'verbose_name': 'Testservice Service Instance',
+            },
+            bases=('core.serviceinstance',),
+        ),
+        migrations.AddField(
+            model_name='testserviceduplicateserviceinstance',
+            name='serviceinstance',
+            field=models.ForeignKey(help_text=b'Link to the ServiceInstance this was duplicated from', on_delete=django.db.models.deletion.CASCADE, related_name='duplicates', to='testservice.TestserviceServiceInstance'),
+        ),
+    ]
diff --git a/testservice/xos/synchronizer/migrations/__init__.py b/testservice/xos/synchronizer/migrations/__init__.py
new file mode 100644
index 0000000..72d9768
--- /dev/null
+++ b/testservice/xos/synchronizer/migrations/__init__.py
@@ -0,0 +1,15 @@
+#!/usr/bin/python
+
+# 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/testservice/xos/synchronizer/model-deps b/testservice/xos/synchronizer/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/testservice/xos/synchronizer/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/testservice/xos/synchronizer/model_policies/model_policy_testservice_serviceinstance.py b/testservice/xos/synchronizer/model_policies/model_policy_testservice_serviceinstance.py
new file mode 100644
index 0000000..71dfab7
--- /dev/null
+++ b/testservice/xos/synchronizer/model_policies/model_policy_testservice_serviceinstance.py
@@ -0,0 +1,107 @@
+# Copyright 2018-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 __future__ import absolute_import
+
+import os
+import sys
+
+from xossynchronizer.model_policies.policy import Policy
+
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+from helpers import put_signal, wait_for_signal
+
+DUP_FIELD_NAMES = ["name",
+                   "some_integer",
+                   "some_other_integer",
+                   "optional_string",
+                   "optional_string_with_default",
+                   "optional_string_with_choices",
+                   "optional_string_max_length",
+                   "optional_string_stripped",
+                   "optional_string_date",
+                   "optional_string_ip",
+                   "optional_string_indexed",
+                   "required_string",
+                   "required_bool_default_false",
+                   "required_bool_default_true",
+                   "optional_int",
+                   "optional_int_with_default",
+                   "optional_int_with_min",
+                   "optional_int_with_max",
+                   "required_int_with_default",
+                   "optional_float",
+                   "optional_float_with_default"]
+
+
+class TestserviceServiceInstancePolicy(Policy):
+    """
+    TestserviceServiceInstancePolicy
+    Implements model policy for TestserviceInstance
+    """
+
+    model_name = "TestserviceServiceInstance"
+
+    def handle_create(self, model):
+        self.logger.debug(
+            "MODEL_POLICY: enter handle_create for TestserviceServiceInstance %s" %
+            model.id)
+        self.handle_update(model)
+        # TODO: Implement creation policy, if it differs from update policy
+
+    def handle_update(self, model):
+        self.logger.info("TEST:POLICY_UPDATE_START", model_class=model.model_name, model_name=model.name, id=model.id,
+                         some_integer=model.some_integer, some_other_integer=model.some_other_integer)
+
+        put_signal(self.logger, model, "policy_start")
+
+        if model.policy_after_sync or model.sync_during_policy:
+            wait_for_signal(self.logger, model, "sync_done")
+
+        if model.policy_during_sync:
+            wait_for_signal(self.logger, model, "sync_start")
+
+        if model.update_during_policy:
+            model.some_other_integer = model.some_other_integer + 1
+            model.save(update_fields=["some_other_integer"])
+
+        if model.create_duplicate:
+            dups = self.model_accessor.TestserviceDuplicateServiceInstance.objects.filter(serviceinstance_id=model.id)
+            if dups:
+                dup = dups[0]
+            else:
+                dup = self.model_accessor.TestserviceDuplicateServiceInstance(serviceinstance=model)
+
+            for fieldname in DUP_FIELD_NAMES:
+                value = getattr(model, fieldname)
+                if value is not None:
+                    setattr(dup, fieldname, value)
+
+            dup.save()
+        else:
+            # See if one was previously created when create_duplicate was previously true. If so, delete it
+            dups = self.model_accessor.TestserviceDuplicateServiceInstance.objects.filter(serviceinstance_id=model.id)
+            for dup in dups:
+                dup.delete()
+
+    def after_policy_save(self, model):
+        put_signal(self.logger, model, "policy_done")
+        self.logger.info("TEST:POLICY_DONE", model_class=model.model_name, model_name=model.name, id=model.id,
+                         some_integer=model.some_integer, some_other_integer=model.some_other_integer)
+
+    def handle_delete(self, model):
+        self.logger.debug(
+            "MODEL_POLICY: enter handle_delete for TestserviceServiceInstance %s" %
+            model.id)
+        # TODO: Implement delete policy
diff --git a/testservice/xos/synchronizer/model_policies/test_model_policy_testservice_serviceinstance.py b/testservice/xos/synchronizer/model_policies/test_model_policy_testservice_serviceinstance.py
new file mode 100644
index 0000000..e2a7005
--- /dev/null
+++ b/testservice/xos/synchronizer/model_policies/test_model_policy_testservice_serviceinstance.py
@@ -0,0 +1,98 @@
+# Copyright 2018-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 __future__ import absolute_import
+
+import imp
+import os
+import sys
+import unittest
+from mock import Mock
+
+test_path = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+
+
+class TestModelPolicyTestserviceServiceInstance(unittest.TestCase):
+
+    def setUp(self):
+
+        self.sys_path_save = sys.path
+
+        # Setting up the config module
+        from xosconfig import Config
+        config = os.path.join(test_path, "../test_config.yaml")
+        Config.clear()
+        Config.init(config, "synchronizer-config-schema.yaml")
+        # END Setting up the config module
+
+        from xossynchronizer.mock_modelaccessor_build import build_mock_modelaccessor
+
+        # Can't use mock_modelaccessor_config because we're not in the xos-services directory, so do it
+        # the long way...
+        xos_dir = os.path.join(test_path, "../../../../xos")
+        services_dir = os.path.join(test_path, "../../../..")
+        service_xprotos = [os.path.join(test_path, "../models/testservice.xproto")]
+        build_mock_modelaccessor(None, xos_dir, services_dir, service_xprotos)
+#        mock_modelaccessor_config(test_path, [("testservice", "testservice.xproto")])
+
+        import xossynchronizer.modelaccessor
+        import mock_modelaccessor
+        imp.reload(mock_modelaccessor)  # in case nose2 loaded it in a previous test
+        imp.reload(xossynchronizer.modelaccessor)  # in case nose2 loaded it in a previous test
+
+        from model_policy_testservice_serviceinstance import TestserviceServiceInstancePolicy
+        from xossynchronizer.modelaccessor import model_accessor
+
+        self.model_accessor = model_accessor
+
+        # import all class names to globals
+        for (k, v) in model_accessor.all_model_classes.items():
+            globals()[k] = v
+
+        # Some of the functions we call have side-effects, reset the world.
+        model_accessor.reset_all_object_stores()
+
+        self.policy = TestserviceServiceInstancePolicy(self.model_accessor)
+        self.si = Mock(sync_after_policy=False,
+                       sync_during_policy=False,
+                       policy_after_sync=False,
+                       policy_during_sync=False,
+                       update_during_sync=False,
+                       update_during_policy=False,
+                       create_duplicate=False)
+
+    def tearDown(self):
+        sys.path = self.sys_path_save
+        self.si = None
+
+#    def test_not_synced(self):
+#        self.si.valid = "awaiting"
+#        self.si.updated = 2
+#        self.si.enacted = 1
+#        self.si.backend_code = 0
+#
+#        with self.assertRaises(Exception) as e:
+#            self.policy.handle_update(self.si)
+#
+#        self.assertIn("has not been synced yet", e.exception.message)
+
+    def test_skip_update(self):
+        self.si.valid = "awaiting"
+        self.si.backend_code = 1
+
+        self.policy.handle_update(self.si)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/testservice/xos/synchronizer/models/testservice.xproto b/testservice/xos/synchronizer/models/testservice.xproto
new file mode 100644
index 0000000..a352ee3
--- /dev/null
+++ b/testservice/xos/synchronizer/models/testservice.xproto
@@ -0,0 +1,96 @@
+option app_label = "testservice";
+option name = "testservice";
+
+message TestserviceService (Service){
+  option verbose_name = "Testservice Service";
+}
+
+message TestserviceServiceInstance (ServiceInstance){
+  option owner_class_name = "TestserviceService";
+  option verbose_name = "Testservice Service Instance";
+
+  required bool sync_after_policy = 1 [default = False];
+  required bool sync_during_policy = 2 [default = False];
+  required bool policy_after_sync = 3 [default = False];
+  required bool policy_during_sync = 4 [default = False];
+
+  required bool update_during_sync = 5 [default = False];
+  required bool update_during_policy = 6 [default = False];
+
+  required bool create_duplicate = 9 [default = False];
+
+  // used by the policy/syncstep ordering tests
+  required int32 some_integer = 7 [default = 0];
+  required int32 some_other_integer = 8 [default = 0];
+
+  // optional strings
+  optional string optional_string = 20 [];
+  optional string optional_string_with_default = 21 [default="some_default"];
+  optional string optional_string_with_choices = 22 [choices="(('one', 'one'), ('two', 'two'))"];
+  optional string optional_string_max_length = 23 [max_length=32];
+  optional string optional_string_stripped = 24 [content_type="stripped", max_length=32];
+  optional string optional_string_date = 25 [content_type = "date"];
+  optional string optional_string_ip = 26 [content_type = "ip"];
+  optional string optional_string_indexed = 27 [db_index = True];
+
+  // required strings
+  required string required_string = 30 [default="some_default"];
+
+  // booleans
+  required bool required_bool_default_false = 40 [default=False];
+  required bool required_bool_default_true = 41 [default=True];
+
+  // integers
+  optional int32 optional_int = 50 [];
+  optional int32 optional_int_with_default = 51 [default=123];
+  optional int32 optional_int_with_min = 52 [min_value=100];
+  optional int32 optional_int_with_max = 53 [max_value=199];
+  required int32 required_int_with_default = 54 [default=456];
+
+  // floats
+  optional float optional_float = 60 [];
+  optional float optional_float_with_default = 61 [default=3.3];
+}
+
+message TestserviceDuplicateServiceInstance (XOSBase){
+  option verbose_name = "Testservice Duplicate Service Instance";
+
+  required manytoone serviceinstance->TestserviceServiceInstance:duplicates = 1:1001 [
+         help_text = "Link to the ServiceInstance this was duplicated from"];
+
+  required string name = 2 [
+         help_text = "Named copied From serviceInstance",
+         max_length = 256];
+
+  // used by the policy/syncstep ordering tests
+  required int32 some_integer = 7 [default = 0];
+  required int32 some_other_integer = 8 [default = 0];
+
+  // optional strings
+  optional string optional_string = 20 [];
+  optional string optional_string_with_default = 21 [default="some_default"];
+  optional string optional_string_with_choices = 22 [choices="(('one', 'one'), ('two', 'two'))"];
+  optional string optional_string_max_length = 23 [max_length=32];
+  optional string optional_string_stripped = 24 [content_type="stripped", max_length=32];
+  optional string optional_string_date = 25 [content_type = "date"];
+  optional string optional_string_ip = 26 [content_type = "ip"];
+  optional string optional_string_indexed = 27 [db_index = True];
+
+  // required strings
+  required string required_string = 30 [default="some_default"];
+
+  // booleans
+  required bool required_bool_default_false = 40 [default=False];
+  required bool required_bool_default_true = 41 [default=True];
+
+  // integers
+  optional int32 optional_int = 50 [];
+  optional int32 optional_int_with_default = 51 [default=123];
+  optional int32 optional_int_with_min = 52 [min_value=100];
+  optional int32 optional_int_with_max = 53 [max_value=199];
+  required int32 required_int_with_default = 54 [default=456];
+
+  // floats
+  optional float optional_float = 60 [];
+  optional float optional_float_with_default = 61 [default=3.3];
+}
diff --git a/testservice/xos/synchronizer/steps/sync_testservice_serviceinstance.py b/testservice/xos/synchronizer/steps/sync_testservice_serviceinstance.py
new file mode 100644
index 0000000..5c4b11a
--- /dev/null
+++ b/testservice/xos/synchronizer/steps/sync_testservice_serviceinstance.py
@@ -0,0 +1,73 @@
+# Copyright 2018-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 __future__ import absolute_import
+
+import os
+import sys
+from xossynchronizer.steps.syncstep import SyncStep
+from xossynchronizer.modelaccessor import TestserviceServiceInstance
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+from helpers import put_signal, wait_for_signal
+
+log = create_logger(Config().get('logging'))
+
+
+class SyncTestserviceServiceInstance(SyncStep):
+    """
+    SyncTestserviceServiceInstance
+    Implements sync step for syncing Testservice Services
+    """
+
+    provides = [TestserviceServiceInstance]
+    observes = [TestserviceServiceInstance]
+    requested_interval = 0
+
+    def sync_record(self, model):
+        log.info("TEST:SYNC_START",
+                 model_class=model.model_name,
+                 model_name=model.name,
+                 id=model.id,
+                 some_integer=model.some_integer,
+                 some_other_integer=model.some_other_integer)
+
+        put_signal(log, model, "sync_start")
+
+        if model.sync_after_policy or model.policy_during_sync:
+            wait_for_signal(log, model, "policy_done")
+
+        if model.sync_during_policy:
+            wait_for_signal(log, model, "policy_start")
+
+        if model.update_during_sync:
+            model.some_integer = model.some_integer + 1
+            model.save(update_fields=["some_integer"])
+
+    def after_sync_save(self, model):
+        put_signal(log, model, "sync_done")
+        log.info("TEST:SYNC_DONE",
+                 model_class=model.model_name,
+                 model_name=model.name,
+                 id=model.id,
+                 some_integer=model.some_integer,
+                 some_other_integer=model.some_other_integer)
+
+    def delete_record(self, model):
+        log.info("Deleting TestserviceServiceInstance",
+                 object=str(model))
+        # TODO: Implement delete step
diff --git a/testservice/xos/synchronizer/steps/test_sync_testservice_serviceinstance.py b/testservice/xos/synchronizer/steps/test_sync_testservice_serviceinstance.py
new file mode 100644
index 0000000..c9e8af9
--- /dev/null
+++ b/testservice/xos/synchronizer/steps/test_sync_testservice_serviceinstance.py
@@ -0,0 +1,92 @@
+# Copyright 2018-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 __future__ import absolute_import
+
+import imp
+import os
+import sys
+import unittest
+from mock import Mock
+
+test_path = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+
+
+class TestSyncTestserviceServiceInstance(unittest.TestCase):
+
+    def setUp(self):
+
+        self.sys_path_save = sys.path
+
+        # Setting up the config module
+        from xosconfig import Config
+        config = os.path.join(test_path, "../test_config.yaml")
+        Config.clear()
+        Config.init(config, "synchronizer-config-schema.yaml")
+        # END Setting up the config module
+
+        from xossynchronizer.mock_modelaccessor_build import build_mock_modelaccessor
+
+        # Can't use mock_modelaccessor_config because we're not in the xos-services directory, so do it
+        # the long way...
+        xos_dir = os.path.join(test_path, "../../../../xos")
+        services_dir = os.path.join(test_path, "../../../..")
+        service_xprotos = [os.path.join(test_path, "../models/testservice.xproto")]
+        build_mock_modelaccessor(None, xos_dir, services_dir, service_xprotos)
+
+        import xossynchronizer.modelaccessor
+        import mock_modelaccessor
+        imp.reload(mock_modelaccessor)  # in case nose2 loaded it in a previous test
+        imp.reload(xossynchronizer.modelaccessor)  # in case nose2 loaded it in a previous test
+
+        from sync_testservice_serviceinstance import SyncTestserviceServiceInstance
+        from xossynchronizer.modelaccessor import model_accessor
+
+        self.model_accessor = model_accessor
+
+        # import all class names to globals
+        for (k, v) in model_accessor.all_model_classes.items():
+            globals()[k] = v
+
+        self.sync_step = SyncTestserviceServiceInstance
+
+        # TODO: Use modelaccessor instead
+        # create a mock instance instance
+        self.model = Mock(name="Example",
+                          some_integer=0,
+                          sync_after_policy=False,
+                          sync_during_policy=False,
+                          policy_after_sync=False,
+                          policy_during_sync=False,
+                          update_during_sync=False,
+                          update_during_policy=False,
+                          create_duplicate=False)
+
+    def tearDown(self):
+        self.model = None
+        sys.path = self.sys_path_save
+
+    # TODO - The following two tests are very simple, replace with more meaningful ones
+
+    def test_save(self):
+        # Tests that the model can be saved
+
+        self.model.name = "testservice_test"
+        self.model.update_during_sync = True
+        self.sync_step(model_accessor=self.model_accessor).sync_record(self.model)
+        self.model.save.assert_called()
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/testservice/xos/synchronizer/test_config.yaml b/testservice/xos/synchronizer/test_config.yaml
new file mode 100644
index 0000000..f2f0715
--- /dev/null
+++ b/testservice/xos/synchronizer/test_config.yaml
@@ -0,0 +1,28 @@
+# Copyright 2018-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: testservice-testconfig
+accessor:
+  username: xosadmin@opencord.org
+  password: "sample"
+  kind: "testframework"
+logging:
+  version: 1
+  handlers:
+    console:
+      class: logging.StreamHandler
+  loggers:
+    'multistructlog':
+      handlers:
+          - console
diff --git a/testservice/xos/synchronizer/testservice-synchronizer.py b/testservice/xos/synchronizer/testservice-synchronizer.py
new file mode 100644
index 0000000..86313be
--- /dev/null
+++ b/testservice/xos/synchronizer/testservice-synchronizer.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+
+# Copyright 2018-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.
+
+"""
+testservice-synchronizer.py
+This is the main entrypoint for the synchronizer. It loads the config file, and
+then starts the synchronizer.
+"""
+
+from __future__ import absolute_import
+
+import os
+from xossynchronizer import Synchronizer
+from xosconfig import Config
+
+base_config_file = os.path.abspath(os.path.dirname(
+    os.path.realpath(__file__)) + '/config.yaml')
+mounted_config_file = os.path.abspath(os.path.dirname(
+    os.path.realpath(__file__)) + '/mounted_config.yaml')
+
+if os.path.isfile(mounted_config_file):
+    Config.init(base_config_file, 'synchronizer-config-schema.yaml',
+                mounted_config_file)
+else:
+    Config.init(base_config_file, 'synchronizer-config-schema.yaml')
+
+Synchronizer().run()
diff --git a/testservice/xos/unittest.cfg b/testservice/xos/unittest.cfg
new file mode 100644
index 0000000..49cc8fd
--- /dev/null
+++ b/testservice/xos/unittest.cfg
@@ -0,0 +1,7 @@
+[unittest]
+plugins=nose2.plugins.junitxml
+code-directories=synchronizer
+                 model_policies
+                 steps
+                 pull_steps
+                 event_steps