SEBA-497 delayering, Makefile, and tox for rcord-synchronizer

Change-Id: I1005db39de51e43bdfd9b17fe6290a37131e4605
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..b8f06bb
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+.tox
+venv-service
diff --git a/.gitignore b/.gitignore
index a04b416..0d57996 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,7 @@
-.idea/
-*.pyc
-
-.coverage
-htmlcov
+.idea
+.tox
 .coverage
 coverage.xml
-nose2-junit.xml
+nose2-results.xml
+venv-service
+*.pyc
diff --git a/Dockerfile.synchronizer b/Dockerfile.synchronizer
index c94b355..0d156c2 100644
--- a/Dockerfile.synchronizer
+++ b/Dockerfile.synchronizer
@@ -17,8 +17,14 @@
 
 # xosproject/rcord-synchronizer
 
-FROM xosproject/xos-synchronizer-base:3.0.0
+FROM xosproject/alpine-grpc-base:0.9.0
 
+# Install pip packages
+COPY requirements.txt /tmp/requirements.txt
+RUN pip install -r /tmp/requirements.txt \
+ && pip freeze > /var/xos/pip_freeze_rcord_service_`date -u +%Y%m%dT%H%M%S`
+
+# Copy files
 COPY xos/synchronizer /opt/xos/synchronizers/rcord
 COPY VERSION /opt/xos/synchronizers/rcord/
 
@@ -55,4 +61,4 @@
       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 bash -c "python rcord-synchronizer.py"
+CMD ["/usr/bin/python", "/opt/xos/synchronizers/rcord/rcord-synchronizer.py"]
diff --git a/Makefile b/Makefile
index 94efe35..3d56a8a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,22 +1,80 @@
-REGISTRY          ?=
-REPOSITORY        ?=
-DOCKER_BUILD_ARGS ?=
-SERVICE           ?= rcord
-MAKEFILE_DIR      := $(dir $(realpath $(firstword $(MAKEFILE_LIST))))
-TAG               ?= $(shell cat ${MAKEFILE_DIR}/VERSION)
-IMAGENAME         := ${REGISTRY}${REPOSITORY}${SERVICE}-synchronizer:${TAG}
-SHELL             := /bin/bash
+# Copyright 2019-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.
 
-all: build push
+# Configure shell
+SHELL = bash -e -o pipefail
 
-build:
-	docker build $(DOCKER_BUILD_ARGS) -t ${IMAGENAME} -f Dockerfile.synchronizer .
+# Variables
+VERSION                  ?= $(shell cat ./VERSION)
+SERVICE_NAME             ?= $(notdir $(abspath .))
+SYNCHRONIZER_NAME        ?= rcord-synchronizer
 
-push:
-	docker push ${IMAGENAME}
+## Docker related
+DOCKER_REGISTRY          ?=
+DOCKER_REPOSITORY        ?=
+DOCKER_BUILD_ARGS        ?=
+DOCKER_TAG               ?= ${VERSION}
+DOCKER_IMAGENAME         := ${DOCKER_REGISTRY}${DOCKER_REPOSITORY}${SYNCHRONIZER_NAME}:${DOCKER_TAG}
 
-test:
-	source ../../xos/venv-xos/bin/activate && cd xos && nose2 --verbose --coverage-report term || echo "Please install the XOS virtual environment"
+## Docker labels. Only set ref and commit date if committed
+DOCKER_LABEL_VCS_URL     ?= $(shell git remote get-url $(shell git remote))
+DOCKER_LABEL_VCS_REF     ?= $(shell git diff-index --quiet HEAD -- && git rev-parse HEAD || echo "unknown")
+DOCKER_LABEL_COMMIT_DATE ?= $(shell git diff-index --quiet HEAD -- && git show -s --format=%cd --date=iso-strict HEAD || echo "unknown" )
+DOCKER_LABEL_BUILD_DATE  ?= $(shell date -u "+%Y-%m-%dT%H:%M:%SZ")
 
