#!/usr/bin/env python
#
#   ConVirt   -  Copyright (c) 2008 Convirture Corp.
#   ======
#
# ConVirt is a Virtualization management tool with a graphical user
# interface that allows for performing the standard set of VM operations
# (start, stop, pause, kill, shutdown, reboot, snapshot, etc...). It
# also attempts to simplify various aspects of VM lifecycle management.
#
#
# This software is subject to the GNU General Public License, Version 2 (GPLv2)
# and for details, please consult it at:
#
#    http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
# 
#
# author : Jd <jd_jedi@users.sourceforge.net>
#

# Represents ManagedNode with Xen

import sys,os,re,types, socket

from convirt.core.model.ManagedNode import ManagedNode

from convirt.core.utils.utils import XMConfig, constants, Poller

from convirt.core.model.VM import VM
from convirt.core.model.VNode import VNode

from XenDomain import *
from XenVMM import XenVMM
from xen_constants import *
import traceback


class XenNode(VNode):
    """
    Interface that represents a node being managed.It defines useful APIs
    for clients to be able to do Management for a virtualized Node
    """

    def __init__(self,
                 hostname = None,
                 username= Node.DEFAULT_USER,
                 password=None,
                 isRemote=False,
                 protocol = "tcp",
                 tcp_port = 8006,
                 ssh_port = 22,
                 migration_port = 8002,
                 helper = None,
                 store = None,
                 use_keys = False,
                 address = None):

        VNode.__init__(self,
                       "xen", #platform, I'm xen node
                       store,
                       hostname,
                       username, password,
                       isRemote,
                       ssh_port,
                       helper,
                       use_keys,
                       address)
        self._dom0     = None

        self.metrics_helper = MetricsHelper(self)

        self.tcp_port = tcp_port
        self.migration_port = migration_port
        self.protocol = protocol


    def get_auto_config_dir(self):
        return '/etc/xen/auto'

    #def get_config_dir(self):
    #    return '/etc/xen'


        
    # Factory method to create vm
    def new_config(self, filename):
        return XenConfig(self, filename)

    def new_vm_from_config(self, config):
        return XenDomain(self, config=config)

    def new_vm_from_info(self, info):
        return XenDomain(self, vm_info=info)

    # return a list of running VMs
    def get_running_vms(self):
        current_dict = {}
        vm_list_info = self.get_vmm().get_vms()
        for vm_info in vm_list_info:
            vm = XenDomain(self,vm_info=vm_info)
            if vm.id == 0:
                self._set_dom0(vm)
            #else
            #ALLOW Dom 0 to be in the list too
            #This would allow twaking mem and cpus
            # The xend-managed returns non-running vms too. ignore id = None 
            if vm.id is not None: 
                current_dict[vm.name] = vm
            
        return current_dict

    def _init_vmm(self):
        return XenVMM(self.protocol, self.hostname, self.tcp_port,
                      self.isRemote,
                      self.node_proxy.transport,
                      self.ssh_port, self.username, self.password,
                      self.use_keys, self)
    

    #def __getattr__(self, name):
    #    if name == 'node_info':
    #        return self.get_vmm().info()
    #    else:
    #        return VNode.__getattr__(self, name)

    # encapsulate the host and dom0 information here.        
    def __getitem__(self, param):
        node_info = self.node_info
        val = node_info[param]
        #val = VNode.__getitem__(self, param)
        if  val == None:
            # try the dom0
            return self._dom0[param]
        else:
            return val


    def _set_dom0(self,dom):
        self._dom0 = dom

    # provide the snapshot to the base class
    def get_metric_snapshot(self):
       return self.metrics_helper.getFrame()


    # can Server run hvm image? 
    def is_hvm(self):
       if self["xen_caps"] and self["xen_caps"].find("hvm") > -1:
           return True
       else:
           return False

    def is_image_compatible(self, image):
        if image:
            if image.is_hvm() and self.is_hvm():
		return True
            if self.get_platform() == image.get_platform() :
                # xen image check for hvm
                if image.is_hvm():
                    return self.is_hvm() # node needs to be hvm enabled as well
                else:
                    return True
        return False


   
    # check if a given dom can be migrated to the destination node
    def migration_op_checks(self, vm_list, dest_node,live):
       (err_list, warn_list) = VNode.migration_op_checks(self, vm_list, dest_node, live)
       
       # src is not same as dest

       self._compare_node_info(dest_node, "xen_major", err_list)
       self._compare_node_info(dest_node, "xen_minor", err_list)

       # find total mem requirements and check against avail mem

       if len(vm_list) < 2:
          return (err_list, warn_list)

       if not live:
          return (err_list, warn_list)
       
       node_free_mem = self.guess_free_mem(dest_node)
       total_vm_mem = 0
       for vm in vm_list:
          if vm.is_resident():
             total_vm_mem += int(vm["memory"])

       if total_vm_mem > node_free_mem:
          err_list.append(("Memory", "Insufficient memory on destination node. " \
                          "Total VM memory %s, free memory on destination node %s " %
                          (total_vm_mem, node_free_mem)))

               
       
       return (err_list, warn_list)

    def guess_free_mem(self, dest_node):
       mem = dest_node["memory"]
       free_mem = dest_node["free_memory"]
       node_free_mem = 0
       if mem and free_mem:
           dom0Mem = int(mem)
           dom0Min = 256 # assume that dom0 can ballon down to 256 M
           if dom0Mem > dom0Min :
               node_free_mem = int(free_mem) + dom0Mem - dom0Min
               print "returning adjusted free mem", node_free_mem
           else:
               node_free_mem = int(free_mem )


       return node_free_mem

       

    ## TBD : Make output of these checks more structured.
    ##       Test name, Context (vm name), message
    def migration_vm_checks(self, vm_name, dest_node, live):
       """
       Implements a series of compatiblity checks required for successful
       migration.
       """
       (err_list, warn_list) = VNode.migration_vm_checks(self, vm_name,
                                                         dest_node, live)

       vm = self.get_dom(vm_name)
       if vm == None :
          err_list.append(("VM", "VM %s not found."% vm_name))
          return (err_list, warn_list)


       # mem assumed to be in MB (same unit)
       vm_memory = 0
       if vm.is_resident():
          vm_memory = vm["memory"]
          
       node_free_mem =  self.guess_free_mem(dest_node)
       if int(vm_memory) >  node_free_mem:
          err_list.append(("Memory","Insufficient memory on destination node. " \
                          "VM memory %s, free memory on destination node %s " %
                          (vm["memory"], node_free_mem)))

       # TBD : compare CPUs. This needs to encode compatibility list.
       #       check AMD/INTEL
       #       X86/X32

       # 32 bit vs 64 bit kernel

       # critical files available or not.
       vm_conf = vm.get_config()
       if vm_conf is not None:
          bootloader = vm_conf["bootloader"]
          if bootloader and bootloader.strip() is not "":
             if not dest_node.node_proxy.file_exists(bootloader.strip()):
                err_list.append(("Bootloader","Bootloader %s for %s vm not found on destination node." % (bootloader.strip(), vm.name)))
          kernel = vm_conf["kernel"]
          if kernel and kernel.strip() is not "":
             if not dest_node.node_proxy.file_exists(kernel.strip()):
                err_list.append(("Kernel","Kernel %s for %s vm not found on destination node." % (kernel.strip(), vm.name)))
          ramdisk = vm_conf["ramdisk"]
          if ramdisk and ramdisk.strip() is not "":
             if not dest_node.node_proxy.file_exists(ramdisk.strip()):
                err_list.append(("Ramdisk", "Ramdisk %s for %s vm not found on destination node." % (ramdisk.strip(), vm.name)))

       # hvm availablity
       if vm_conf and vm_conf.is_hvm() \
          and not self["xen_caps"].find("hvm"):
          err_list.append(("HVM","VM %s requires hvm capabilities which are not found on destination node." % (vm.name)))


       # TBD : PAE kernel check

       return (err_list, warn_list)
       

    def _compare_node_info(self,dest_node, key, msg_list):
       src_val = self[key]
       dest_val = dest_node[key]
       
       if src_val != dest_val:
          msg_list.append((key.upper(), "%s mismatch src %s, dest %s" %  (key,
                                                            src_val, dest_val)))

    def get_platform_info(self):
        vmm_info = self.get_vmm_info()
        xen_ver =""
        platform_dict ={}
        xen_major = str(vmm_info['xen_major'])
        xen_minor = str(vmm_info['xen_minor'])
        xen_extra = str(vmm_info['xen_extra'])
        xen_ver += xen_major + "." + xen_minor + xen_extra

        platform_dict['xen_version'] =xen_ver
        caps_value =  vmm_info['xen_caps']
        if caps_value:
            caps_value = caps_value.strip().replace(" ",", ")
            platform_dict['xen_caps']=caps_value
        return platform_dict

    def get_platform_info_display_names(self):
        display_dict = {key_platform_xen_version:display_platform_xen_version,
                        key_platform_xen_caps:display_platform_xen_caps}
        return display_dict

    def get_VM_count(self):
        return (VNode.get_VM_count(self) -1) # adjust for dom0
          
 
