Creating SSH tunnels to monitoring channels to enable dashboard communicate with the channel

Change-Id: I2249a3f5326fd06dd5799b59c9cf486a13594305
diff --git a/xos/models.py b/xos/models.py
index a839b4a..4ab3c15 100644
--- a/xos/models.py
+++ b/xos/models.py
@@ -16,20 +16,101 @@
 
 class CeilometerService(Service):
     KIND = CEILOMETER_KIND
+    LOOK_FOR_IMAGES=[ "ceilometer-service-trusty-server-multi-nic",
+                    ]
 
     class Meta:
         app_label = "monitoring"
         verbose_name = "Ceilometer Service"
         proxy = True
 
+    def get_instance(self):
+        for slice in self.slices.all():
+             for instance in slice.instances.all():
+                 if instance.image.name in self.LOOK_FOR_IMAGES:
+                     return instance
+        return None
+
+    @property
+    def addresses(self):
+        if (not self.id) or (not self.get_instance()):
+            return {}
+
+        addresses = {}
+        for ns in self.get_instance().ports.all():
+            if "private" in ns.network.name.lower():
+                addresses["private"] = (ns.ip, ns.mac)
+            elif ("nat" in ns.network.name.lower()) or ("management" in ns.network.name.lower()):
+                addresses["nat"] = (ns.ip, ns.mac)
+            #TODO: Do we need this client_access_network. Revisit in VTN context
+            #elif "ceilometer_client_access" in ns.network.labels.lower():
+            #    addresses["ceilometer"] = (ns.ip, ns.mac)
+        return addresses
+
+    @property
+    def nat_ip(self):
+        return self.addresses.get("nat", (None, None))[0]
+
+    @property
+    def nat_mac(self):
+        return self.addresses.get("nat", (None, None))[1]
+
+    @property
+    def private_ip(self):
+        return self.addresses.get("private", (None, None))[0]
+
+    @property
+    def private_mac(self):
+        return self.addresses.get("private", (None, None))[1]
+
+    def get_controller(self):
+        if not self.slices.count:
+            raise XOSConfigurationError("The service has no slices")
+        cslice = self.slices.all()[0].controllerslices.all()[0]
+        controller = cslice.controller
+        if not controller:
+            raise XOSConfigurationError("The service slice has no controller")
+        return controller
+
     @property
     def ceilometer_pub_sub_url(self):
-        return self.get_attribute("ceilometer_pub_sub_url", None)
+        if not self.get_instance():
+            return self.get_attribute("ceilometer_pub_sub_url", None)
+        if not self.private_ip:
+            return None
+        return "http://" + self.private_ip + ":4455/"
 
     @ceilometer_pub_sub_url.setter
     def ceilometer_pub_sub_url(self, value):
         self.set_attribute("ceilometer_pub_sub_url", value)
 
+    @property
+    def ceilometer_auth_url(self):
+        #FIXME: Crude way to determine if monitoring service is getting deployed with its own ceilometer+keystone 
+        if not self.get_instance():
+            return self.get_controller().auth_url
+        if not self.private_ip:
+            return None
+        return "http://" + self.private_ip + ":5000/v2.0"
+
+    @property
+    def ceilometer_admin_user(self):
+        if not self.get_instance():
+            return self.get_controller().admin_user
+        return 'admin'
+
+    @property
+    def ceilometer_admin_password(self):
+        if not self.get_instance():
+            return self.get_controller().admin_password
+        return 'password'
+
+    @property
+    def ceilometer_admin_tenant(self):
+        if not self.get_instance():
+            return self.get_controller().admin_tenant
+        return 'admin'
+
 class MonitoringChannel(TenantWithContainer):   # aka 'CeilometerTenant'
     class Meta:
         proxy = True
@@ -165,10 +246,41 @@
         return 8888+self.id
 
     @property