-migrate:
-	source ../../xos/venv-xos/bin/activate && cd xos && xos-migrate -s $(SERVICE)
\ No newline at end of file
+## Migration related - paths are relative to the xos subdirectory within this repo
+XOS_DIR                  ?= "../../../xos"
+SERVICES_DIR             ?= "../.."
+
+all: test
+
+docker-build:
+	docker build $(DOCKER_BUILD_ARGS) \
+    -t ${DOCKER_IMAGENAME} \
+    --build-arg org_label_schema_version="${VERSION}" \
+    --build-arg org_label_schema_vcs_url="${DOCKER_LABEL_VCS_URL}" \
+    --build-arg org_label_schema_vcs_ref="${DOCKER_LABEL_VCS_REF}" \
+    --build-arg org_label_schema_build_date="${DOCKER_LABEL_BUILD_DATE}" \
+    --build-arg org_opencord_vcs_commit_date="${DOCKER_LABEL_COMMIT_DATE}" \
+    -f Dockerfile.synchronizer .
+
+docker-push:
+	docker push ${DOCKER_IMAGENAME}
+
+test: test-unit test-migration
+
+test-unit:
+	tox
+
+venv-service:
+	virtualenv $@;\
+    source ./$@/bin/activate ; set -u ;\
+    pip install -r requirements.txt xosmigrate~=3.0.1
+
+create-migration: venv-service
+	source ./venv-service/bin/activate; set -u;\
+    cd xos; xos-migrate --xos-dir ${XOS_DIR} --services-dir ${SERVICES_DIR} -s ${SERVICE_NAME}
+
+test-migration: venv-service
+	source ./venv-service/bin/activate; set -u;\
+    cd xos; xos-migrate --xos-dir ${XOS_DIR} --services-dir ${SERVICES_DIR} -s ${SERVICE_NAME} --check
+
+clean:
+	find . -name '*.pyc' | xargs rm -f
+	rm -rf \
+    .tox \
+    venv-service \
+    xos/.coverage \
+    xos/coverage.xml \
+    xos/nose2-results.xml
diff --git a/VERSION b/VERSION
index 0c64b5c..f0bb29e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.2.0-dev
+1.3.0
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e039eea
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+xossynchronizer~=3.0.1
+xosapi~=3.0.1
+xoskafka~=3.0.1
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..9064a11
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,46 @@
+; Copyright 2019-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.
+
+[tox]
+envlist = py27
+; future; envlist = py27,py35,py36,py37
+skip_missing_interpreters = true
+skipsdist = True
+
+[testenv]
+deps =
+  -r requirements.txt
+  requests_mock
+  nose2
+;  flake8
+
+changedir = xos
+commands =
+  nose2 -c ../tox.ini --verbose --junit-xml
+; future: flake8
+
+[flake8]
+max-line-length = 119
+
+[unittest]
+plugins = nose2.plugins.junitxml
+
+[junit-xml]
+path = nose2-results.xml
+
+[coverage]
+always-on = True
+coverage-report =
+  term
+  xml
diff --git a/xos/synchronizer/__init__.py b/xos/synchronizer/__init__.py
new file mode 100644
index 0000000..19d1424
--- /dev/null
+++ b/xos/synchronizer/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2019-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/synchronizer/model_policies/__init__.py b/xos/synchronizer/model_policies/__init__.py
new file mode 100644
index 0000000..19d1424
--- /dev/null
+++ b/xos/synchronizer/model_policies/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2019-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/synchronizer/model_policies/model_policy_rcordsubscriber.py b/xos/synchronizer/model_policies/model_policy_rcordsubscriber.py
index cb260ed..dbf2b71 100644
--- a/xos/synchronizer/model_policies/model_policy_rcordsubscriber.py
+++ b/xos/synchronizer/model_policies/model_policy_rcordsubscriber.py
@@ -16,6 +16,7 @@
 from xossynchronizer.modelaccessor import ServiceInstanceLink, model_accessor
 from xossynchronizer.model_policies.policy import Policy
 