class MetricsHelper:
    """A Helper to fetch and format runtime metrics from a Xen Host"""

    FRAME_CMD = 'xentop -b -i 2 -d 1'
    
    def __init__(self, node):
        self.node = node

    def getFrame(self):
        """returns a dictionary containing metrics for all running domains
        in a frame"""
        (retbuf, retcode) = self.node.node_proxy.exec_cmd(self.FRAME_CMD,
                                                          self.node.exec_path)
        if retcode: return None

        # hack to eliminate unwanted vbd entries.
        cleansed_retbuf = re.sub('vbd.*\n','',retbuf)

        frame = {} # the output metric frame (dict of dict's)


        #extract the xen version
        m = re.search('xentop.*(Xen.*?)\n',cleansed_retbuf,re.S)
        if not m:
           #build it from node info
           v = "Xen " + str(self.node["xen_major"]) + "." +str(self.node["xen_minor"])+ \
               self.node["xen_extra"]
           frame['VER'] = v
        else:
           frame['VER'] = m.group(1)

        #initialise aggregation counters
        frame['VM_TOTAL_CPU'] = 0.0  # not normalized cpu %
        frame['VM_TOTAL_MEM'] = 0.0  # memory used (not %)
        frame['VM_TOTAL_CPU(%)'] = 0.0
        frame['VM_TOTAL_MEM(%)'] = 0.0

        frame['VM_TOTAL_NETS'] = 0
        frame['VM_TOTAL_NETTX(k)'] = 0
        frame['VM_TOTAL_NETRX(k)'] = 0

        frame['VM_TOTAL_VBDS']   = 0
        frame['VM_TOTAL_VBD_OO'] = 0
        frame['VM_TOTAL_VBD_RD'] = 0
        frame['VM_TOTAL_VBD_WR'] = 0

        # split the returned buffer into individual frame buffers ...
        frames = re.split('xentop.*\n',cleansed_retbuf,re.S)
        #... and use the last frame buffer for creating the metric frame
        fbuffer = frames[-1:][0]

        # extract host cpu and mem configuration
        m = re.search('Mem:(.*) total.*CPUs:(.*)',fbuffer)
        if not m:
           # build info from node info
           cpu_str = str(self.node["nr_cpus"]) + " @ " + \
                     str(self.node["cpu_mhz"]) + "MHz"
           frame['SERVER_CPUs'] = cpu_str
           frame['SERVER_MEM'] =  str(self.node["total_memory"]) + "M"
        else:
           frame['SERVER_CPUs'] = m.group(2).strip()
           frame['SERVER_MEM'] = m.group(1).strip()

        # extract overall runtime domain stats
        m = re.search('(\d+) running.*(\d+) paused.*(\d+) crashed',fbuffer)

        running = 0
        paused  = 0
        crashed = 0
        
        agg_found = False
        if not m:
           # do aggregation in the loop later
           frame['RUNNING_VMs'] = 0
           frame['PAUSED_VMs']  = 0
           frame['CRASHED_VMs'] = 0
        else:
           agg_found = True
           frame['RUNNING_VMs'] = int(m.group(1).strip())
           frame['PAUSED_VMs']  = int(m.group(2).strip())
           frame['CRASHED_VMs'] = int(m.group(3).strip())

        # parse the metric frame buffer for per domain stats
        lines = fbuffer.split('\n')[:-1]
        mbuffer = ''
        # strip unused entries at the top of the buffer
        # and extract the metric (sub)buffer
        