+    def ssh_proxy_tunnel(self):
+        return self.get_attribute("ssh_proxy_tunnel", False)
+
+    @ssh_proxy_tunnel.setter
+    def ssh_proxy_tunnel(self, value):
+        self.set_attribute("ssh_proxy_tunnel", value)
+
+    @property
+    def ssh_tunnel_port(self):
+        return self.get_attribute("ssh_tunnel_port")
+
+    @ssh_tunnel_port.setter
+    def ssh_tunnel_port(self, value):
+        self.set_attribute("ssh_tunnel_port", value)
+
+    @property
+    def ssh_tunnel_ip(self):
+        return self.get_attribute("ssh_tunnel_ip")
+
+    @ssh_tunnel_ip.setter
+    def ssh_tunnel_ip(self, value):
+        self.set_attribute("ssh_tunnel_ip", value)
+
+    @property
     def ceilometer_url(self):
-        if not self.private_ip:
-            return None
-        return "http://" + self.private_ip + ":" + str(self.ceilometer_port) + "/"
+        if self.ssh_proxy_tunnel:
+            if self.ssh_tunnel_ip and self.ssh_tunnel_port:
+                return "http://" + self.ssh_tunnel_ip + ":" + str(self.ssh_tunnel_port) + "/"
+            else:
+                return None
+        else:
+            if self.private_ip and self.ceilometer_port:
+                return "http://" + self.private_ip + ":" + str(self.ceilometer_port) + "/"
+            else:
+                return None
 
 def model_policy_monitoring_channel(pk):
     # TODO: this should be made in to a real model_policy
diff --git a/xos/synchronizer/steps/sync_monitoringchannel.py b/xos/synchronizer/steps/sync_monitoringchannel.py
index bf3390b..3b97741 100644
--- a/xos/synchronizer/steps/sync_monitoringchannel.py
+++ b/xos/synchronizer/steps/sync_monitoringchannel.py
@@ -4,6 +4,11 @@
 import sys
 import base64
 import time
+#import threading
+import subprocess
+import random
+import tempfile
+#from sshtunnel import SSHTunnelForwarder
 from django.db.models import F, Q
 from xos.config import Config
 from synchronizers.base.syncstep import SyncStep
@@ -18,6 +23,74 @@
 
 logger = Logger(level=logging.INFO)
 
+class SSHTunnel:
+
+    def __init__(self, localip, localport, key, remoteip, remote_port, jumpuser, jumphost):
+        self.key = key
+        self.remote_host = remoteip        # Remote ip on remotehost
+        self.remote_port = remote_port
+        # Get a temporary file name
+        tmpfile = tempfile.NamedTemporaryFile()
+        tmpfile.close()
+        self.socket = tmpfile.name
+        self.local_port = localport
+        self.local_host = localip
+        self.jump_user = jumpuser        # Remote user on remotehost
+        self.jump_host = jumphost        # What host do we send traffic to
+        self.open = False
+
+    def start(self):
+        exit_status = subprocess.call(['ssh', '-MfN',
+            '-S', self.socket,
+            '-i', self.key,
+            '-L', '{}:{}:{}:{}'.format(self.local_host, self.local_port, self.remote_host, self.remote_port),
+            '-o', 'ExitOnForwardFailure=True',
+            self.jump_user + '@' + self.jump_host
+        ])
+        if exit_status != 0:
+            raise Exception('SSH tunnel failed with status: {}'.format(exit_status))
+        if self.send_control_command('check') != 0:
+            raise Exception('SSH tunnel failed to check')
+        self.open = True
+
+    def stop(self):
+        if self.open:
+            if self.send_control_command('exit') != 0:
+                raise Exception('SSH tunnel failed to exit')
+            self.open = False
+
+    def send_control_command(self, cmd):
+        return subprocess.check_call(['ssh', '-S', self.socket, '-O', cmd, '-l', self.jump_user, self.jump_host])
+
+    def __enter__(self):
+        self.start()
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.stop()
+
+
+#class SshTunnel(threading.Thread):
+#    def __init__(self, localip, localport, remoteip, remoteport, proxy_ssh_key, jumpuser, jumphost):
+#        threading.Thread.__init__(self)
+#        self.localip = localip          # Local ip to listen to
+#        self.localport = localport      # Local port to listen to
+#        self.remoteip = remoteip        # Remote ip on remotehost
+#        self.remoteport = remoteport    # Remote port on remotehost
+#        self.proxy_ssh_key = proxy_ssh_key
+#        self.jumpuser = jumpuser        # Remote user on remotehost
+#        self.jumphost = jumphost        # What host do we send traffic to
+#        self.daemon = True              # So that thread will exit when
+#                                        # main non-daemon thread finishes
+#
+#    def run(self):
+#        if subprocess.call([
+#            'ssh', '-N',
+#                   '-i', self.proxy_ssh_key,
+#                   '-L', self.localip + ':' + str(self.localport) + ':' + self.remoteip + ':' + str(self.remoteport),
+#                   jumpuser + '@' + jumphost ]):
+#            raise Exception ('ssh tunnel setup failed')
+
 class SyncMonitoringChannel(SyncInstanceUsingAnsible):
     provides=[MonitoringChannel]
     observes=MonitoringChannel