+
 class RCORDSubscriberPolicy(Policy):
     model_name = "RCORDSubscriber"
 
@@ -25,7 +26,9 @@
     def handle_update(self, si):
 
         if si.status == "pre-provisioned":
-            self.logger.debug("MODEL_POLICY: Skipping chain creation as RCORDSubscriber %s is in 'pre-provisioned' state" % si.id)
+            self.logger.debug(
+                "MODEL_POLICY: Skipping chain creation as RCORDSubscriber %s is in 'pre-provisioned' state" %
+                si.id)
             return
 
         chain = si.subscribed_links.all()
@@ -37,7 +40,9 @@
                 # delete chain
                 self.logger.debug("MODEL_POLICY: deleting RCORDSubscriber chain from %s" % si.id, status=si.status)
                 for link in chain:
-                    self.logger.debug("Removing link %s" % link.id, provider_service=link.provider_service_instance.leaf_model, subscriber_service=link.subscriber_service_instance.leaf_model)
+                    self.logger.debug("Removing link %s" % link.id,
+                                      provider_service=link.provider_service_instance.leaf_model,
+                                      subscriber_service=link.subscriber_service_instance.leaf_model)
                     link.delete()
                     link.provider_service_instance.leaf_model.delete()
 
diff --git a/xos/synchronizer/model_policies/test_model_policy_rcordsubscriber.py b/xos/synchronizer/model_policies/test_model_policy_rcordsubscriber.py
index 05df866..3405c6a 100644
--- a/xos/synchronizer/model_policies/test_model_policy_rcordsubscriber.py
+++ b/xos/synchronizer/model_policies/test_model_policy_rcordsubscriber.py
@@ -17,9 +17,11 @@
 from mock import patch, Mock
 
 
-import os, sys
+import os
+import sys
 
-test_path=os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+test_path = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+
 
 class TestModelPolicyRCORDSubscriber(unittest.TestCase):
     def setUp(self):
@@ -38,7 +40,7 @@
 
         import xossynchronizer.modelaccessor
         import mock_modelaccessor
-        reload(mock_modelaccessor) # in case nose2 loaded it in a previous test
+        reload(mock_modelaccessor)  # in case nose2 loaded it in a previous test
         reload(xossynchronizer.modelaccessor)      # in case nose2 loaded it in a previous test
 
         from xossynchronizer.modelaccessor import model_accessor
@@ -68,7 +70,7 @@
         self.policy.handle_create(si)
 
         with patch.object(VOLTServiceInstance, "save", autospec=True) as save_volt, \
-             patch.object(ServiceInstanceLink, "save", autospec=True) as save_link:
+                patch.object(ServiceInstanceLink, "save", autospec=True) as save_link:
 
             self.policy.handle_create(si)
             self.assertEqual(save_link.call_count, 0)
@@ -78,9 +80,9 @@
         si = self.si
         si.is_new = False
         si.subscribed_links.all.return_value = ["already", "have", "a", "chain"]
-        
+
         with patch.object(VOLTServiceInstance, "save", autospec=True) as save_volt, \
-             patch.object(ServiceInstanceLink, "save", autospec=True) as save_link:
+                patch.object(ServiceInstanceLink, "save", autospec=True) as save_link:
 
             self.policy.handle_create(si)
             self.assertEqual(save_link.call_count, 0)
@@ -100,7 +102,7 @@
         si.owner.subscribed_dependencies.all.return_value = [service_dependency]
 
         with patch.object(VOLTServiceInstance, "save", autospec=True) as save_volt, \
-             patch.object(ServiceInstanceLink, "save", autospec=True) as save_link:
+                patch.object(ServiceInstanceLink, "save", autospec=True) as save_link:
 
             self.policy.handle_create(si)
             self.assertEqual(save_link.call_count, 1)
@@ -111,18 +113,17 @@
         volt.name = "volt"
 
         link = ServiceInstanceLink()