##         for l in lines:
##             if l.strip().startswith('NAME'):
##                 mbuffer = lines[lines.index(l):]
##                 break

        for i in range(1,len(lines)+1):
           ndx = len(lines) - i
           l = lines[ndx]
           if l.strip().startswith('NAME'):
              mbuffer = lines[ndx:]
              break

        # construct the metric frame as a dict of dictionaries
        # containing metric-name, metric-value pairs
        cleanup_exp = re.compile('[a-zA-Z]+\s[a-zA-Z]+ | n/a')
        for d in mbuffer[1:]:
            cleansed = re.sub(cleanup_exp,'None',d)
            d_frame = dict(zip(mbuffer[0].split(),cleansed.split()))

            if not agg_found:
               st = d_frame["STATE"]
               if st and st != "n/a":
                  # NOTE : xentop has its own state format string.
                  # I cant believe this mess.
                  if st[5] == 'r' or st[2] == 'b':
                     frame['RUNNING_VMs'] += 1
                  elif st[4] == 'p':
                     frame['PAUSED_VMs'] += 1
                  elif st[3] == 'c':
                     frame['CRASHED_VMs'] += 1



            if d_frame.get('CPU(%)') is not None:
               if d_frame['NAME'] != 'Domain-0':
                  # keep running count of total cpu util (not-normalized)
                  frame["VM_TOTAL_CPU"] += float(d_frame["CPU(%)"])
               
               nr_cpus = self.node["nr_cpus"]
               if nr_cpus > 1: # adjust the utilization to 100%
                  d_frame['CPU(%)'] = str(float(d_frame['CPU(%)']) / nr_cpus)
                                          

            self.node.augment_storage_stats(d_frame["NAME"], d_frame)
            
            frame[d_frame["NAME"]] = d_frame
            # compute running aggregates
            if d_frame['NAME'] != 'Domain-0':
                frame['VM_TOTAL_CPU(%)'] += float(d_frame['CPU(%)'])
                frame['VM_TOTAL_MEM(%)'] += float(d_frame['MEM(%)'])

                frame['VM_TOTAL_NETS'] += int(d_frame['NETS'])
                frame['VM_TOTAL_NETTX(k)'] += int(d_frame['NETTX(k)'])
                frame['VM_TOTAL_NETRX(k)'] += int(d_frame['NETRX(k)'])

                frame['VM_TOTAL_VBDS'] += int(d_frame['VBDS'])  
                frame['VM_TOTAL_VBD_OO'] += int(d_frame['VBD_OO'])
                frame['VM_TOTAL_VBD_RD'] += int(d_frame['VBD_RD'])
                frame['VM_TOTAL_VBD_WR'] += int(d_frame['VBD_WR'])


        # vm memory used in the same units as total memory.
        frame['VM_TOTAL_MEM'] =  (frame['VM_TOTAL_MEM(%)'] * self.node["total_memory"]) / 100.0

        # do not report Dom0
        if frame['RUNNING_VMs'] > 0 : 
           frame['RUNNING_VMs'] = frame['RUNNING_VMs'] - 1


        self.node.update_storage_totals(frame)
        return frame
        

