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"