-        link.subscriber_service_instance= self.si
+        link.subscriber_service_instance = self.si
         link.provider_service_instance = volt
         link.provider_service_instance.leaf_model = volt
 
-
         si = self.si
         si.is_new = False
         si.status = "awaiting-auth"
         si.subscribed_links.all.return_value = [link]
 
         with patch.object(VOLTServiceInstance, "delete", autospec=True) as delete_volt, \
-             patch.object(ServiceInstanceLink, "delete", autospec=True) as delete_link:
+                patch.object(ServiceInstanceLink, "delete", autospec=True) as delete_link:
 
             self.policy.handle_create(si)
             self.assertEqual(delete_link.call_count, 1)
diff --git a/xos/synchronizer/models/__init__.py b/xos/synchronizer/models/__init__.py
new file mode 100644
index 0000000..19d1424
--- /dev/null
+++ b/xos/synchronizer/models/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2019-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/synchronizer/models/convenience/rcordsubscriber.py b/xos/synchronizer/models/convenience/rcordsubscriber.py
index 47c29ed..e6e7bf0 100644
--- a/xos/synchronizer/models/convenience/rcordsubscriber.py
+++ b/xos/synchronizer/models/convenience/rcordsubscriber.py
@@ -14,9 +14,9 @@
 # limitations under the License.
 
 
-import json
 from xosapi.orm import ORMWrapper, register_convenience_wrapper
 
+
 class ORMWrapperRCORDSubscriber(ORMWrapper):
     @property
     def volt(self):
@@ -24,7 +24,7 @@
         for link in links:
             # FIXME: hardcoded service dependency
             # cast from ServiceInstance to VOLTServiceInstance
-            volts = self.stub.VOLTServiceInstance.objects.filter(id = link.provider_service_instance.id)
+            volts = self.stub.VOLTServiceInstance.objects.filter(id=link.provider_service_instance.id)
             if volts:
                 return volts[0]
         return None
@@ -38,4 +38,5 @@
                        "downlink_speed",
                        "status")
 
+
 register_convenience_wrapper("RCORDSubscriber", ORMWrapperRCORDSubscriber)
diff --git a/xos/synchronizer/models/models.py b/xos/synchronizer/models/models.py
index 7227ab9..f5ac079 100644
--- a/xos/synchronizer/models/models.py
+++ b/xos/synchronizer/models/models.py
@@ -16,17 +16,20 @@
 import socket
 import random
 
-from xos.exceptions import XOSValidationError, XOSProgrammingError, XOSPermissionDenied
+from xos.exceptions import XOSValidationError, XOSProgrammingError
 from models_decl import RCORDService_decl, RCORDSubscriber_decl, RCORDIpAddress_decl, BandwidthProfile_decl
 
+
 class BandwidthProfile(BandwidthProfile_decl):
     class Meta:
         proxy = True
 
+
 class RCORDService(RCORDService_decl):
     class Meta:
         proxy = True
 
+
 class RCORDIpAddress(RCORDIpAddress_decl):
     class Meta:
         proxy = True
@@ -44,6 +47,7 @@
         super(RCORDIpAddress, self).save(*args, **kwargs)
         return
 
+
 class RCORDSubscriber(RCORDSubscriber_decl):
 
     class Meta:
@@ -149,7 +153,9 @@
                     is_update_with_same_tag = True
 
             if self.c_tag in self.get_used_c_tags() and not is_update_with_same_tag:
-                raise XOSValidationError("The c_tag you specified (%s) has already been used on device %s" % (self.c_tag, self.onu_device))
+                raise XOSValidationError(
+                    "The c_tag you specified (%s) has already been used on device %s" %
+                    (self.c_tag, self.onu_device))
 
         # validate s_tag and c_tag combination
         if self.c_tag and self.s_tag:
@@ -173,10 +179,14 @@
 
         self.set_owner()
 