# Test code
if __name__ == "__main__":
    test_domu = "test"
    host = "localhost"
    dom_file = '/etc/xen/test'
    dom_2b_started = 'test'
    dom_2b_shutdown = 'test'

    username = 'root'
    passwd = ''
    
    # basic connectivity
    remote = False
    if not remote:
        host = "localhost"
    else:
        host = '192.168.123.155'
        test_domu = "test"
        dom_file = '/etc/xen/test'
        dom_2b_started = 'test'
        dom_2b_shutdown = 'test'
        
    managed_node = XenNode(hostname=host,
                           username = username,
                           password = passwd,
                           isRemote=remote)

    ## create/destroy dom
    dom_config = XenConfig(managed_node, dom_file)
    dom_config["memory"] = 256
    dom_config["vif"] = ['bridge=xenbr1']
    m = { 'VM_NAME':'foo', 'IMAGE_NAME':'anaconda' } 
    dom_config.instantiate_config(m)
    print dom_config.default_computed_options, dom_config.get_computed_options()
    dom_config.save("/foo/test_config")

    sys.exit(0)
    
    dom_config = XenConfig(managed_node,dom_file)
    dom_name = managed_node.create_dom(dom_config)
    print 'resident?', managed_node.isResident(dom_name)
    managed_node.destroy_dom(dom_name)
    print 'Doms b4 removal: ',managed_node.get_dom_names()
    managed_node.remove_dom_config(dom_file)
    print 'Doms post removal: ',managed_node.get_dom_names()

    dom_name = managed_node.create_dom_from_file(dom_file)
    print 'resident?', managed_node.isResident(dom_name)
    managed_node.destroy_dom(dom_name)
    print 'Doms b4 removal: ',managed_node.get_dom_names()
    managed_node.remove_dom_config(dom_file)
    print 'Doms post removal: ',managed_node.get_dom_names()
    
    ## start / stop dom and check its running state    

    managed_node.add_dom_config(dom_file)
    for name in  managed_node.get_dom_names():
        print name
        
    ## start / stop dom and check its running state
    managed_node.start_dom(dom_2b_started)
    print 'resident?', managed_node.isResident(dom_2b_started)
    print 'memory: ',managed_node.get_dom(dom_2b_started)["memory"] 
    print 'destroying ... '
    managed_node.destroy_dom(dom_2b_started)
    print "resident?" ,managed_node.isResident(dom_2b_shutdown)
    
    
    sys.exit(0)
    #################################################
    
    doms  = managed_node.get_doms()  # test dom information.
    for dom in doms:
        print "##### some info from dom object for dom  ###"
        for key in ("name", "memory", "kernel"):
            print key, "=" , dom[key]

        if dom.is_resident():
            print "device" ,"=", dom["device"]  #priniting first device only, BUG!!
            print "image" , "=", dom["image"]
            print "state", "=", dom.state
        else:
            print "disk"," ="
            for disk in dom.get_config().getDisks():
                print disk
            print "network", "=", dom.get_config()["vif"]


    managed_node.remove_dom_config(dom_file)
    for name in  managed_node.get_dom_names():
        print name


    # get doms by name or id
    print "Access domain by name as well as id"
    dom_by_name = doms[test_domu]
    dom_by_id = doms[doms[test_domu].id]

    print dom_by_name.name, dom_by_id.name


    # get the stats
