XOS watcher changes to handle routing table updates inside instances

Change-Id: I59de37aca215b90563ef5edf492473c7acf4dcc6
diff --git a/xos/core/admin.py b/xos/core/admin.py
index fa33a7f..30b5d3a 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -1090,7 +1090,7 @@
 class XosModelAdmin(XOSBaseAdmin):
     list_display = ("backend_status_icon", "name",)
     list_display_links = ('backend_status_icon', 'name',)
-    fieldList = ["name", "ui_port", "bootstrap_ui_port", "docker_project_name", "db_container_name", "enable_build", "frontend_only",
+    fieldList = ["name", "ui_port", "bootstrap_ui_port", "docker_project_name", "db_container_name", "redis_container_name", "enable_build", "frontend_only",
                  "source_ui_image", "extra_hosts", "no_start"]
     fieldsets = [
         (None, {'fields': fieldList, 'classes': ['suit-tab suit-tab-general']})]
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index ff3287e..894033a 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -408,6 +408,20 @@
     def get_vtn_src_names(self):
         return [x["name"] + "_" + x["net_id"] for x in self.get_vtn_src_nets()]
 
+    def get_composable_networks(self):
+	SUPPORTED_VTN_SERVCOMP_KINDS = ['VSG','PRIVATE']
+
+        nets = []
+        for slice in self.slices.all():
+            for net in slice.networks.all():
+                if (net.template.vtn_kind not in SUPPORTED_VTN_SERVCOMP_KINDS) or (net.owner != slice):
+                    continue
+
+                if not net.controllernetworks.exists():
+                    continue
+                nets.append(net)
+        return nets
+
 
 class ServiceAttribute(PlCoreBase):
     name = models.CharField(help_text="Attribute Name", max_length=128)
diff --git a/xos/core/models/xosmodel.py b/xos/core/models/xosmodel.py
index ab7edd6..8ca65f4 100644
--- a/xos/core/models/xosmodel.py
+++ b/xos/core/models/xosmodel.py
@@ -12,7 +12,9 @@
     ui_port = models.IntegerField(help_text="Port for XOS UI", default=80)
     bootstrap_ui_port = models.IntegerField(help_text="Port for XOS UI", default=81)
     db_container_name = StrippedCharField(max_length=200, help_text="name of XOS db container", default="xos_db")
+    redis_container_name = StrippedCharField(max_length=200, help_text="name of XOS redis container", default="xos_redis")
     docker_project_name = StrippedCharField(max_length=200, help_text="docker project name")
+    #FIXME: duplicate, delete?
     db_container_name = StrippedCharField(max_length=200, help_text="database container name")
     enable_build = models.BooleanField(help_text="True if Onboarding Synchronizer should build XOS as necessary", default=True)
     frontend_only = models.BooleanField(help_text="If True, XOS will not start synchronizer containers", default=False)
diff --git a/xos/synchronizers/base/SyncInstanceUsingAnsible.py b/xos/synchronizers/base/SyncInstanceUsingAnsible.py
index dd7e5c6..11486b7 100644
--- a/xos/synchronizers/base/SyncInstanceUsingAnsible.py
+++ b/xos/synchronizers/base/SyncInstanceUsingAnsible.py
@@ -8,7 +8,7 @@
 from xos.config import Config
 from synchronizers.base.syncstep import SyncStep
 from synchronizers.base.ansible import run_template_ssh
-from core.models import Service, Slice, ControllerSlice, ControllerUser
+from core.models import Service, Slice, ControllerSlice, ControllerUser, ModelLink, CoarseTenant, Tenant
 from xos.logger import Logger, logging
 
 logger = Logger(level=logging.INFO)
@@ -261,3 +261,84 @@
                 self.map_delete_outputs(o,res)
         except AttributeError:
                 pass
+
+    #In order to enable the XOS watcher functionality for a synchronizer, define the 'watches' attribute 
+    #in the derived class: eg. watches = [ModelLink(CoarseTenant,via='coarsetenant')]
+    #This base class implements the notification handler for handling CoarseTenant model notifications
+    #If a synchronizer need to watch on multiple objects, the additional handlers need to be implemented
+    #in the derived class and override the below handle_watched_object() method to route the notifications
+    #accordingly
+    def handle_watched_object(self, o):
+        logger.info("handle_watched_object is invoked for object %s" % (str(o)),extra=o.tologdict())
+        if (type(o) is CoarseTenant):
+           self.handle_service_composition_watch_notification(o)
+        pass
+
+    def handle_service_composition_watch_notification(self, coarse_tenant):
+        cls_obj = self.observes
+        if (type(cls_obj) is list):
+            cls_obj = cls_obj[0]
+        logger.info("handle_watched_object observed model %s" % (cls_obj))
+
+        objs = cls_obj.objects.filter(kind=cls_obj.KIND).all()
+
+        for obj in objs:
+            self.handle_service_composition_for_object(obj, coarse_tenant)
+        pass
+
+    def handle_service_composition_for_object(self, obj, coarse_tenant):
+        try:
+           instance = self.get_instance(obj)
+           valid_instance = True
+        except:
+           valid_instance = False
+
+        if not valid_instance:
+           logger.warn("handle_watched_object: No valid instance found for object %s" % (str(obj)))
+           return
+
+        provider_service = coarse_tenant.provider_service
+        subscriber_service = coarse_tenant.subscriber_service
+
+        if isinstance(obj,Service):
+            if obj.id == provider_service.id:
+                matched_service = provider_service
+                other_service = subscriber_service
+            elif obj.id == subscriber_service.id:
+                matched_service = subscriber_service
+                other_service = provider_service
+            else:
+                logger.info("handle_watched_object: Service object %s does not match with any of composed services" % (str(obj)))
+                return
+        elif isinstance(obj,Tenant):
+            if obj.provider_service.id == provider_service.id:
+                matched_service = provider_service
+                other_service = subscriber_service
+            elif obj.provider_service.id == subscriber_service.id:
+                matched_service = subscriber_service
+                other_service = provider_service
+            else:
+                logger.info("handle_watched_object: Tenant object %s does not match with any of composed services" % (str(obj)))
+                return
+        else:
+           logger.warn("handle_watched_object: Model object %s is of neither Service nor Tenant type" % (str(obj)))
+
+        src_networks = matched_service.get_composable_networks()
+        target_networks = other_service.get_composable_networks()
+        if src_networks and target_networks:
+            src_network = src_networks[0] #Only one composable network should present per service
+            target_network = target_networks[0]
+            src_ip = instance.get_network_ip(src_network.name)
+            target_subnet = target_network.controllernetworks.all()[0].subnet
+  
+            #TODO: Run ansible playbook to update the routing table entries in the instance
+            fields = self.get_ansible_fields(instance)
+            fields["ansible_tag"] =  obj.__class__.__name__ + "_" + str(obj.id) + "_service_composition"
+            fields["src_intf_ip"] = src_ip
+            fields["target_subnet"] = target_subnet
+            #Template file is available under .../synchronizers/shared_templates
+            service_composition_template_name = "sync_service_composition.yaml"
+            logger.info("handle_watched_object: Updating routing tables in the instance associated with object %s: target_subnet:%s src_ip:%s" % (str(obj), target_subnet, src_ip))
+            SyncInstanceUsingAnsible.run_playbook(self, obj, fields, service_composition_template_name)
+        else:
+           logger.info("handle_watched_object: No intersection of composable networks between composed services %s" % (str(coarse_tenant)))
diff --git a/xos/synchronizers/base/ansible.py b/xos/synchronizers/base/ansible.py
index 134c1c3..e5077a4 100644
--- a/xos/synchronizers/base/ansible.py
+++ b/xos/synchronizers/base/ansible.py
@@ -15,7 +15,7 @@
 step_dir = Config().observer_steps_dir
 sys_dir = Config().observer_sys_dir
 
-os_template_loader = jinja2.FileSystemLoader( searchpath=step_dir)
+os_template_loader = jinja2.FileSystemLoader( searchpath=[step_dir, "/opt/xos/synchronizers/shared_templates"])
 os_template_env = jinja2.Environment(loader=os_template_loader)
 
 def parse_output(msg):
diff --git a/xos/synchronizers/base/backend.py b/xos/synchronizers/base/backend.py
index 6f91c59..a23efdf 100644
--- a/xos/synchronizers/base/backend.py
+++ b/xos/synchronizers/base/backend.py
@@ -1,7 +1,10 @@
 import os
+import inspect
+import imp
 import sys
 import threading
 import time
+from syncstep import SyncStep
 from synchronizers.base.event_loop import XOSObserver
 from xos.logger import Logger, logging
 from xos.config import Config
@@ -18,17 +21,43 @@
 
 class Backend:
 
+    def load_sync_step_modules(self, step_dir=None):
+        sync_steps = []
+        if step_dir is None:
+            try:
+                step_dir = Config().observer_steps_dir
+            except:
+                step_dir = '/opt/xos/synchronizers/openstack/steps'
+
+
+        for fn in os.listdir(step_dir):
+            pathname = os.path.join(step_dir,fn)
+            if os.path.isfile(pathname) and fn.endswith(".py") and (fn!="__init__.py"):
+                module = imp.load_source(fn[:-3],pathname)
+                for classname in dir(module):
+                    c = getattr(module, classname, None)
+
+                    # make sure 'c' is a descendent of SyncStep and has a
+                    # provides field (this eliminates the abstract base classes
+                    # since they don't have a provides)
+
+                    if inspect.isclass(c) and issubclass(c, SyncStep) and hasattr(c,"provides") and (c not in sync_steps):
+                        sync_steps.append(c)
+        return sync_steps
+
     def run(self):
         update_diag(sync_start=time.time(), backend_status="0 - Synchronizer Start")
 
+        sync_steps = self.load_sync_step_modules()
+
         # start the observer
-        observer = XOSObserver()
+        observer = XOSObserver(sync_steps)
         observer_thread = threading.Thread(target=observer.run,name='synchronizer')
         observer_thread.start()
 
         # start the watcher thread
         if (watchers_enabled):
-            watcher = XOSWatcher()
+            watcher = XOSWatcher(sync_steps)
             watcher_thread = threading.Thread(target=watcher.run,name='watcher')
             watcher_thread.start()
 
diff --git a/xos/synchronizers/base/event_loop.py b/xos/synchronizers/base/event_loop.py
index 225c018..4c7ab1b 100644
--- a/xos/synchronizers/base/event_loop.py
+++ b/xos/synchronizers/base/event_loop.py
@@ -96,10 +96,11 @@
 	sync_steps = []
 
 
-	def __init__(self):
+	def __init__(self,sync_steps):
 		# The Condition object that gets signalled by Feefie events
 		self.step_lookup = {}
-		self.load_sync_step_modules()
+		#self.load_sync_step_modules()
+                self.sync_steps = sync_steps
 		self.load_sync_steps()
 		self.event_cond = threading.Condition()
 
diff --git a/xos/synchronizers/base/watchers.py b/xos/synchronizers/base/watchers.py
index c29db32..753cebe 100644
--- a/xos/synchronizers/base/watchers.py
+++ b/xos/synchronizers/base/watchers.py
@@ -7,7 +7,6 @@
 import commands
 import threading
 import json
-import pdb
 import pprint
 import traceback
 
@@ -28,6 +27,8 @@
 from diag import update_diag
 import redis
 
+logger = Logger(level=logging.INFO)
+
 class XOSWatcher:
     def load_sync_step_modules(self, step_dir=None):
         if step_dir is None:
@@ -61,16 +62,17 @@
                     except:
                         self.watch_map[w.dest.__name__]=[w]
 
-    def __init__(self):
+    def __init__(self,sync_steps):
         self.watch_map = {}
-        self.sync_steps = []
-        self.load_sync_step_modules()
+        self.sync_steps = sync_steps
+        #self.load_sync_step_modules()
         self.load_sync_steps()
         r = redis.Redis("redis")
         channels = self.watch_map.keys()
         self.redis = r
         self.pubsub = self.redis.pubsub()
         self.pubsub.subscribe(channels)
+        logger.info("XOS watcher initialized")
 
     def run(self):
         for item in self.pubsub.listen():
@@ -80,12 +82,12 @@
                 data = json.loads(item['data'])
                 pk = data['pk']
                 changed_fields = data['changed_fields']
-                pdb.set_trace()
                 for w in entry:
                     if w.into in changed_fields or not w.into:
                         if (hasattr(w.source, 'handle_watched_object')):
                             o = w.dest.objects.get(pk=data['pk'])
                             step = w.source()
                             step.handle_watched_object(o)
-            except:
+            except Exception as e:
+                logger.warn("XOS watcher: exception %s while processing object: %s" % (type(e),e))
                 pass
diff --git a/xos/synchronizers/onboarding/xosbuilder.py b/xos/synchronizers/onboarding/xosbuilder.py
index 38e2def..c23f996 100644
--- a/xos/synchronizers/onboarding/xosbuilder.py
+++ b/xos/synchronizers/onboarding/xosbuilder.py
@@ -323,12 +323,18 @@
 #                            {"image": "xosproject/xos-postgres",
 #                             "expose": [5432]}
 
+         external_links=[]
+         if xos.db_container_name:
+             external_links.append("%s:%s" % (xos.db_container_name, "xos_db"))
+         if xos.redis_container_name:
+             external_links.append("%s:%s" % (xos.redis_container_name, "redis"))
+
          containers["xos_ui"] = \
                             {"image": "xosproject/xos-ui",
                              "command": "python /opt/xos/manage.py runserver 0.0.0.0:%d --insecure --makemigrations" % xos.ui_port,
                              "ports": {"%d"%xos.ui_port : "%d"%xos.ui_port},
                              #"links": ["xos_db"],
-                             "external_links": ["%s:%s" % (xos.db_container_name, "xos_db")],
+                             "external_links": external_links,
                              "extra_hosts": extra_hosts,
                              "volumes": volume_list}
 
@@ -357,7 +363,7 @@
                      containers["xos_synchronizer_%s" % c.name] = \
                                     {"image": "xosproject/xos-synchronizer-%s" % c.name,
                                      "command": command,
-                                     "external_links": ["%s:%s" % (xos.db_container_name, "xos_db")],
+                                     "external_links": external_links,
                                      "extra_hosts": extra_hosts,
                                      "volumes": volume_list}
 
diff --git a/xos/synchronizers/shared_templates/sync_service_composition.yaml b/xos/synchronizers/shared_templates/sync_service_composition.yaml
new file mode 100644
index 0000000..d144ea3
--- /dev/null
+++ b/xos/synchronizers/shared_templates/sync_service_composition.yaml
@@ -0,0 +1,24 @@
+---
+- hosts: {{ instance_name }}
+  #gather_facts: False
+  connection: ssh
+  user: ubuntu
+  sudo: yes
+  vars:
+      target_subnet : {{ target_subnet }}
+      src_intf_ip : {{ src_intf_ip }}
+
+
+  tasks:
+  - name: Find the interface that has specified src ip
+    shell: ifconfig | grep -B1 {{ src_intf_ip }} | head -n1 | awk '{print $1}'
+    register: src_intf
+
+  - name: debug
+    debug: var=src_intf.stdout
+
+  - name: set up the network
+    shell: "{{ '{{' }} item {{ '}}' }}"
+    with_items:
+       - sudo ip route add {{ target_subnet }} dev {{ '{{' }} src_intf.stdout {{ '}}' }}
+
diff --git a/xos/tosca/custom_types/xos.m4 b/xos/tosca/custom_types/xos.m4
index e1f6c2e..4cbf675 100644
--- a/xos/tosca/custom_types/xos.m4
+++ b/xos/tosca/custom_types/xos.m4
@@ -27,6 +27,10 @@
                 type: string
                 required: false
                 description: Database container name
+            redis_container_name:
+                type: string
+                required: false
+                description: redis container name
             source_ui_image:
                 type: string
                 required: false
diff --git a/xos/tosca/custom_types/xos.yaml b/xos/tosca/custom_types/xos.yaml
index 8403a9e..fc9eb42 100644
--- a/xos/tosca/custom_types/xos.yaml
+++ b/xos/tosca/custom_types/xos.yaml
@@ -57,6 +57,10 @@
                 type: string
                 required: false
                 description: Database container name
+            redis_container_name:
+                type: string
+                required: false
+                description: redis container name
             source_ui_image:
                 type: string
                 required: false
diff --git a/xos/tosca/resources/xosmodel.py b/xos/tosca/resources/xosmodel.py
index e9662b3..5c7cb3e 100644
--- a/xos/tosca/resources/xosmodel.py
+++ b/xos/tosca/resources/xosmodel.py
@@ -4,7 +4,7 @@
 class XOSXOS(XOSResource):
     provides = "tosca.nodes.XOS"
     xos_model = XOS
-    copyin_props = ["ui_port", "bootstrap_ui_port", "docker_project_name", "db_container_name", "enable_build", "frontend_only", "source_ui_image", "extra_hosts"]
+    copyin_props = ["ui_port", "bootstrap_ui_port", "docker_project_name", "db_container_name", "redis_container_name", "enable_build", "frontend_only", "source_ui_image", "extra_hosts"]
 
 class XOSVolume(XOSResource):
     provides = "tosca.nodes.XOSVolume"