-        if self.status != "pre-provisioned" and hasattr(self.owner.leaf_model, "access") and self.owner.leaf_model.access == "voltha" and not self.deleted:
+        if self.status != "pre-provisioned" and \
+                hasattr(self.owner.leaf_model, "access") and \
+                self.owner.leaf_model.access == "voltha" and \
+                not self.deleted:
 
             # if the access network is managed by voltha, validate that onu_device actually exist
-            volt_service = self.owner.provider_services[0].leaf_model # we assume RCORDService is connected only to the vOLTService
+            # we assume RCORDService is connected only to the vOLTService
+            volt_service = self.owner.provider_services[0].leaf_model
 
             if not volt_service.has_access_device(self.onu_device):
                 raise XOSValidationError("The onu_device you specified (%s) does not exists" % self.onu_device)
diff --git a/xos/synchronizer/models/test_models.py b/xos/synchronizer/models/test_models.py
index 072b7f5..4fe767f 100644
--- a/xos/synchronizer/models/test_models.py
+++ b/xos/synchronizer/models/test_models.py
@@ -13,25 +13,30 @@
 # limitations under the License.
 
 import unittest
-import os, sys
+import os
+import sys
 from mock import patch, Mock, MagicMock
 
-test_path=os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
-service_dir=os.path.join(test_path, "../../../..")
-xos_dir=os.path.join(test_path, "../../..")
+test_path = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+service_dir = os.path.join(test_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")
+    xos_dir = os.path.join(test_path, "../../../../../../orchestration/xos/xos")
+    services_dir = os.path.join(xos_dir, "../../xos_services")
 
 # mocking XOS exception, as they're based in Django
+
+
 class Exceptions:
     XOSValidationError = Exception
     XOSProgrammingError = Exception
     XOSPermissionDenied = Exception
 
+
 class XOS:
     exceptions = Exceptions
 
+
 class TestRCORDModels(unittest.TestCase):
     def setUp(self):
 
@@ -51,7 +56,6 @@
         self.models_decl.RCORDIpAddress_decl.objects = Mock()
         self.models_decl.RCORDIpAddress_decl.objects.filter.return_value = []
 
-
         modules = {
             'xos.exceptions': self.xos.exceptions,
             'models_decl': self.models_decl
@@ -68,7 +72,7 @@
 
         self.rcord_subscriber = RCORDSubscriber()
         self.rcord_subscriber.deleted = False
-        self.rcord_subscriber.id = None # this is a new model
+        self.rcord_subscriber.id = None  # this is a new model
         self.rcord_subscriber.is_new = True
         self.rcord_subscriber.onu_device = "BRCM1234"
         self.rcord_subscriber.c_tag = 111
@@ -80,7 +84,7 @@
         self.rcord_subscriber.owner.provider_services = [self.volt]
 
         self.rcord_ip = RCORDIpAddress()
-        self.rcord_ip.subscriber = 1;
+        self.rcord_ip.subscriber = 1
 
     def tearDown(self):
         sys.path = self.sys_path_save
@@ -184,7 +188,8 @@
         with self.assertRaises(Exception) as e:
             self.rcord_subscriber.save()
 
-        self.assertEqual(e.exception.message, "The c_tag(111) and s_tag(222) pair you specified,has already been used by Subscriber with Id (123)")
+        self.assertEqual(e.exception.message,
+                         "The c_tag(111) and s_tag(222) pair you specified,has already been used by Subscriber with Id (123)")
         self.models_decl.RCORDSubscriber_decl.save.assert_not_called()
 
     def test_validate_c_tag_on_update(self):
diff --git a/xos/synchronizer/rcord-synchronizer.py b/xos/synchronizer/rcord-synchronizer.py
index 205bd4a..5f82ca5 100644
--- a/xos/synchronizer/rcord-synchronizer.py
+++ b/xos/synchronizer/rcord-synchronizer.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
 
 # Copyright 2017-present Open Networking Foundation
 #
@@ -13,9 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
-#!/usr/bin/env python
-
 # This imports and runs ../../xos-observer.py
 
 import os