##     print "### get measurement snapshot ###"
##     stats = dom_by_name.get_snapshot()
##     for stat in stats:
##         print stat,"=",stats[stat]


    # empty dom config, create a new file
    print "### new empty config and creating a file."
    newcfg = XenConfig(managed_node)
    newcfg["name"] = "Txx"
    newcfg["memory"] = 299
    newcfg.set_filename("/foo/Txx")
    newcfg.write()

    f = managed_node.node_proxy.open("/foo/Txx")
    x = f.read(1024)
    print x
    f.close()

    print "### read config from /etc/xen/auto and write them to /foo"
    ## Dom Config
    for f in managed_node.node_proxy.listdir("/etc/xen/auto"):
        fin = "/etc/xen/auto/"+f
        print fin
        d = XenConfig(managed_node, fin)
        d.save("/foo/" + f)
    

    print "### get first file in /etc/xen/auto and dump its info"
    ## access this through dom
    d = None
    for f in managed_node.node_proxy.listdir("/etc/xen/auto"):
        fin = "/etc/xen/auto/"+f
        d = XenDomain(managed_node, fin)
        break    # pick up first file
    

    if d != None:
        print "#### dumping config ####"
        cfg =  d.get_config()
        cfg.dump()
        print "disk config"
        disks = cfg.getDisks()
        for disk in disks:
            print disk
    
        print "### modified memory to 300, dumping again ### "
        cfg['memory'] = 300
        cfg.dump()
        
    else:
        print "No Dom in /etc/auto"
    


    ## test the nodeinfo
    print "########## Host information ##########"
    for key in ("system", "host","release", "version", "machine", "nr_cpus",
                "nr_nodes","sockets_per_node",
                "cores_per_socket", "threads_per_core",
                "cpu_mhz","total_memory","free_memory","xen_caps",
                "platform_params","xen_changeset"):
        
        print key,"=",managed_node[key]


    print "########## getting dom0 information from managed node ##########"
    # Note dom0 information does not contain few things available in
    # other domus, example, uptime,device, network..!? 
    for key in ("cpu_time", "state","uptime", "online_vcpus"):
        print key,"=",managed_node[key]
    

    

    sys.exit(0)
    
    ### test create
    cfg = XenConfig(managed_node, "/etc/xen/T99")
    cfg["name"] = "T99"
    cfg["memory"] = 256
    cfg["vif"] = []
    cfg["vcpu"] = 1
    cfg["disk"] = ['file:/domu_disks/T99',xvda,w]

    d = managed_node.create_dom(cfg)
    print d["memory"], d.is_resident()

    
    

    
    