@@ -58,15 +131,53 @@
 
         fields = {"unique_id": o.id,
                   "allowed_tenant_ids": o.tenant_list,
-                  "auth_url":instance.controller.auth_url,
-                  "admin_user":instance.controller.admin_user,
-                  "admin_password":instance.controller.admin_password,
-                  "admin_tenant":instance.controller.admin_tenant,
+                  "auth_url":ceilometer_service.ceilometer_auth_url,
+                  "admin_user":ceilometer_service.ceilometer_admin_user,
+                  "admin_password":ceilometer_service.ceilometer_admin_password,
+                  "admin_tenant":ceilometer_service.ceilometer_admin_tenant,
                   "ceilometer_pub_sub_url": ceilometer_pub_sub_url,
                   "full_setup": full_setup}
 
         return fields
 
+    def sync_fields(self, o, fields):
+        try:
+           super(SyncMonitoringChannel, self).sync_fields(o, fields)
+
+           #Check if ssh tunnel is needed
+           proxy_ssh = getattr(Config(), "observer_proxy_ssh", False)
+
+           if proxy_ssh:
+               proxy_ssh_key = getattr(Config(), "observer_proxy_ssh_key", None)
+               proxy_ssh_user = getattr(Config(), "observer_proxy_ssh_user", "root")
+               jump_hostname = fields["hostname"]
+
+               #Get the tunnel detsination               
+               remote_host = o.private_ip
+               remote_port = o.ceilometer_port
+               #FIXME: For now, trying to setup the tunnel on the local port same as the remote port
+               local_port = remote_port
+               local_ip = socket.gethostbyname(socket.gethostname())
+
+#               tunnel = SSHTunnelForwarder(jump_hostname,
+#                                      ssh_username=proxy_ssh_user,
+#                                      ssh_pkey=proxy_ssh_key,
+#                                      ssh_private_key_password="",
+#                                      remote_bind_address=(remote_host,remote_port),
+#                                      local_bind_address=(local_ip,local_port),
+#                                      set_keepalive=300)
+#               tunnel.start()
+               tunnel = SSHTunnel(local_ip, local_port, proxy_ssh_key, remote_host, remote_port, proxy_ssh_user, jump_hostname)
+               tunnel.start()
+
+               #Update the model with ssh tunnel info
+               o.ssh_proxy_tunnel = True
+               o.ssh_tunnel_ip = local_ip
+               o.ssh_tunnel_port = local_port
+
+        except Exception,error:
+           raise Exception(error)
+
     def run_playbook(self, o, fields):
         #ansible_hash = hashlib.md5(repr(sorted(fields.items()))).hexdigest()
         #quick_update = (o.last_ansible_hash == ansible_hash)