#!/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>
#

from convirt.core.utils.utils import PyConfig
import re, os

from convirt.core.utils.NodeProxy import Node
from convirt.core.utils.constants import *



# Contains base classes representing
#        VM,
#        VMConfig,
#        VMStats,
#        VMOperationsException
# Also, has utility classes
#        DiskEntry
#        NetworkEntry

class VMOperationException(Exception):
   def __init__(self, message, errno = None):
      if errno:
         Exception.__init__(self, (errno, message))
      else:
         Exception.__init__(self,  message)
      self.message = message
      self.errno = errno

   def __repr__(self):
      if self.errno != None:
         return "[Error %s]: %s" % (str(self.errno), self.message)
      else:
         return self.message
      


class VM:
    """
    This represent Doms. It encapsulates information about
    running virtual machine : state as well as resource stats
    """
    RUNNING  = 0
    BLOCKED  = 1
    PAUSED   = 2
    SHUTDOWN = 3
    CRASHED  = 4
    NOT_STARTED    = -1
    UNKNOWN  = -2


    def __init__(self, node, config=None,vm_info=None):
       if not config and not vm_info:
          raise Exception("config and vm_info both can not be None")
       
       self.node = node
       self._config = config
       self._vm_info= vm_info

       self.id = None
       self.name = None
       self._is_resident = False
       self.state  = self.UNKNOWN
       
       # Now call the init method that would initialized the above
       # state variables
       self.init()

       
    # to be provided by the derived class
    # set id, name, and _is_resident state
    def init(self):
       raise Exception("Must be implemented by derived class.")


    def get_platform(self):
       raise Exception("Platform not defined!!" + str(self.__class__))

    # interface methods
    """ return the state in which the VM is """
    def get_state(self):
       return self.state

    """ Is the vm dom0 """
    def isDom0(self):
       return False # for most platforms

    def isDomU(self):
       return True # for most platforms

    """ refresh the vm information """
    def refresh(self):
       try:
          self._vm_info = self.node.get_vmm().refresh(self.id)
          self.init()
       except VMOperationException, ex:
          self._vm_info = None
          self._is_resident = False

    """ Save the vm to a given file """
    def _save(self, filename):
       self.node.get_vmm().save(self.id, filename)


    def _reboot(self):
       self.node.get_vmm().reboot(self.id)
       self._vm_info =  None
       self._is_resident = False


    def _start(self, config = None):
       if not config:
          self.node.get_vmm().start(self._config)
       else:
          self.node.get_vmm().start(config)
       self.state = self.RUNNING

    def _shutdown(self):
       self.node.get_vmm().shutdown(self.id)
       self._vm_info =  None
       self._is_resident = False
       self.state = self.SHUTDOWN


    def _destroy(self):
       self.node.get_vmm().destroy(self.id)
       self._vm_info =  None
       self._is_resident = False
       self.state = self.NOT_STARTED


    def _pause(self):
       self.node.get_vmm().pause(self.id)
       self.state = self.PAUSED

    def _unpause(self):
       self.node.get_vmm().unpause(self.id)
       self.state = self.RUNNING

    def _suspend(self):
       self.node.get_vmm().suspend(self.id)
       self.state = PAUSED

    def _resume(self):
       self.node.get_vmm().resume(self.id)
       self.state = RUNNING

    def _migrate(self, dest,live, port):
       self.node.get_vmm().migrate(self.id, dest, live, port)
       self.state = self.NOT_STARTED


    def is_resident(self):
       return self._is_resident
    
    # change to live running VM
    def setVCPUs(self, value):
       self.node.get_vmm().setVCPUs(self.id, value)


    def setMem(self,value):
       self.node.get_vmm().setMem(self.id, value)

    # attach disks in memory
    def attachDisks(self,attach_disk_list):
       self.node.get_vmm().attachDisks(self.id,attach_disk_list)

    # detach disks in memory
    def detachDisks(self,detach_disk_list):
       self.node.get_vmm().detachDisks(self.id,detach_disk_list)

    # Associate a config
    def get_config(self):
       return self._config 
    
    def set_config(self, config):
       self._config=config
       pass

    def get_info(self):
       return self._vm_info

    # get some metric information
    # TBD: Needs some standardization on what minimal info 
    def get_snapshot(self):
       pass


    # expose the vm details via getitem
    # Need some standardization so that UI can display it.
    def __getitem__(self, param):
       pass

    # display strings
    def get_state_string(self):
       state = self.get_state()
       if state == self.RUNNING: return "Running"
       elif state ==  self.BLOCKED : return "Blocked"
       elif state ==  self.PAUSED : return "Paused"
       elif state == self.SHUTDOWN: return "Shutdown"
       elif state == self.CRASHED: return "Crashed"
       elif state == self.UNKNOWN: return "Unknown"
        
       return "Unknown"

    # return (cmd, args) for the console
    def get_console_cmd(self):
       pass

    # get the vnc port to be used
    def get_vnc_port(self):
       return None

    # should we use the graphical display or console
    def is_graphical_console(self):
       return False


    def is_hvm(self):
       if self._config is None:
          return None
       else:
          return self._config.is_hvm()
       
# Disk entry class : very generic.
class DiskEntry:
    """This class provides a structured representation of the disk
    string. The following attributes are available:

     type:     {file|phy}
     filename: the path to the VBD or device
     device:   the device to be used in the guest system
     mode:     {r|w|w!}
     """

    def __init__(self, input):
        if type(input) is str:
            # non-greedy for the disk type
            m = re.match("^((tap:)?.*?):(.*),(.*),(.*)$", input)
            if m:
                self.type = m.group(1)
                self.filename = m.group(3)
                self.device = m.group(4)
                self.mode = m.group(5)
            else:
                print "disk entry : No regexp match for", input
                raise Exception("could not parse disk "+ input)
        elif type(input) in (list, tuple):
            self.type, self.filename, self.device, self.mode = input

    def __repr__(self):
        return "%s:%s,%s,%s" % (self.type, self.filename, self.device,
                                self.mode)




# class to represent disk in an image.
class ImageDiskEntry(DiskEntry):
   CREATE_DISK = "CREATE_DISK"
   USE_DEVICE = "USE_DEVICE"
   USE_ISO = "USE_ISO"
   USE_REF_DISK = "USE_REF_DISK"
   
   def __init__(self, input, image_conf):
      DiskEntry.__init__(self, input)
      self.option = None

      self.disk_create = "no"
      self.size = None
      self.disk_type = ""

      self.image_src = None
      self.image_src_type = None
      self.image_src_format = None

      self.set_image_conf(image_conf)


   def set_image_conf(self, image_conf):
      self.image_conf = image_conf
      self.init()
      
   def init(self):
      if self.image_conf is None:
         return
      device = self.device

      # Vars
      pos = device.find(":cdrom")
      if pos > -1:
         device=device[0:pos]

      create_var = device + "_disk_create"
      image_src_var = device + "_image_src"
      image_src_type_var = device + "_image_src_type"
      image_src_format_var = device + "_image_src_format"
      size_var = device + "_disk_size"
      disk_fs_type_var = device + "_disk_fs_type"
      disk_type_var = device + "_disk_type"

      self.option = self.CREATE_DISK
      self.disk_create = self.image_conf[create_var]
      self.disk_type = self.image_conf[disk_type_var]
      self.size = self.image_conf[size_var]

      if not self.disk_type :
         if self.type == "file":
            self.disk_type = "VBD"
         if self.type == "phy": # assume physical device e.g. cdrom
            self.disk_type = ""

      
      if self.image_conf[image_src_var] :
         self.option = self.USE_REF_DISK
      elif self.type == "phy" :
         self.option = self.USE_DEVICE
      elif self.type=="file" :
         if not self.size and self.filename != "":
            self.option = self.USE_ISO
            self.disk_type = "ISO"


      if self.option == self.CREATE_DISK or self.option == self.USE_REF_DISK:
         if not self.size:
            self.size = 10000
         if not self.filename:
            self.filename = "$VM_DISKS_DIR/$VM_NAME.disk.xm"
            
      self.image_src = self.image_conf[image_src_var]
      self.image_src_type = self.image_conf[image_src_type_var]
      self.image_src_format = self.image_conf[image_src_format_var]

      self.fs_type = self.image_conf[disk_fs_type_var]


class vifEntry:
   def __init__(self, vif_str, image_config=None):
      self.vif_map = {}
      if vif_str:
         self.vif_map= dict([[z.strip() for z in y.split("=")] 
                             for y in [x.strip() 
                                       for x in vif_str.split(",")]])
   def get_mac(self):
      return self.vif_map.get("mac")

   def set_mac(self, v):
      self.vif_map["mac"] = v
      

   def get_bridge(self):
      return self.vif_map.get("bridge")

   def set_bridge(self,v):
      self.vif_map["bridge"] = v

   def __repr__(self):
      s = ''
      def_keys = ["mac", "bridge"]
      keys = self.vif_map.keys()
      for k in def_keys:
         if k in keys:
            keys.remove(k)

      for k in def_keys + keys:
         v=self.vif_map.get(k)
         if v is not None:
            if s != '':
               s = s + ","
            s = s + "%s=%s" %(k,v)
      return s


# Network class
#class NetworkEntry:

# Keep track of type and usage of each disk associated with this VM.
import stat
class VMStorageStat:
   STORAGE_STATS = "STORAGE_STATS"
   DISK_STATS = "DISK_STATS"
   LOCAL_ALLOC = "LOCAL_ALLOCATION"    # Total
   SHARED_ALLOC = "SHARED_ALLOCATION"  # Total

   DISK_NAME = "DISK_NAME"
   DISK_SIZE = "DISK_SIZE"
   DISK_DEV_TYPE = "DEV_TYPE"
   DISK_IS_LOCAL = "IS_LOCAL"

   BLOCK = "BLOCK"
   FILE = "FILE"
   UNKNOWN = "UKNOWN"

   GB_BYTES = 1024.0 * 1024.0 * 1024.0
   
   def __init__(self, config):
      self.config = config
      if self.config[self.STORAGE_STATS]:
         self.storage_stats= self.config[self.STORAGE_STATS]
      else:
         self.storage_stats = {}
         self.config[self.STORAGE_STATS] = self.storage_stats

      self.disk_stats = {}
      if self.storage_stats is not None :
         ds = self.storage_stats.get(self.DISK_STATS)
         if ds is None:
            self.storage_stats[self.DISK_STATS] = self.disk_stats # initial value of {}
         else:
            self.disk_stats = ds
            
      self.local_allocation = self.storage_stats.get(self.LOCAL_ALLOC)
      if not self.local_allocation:
         self.local_allocation = 0
      self.shared_allocation = self.storage_stats.get(self.SHARED_ALLOC)
      if not self.shared_allocation:
         self.shared_allocation = 0

      #def __repr__(self):
      #   return str(self.storage_stats)

   def set_remote(self, filename, is_remote):
      disk_names = [de.filename for de in self.config.getDisks()]
      if filename in disk_names:
          de_stat = self.disk_stats.get(filename)
          if de_stat is not None :
             de_stat[self.DISK_IS_LOCAL] = not is_remote
          else:
             de_stat = { self.DISK_NAME : filename,
                         self.DISK_SIZE : 0,
                         self.DISK_DEV_TYPE : self.UNKNOWN,
                         self.DISK_IS_LOCAL : not is_remote
                         }
             self.disk_stats[filename] = de_stat
      else:
         print "disk not found in ", disk_names
         raise Exception("disk " + filename + " not found" )
         
   def get_remote(self, filename):
      disk_names = [de.filename for de in self.config.getDisks()]
      if filename in disk_names:
          de_stat = self.disk_stats.get(filename)
          if de_stat is not None :
             return (not de_stat[self.DISK_IS_LOCAL])
          else:
             return False
      else:
         return False
   
         
   def update_stats(self):
       for de in self.config.getDisks():
          disk_size,disk_dev_type = self.get_disk_size(de)

          de_stat = self.disk_stats.get(de.filename)
          disk_is_local = True # for now.
             
          if de_stat :
             de_stat[self.DISK_SIZE] = disk_size
             de_stat[self.DISK_DEV_TYPE] = disk_dev_type
          else:
             de_stat = { self.DISK_NAME : de.filename,
                         self.DISK_SIZE : disk_size,
                         self.DISK_DEV_TYPE : disk_dev_type,
                         self.DISK_IS_LOCAL : disk_is_local
                         }
             self.disk_stats[de.filename] = de_stat
       # some disks might have been deleted
       # as well as recompute totals.
       total_local = 0          
       total_shared = 0

       disk_names = [de.filename for de in self.config.getDisks()]
       to_be_deleted = []
       for ds in self.disk_stats.itervalues():
          d_size = ds[self.DISK_SIZE]
          if d_size is None:
             d_size = 0
             
          if ds[self.DISK_NAME] not in disk_names:
             to_be_deleted.append(ds[self.DISK_NAME])
          else:
             if ds[self.DISK_IS_LOCAL]:
                total_local += d_size
             else:
                total_shared += d_size


       for key in to_be_deleted:
          del self.disk_stats[key]

       #import pprint; pprint.pprint( self.disk_stats )
       
       self.storage_stats[self.LOCAL_ALLOC]= total_local
       self.storage_stats[self.SHARED_ALLOC]= total_shared
       self.config[self.STORAGE_STATS] = self.storage_stats

   def get_shared_total(self):
      total = self.storage_stats.get(self.SHARED_ALLOC)
      if total:
         total = total / (self.GB_BYTES)
      else:
         total = 0

      return total

   def get_local_total(self):
      total = self.storage_stats.get(self.LOCAL_ALLOC)
      if total:
         total = total / (self.GB_BYTES)
      else:
         total = 0
      return total


   def get_disk_size(self, de):
       size = None
       dev_type = self.UNKNOWN

       filename=de.filename
       if filename and self.config.managed_node.node_proxy.file_exists(de.filename):
          f_stat = self.config.managed_node.node_proxy.stat(filename)
          if self.config.managed_node.is_remote():
             mode = f_stat.st_mode
          else:
             mode = f_stat[stat.ST_MODE]
          dev_type = self.FILE
          if stat.S_ISREG(mode):
             dev_type=self.FILE
          else:
             dev_type = self.BLOCK
          
          if ((not stat.S_ISREG(mode)) and (not stat.S_ISBLK(mode))):
             print "unknown disk type :", de.filename, f_stat
             return (None, dev_type)

          if filename.find("/dev") == 0:
             # assume block device
             dev_type = self.BLOCK
             try:
                try:
                   cmd="""python -c "import os; filename='%s'; fd=open(filename,'r'); fd.seek(0,2); size=fd.tell() ; fd.close(); print size" """ % filename
                   (output, code) = self.config.managed_node.node_proxy.exec_cmd(cmd)
                   #print "cmd ", cmd, " code -> " , code, " out-> ", output
                   if code == 0:
                      size=eval(output)
                   else:
                      raise Exception(output)
                except Exception, ex:
                   print "error getting disk size for ", filename, ex
             finally:
                pass
          else:
             if self.config.managed_node.is_remote():
                size = f_stat.st_size
             else:
                size = f_stat[stat.ST_SIZE] # TO DO Sparse handling
       else:
          print "Error getting disk size for", filename, \
                "(filename does not exist.)"
          
       return (size, dev_type)

# DomConfig object, will use python config 
class VMConfig(PyConfig):
    """
    reprsents startup config object (information in the conf file)
    """
    
    # Module variable.
    imps = ["import sys",
            "import os",
            "import os.path",
            ]
    

    signature = "# Automtically generated by ConVirt\n"



    # DomConfig follows
    def __init__(self, node=None, filename = None):
        """
        read stuff from file and populate the config
        when filename is None, creates an empty config
        """
        
        self.convirt_generated = False
        self.storage_stats = None
        
        PyConfig.__init__(self,
                          node,
                          filename,
                          VMConfig.signature)

        if filename is None : return

        if len(self.lines) > 0:
            if self.lines[0].find(self.signature) >= 0:
                    self.convirt_generated = True
            
        if self["name"] is None:
            raise Exception("No dom name specified")

        #pick up the name from the file
        self.name = self["name"]

    
    # read_conf
    def read_conf(self, init_glob=None, init_locs=None):
        # Leverage the fact that conf files are python syntax.
        # save on parsing code
        
        # Create global and local dicts for the file.
        # Initialize locals to the vars.
        globs = {}
        locs = {}

        cmd = '\n'.join(self.imps)  

        # Use exec to do the standard imports and
        # define variables we are passing to the script.
        exec cmd in globs, locs
        return PyConfig.read_conf(self, globs, locs)
    
    def __setitem__(self, name, item):
        self.options[name] = item
        if name == "name":
            self.name = item

    # try to keep them in sync
    def set_name(self, name):
        self.name = name
        self["name"] = name
    
    
    # get the configured disks
    # NOTE: do not change this to map, the order is important
    #       and is used at the time of provisioning for mapping disk
    #       entry template name to instantiated names
    def getDisks(self, image_config=None):
        """
        This method returns a more structured version of the config file's
        disk entry
        """
        reslist = []
        for str in self["disk"]:
            reslist.append(ImageDiskEntry(str, image_config))
        return reslist

    def getNetworks(self, image_config=None):
        """
        This method returns a more structured version of the config file's
        vif entry
        """
        reslist = []
        for str in self["vif"]:
            reslist.append(vifEntry(str, image_config))
        return reslist

    # Assumes that the file read contains single line values
    # now preserves the comments and order
    def write(self):
        """Writes the settings out to the filename specified during
        initialization"""
        self.name = self["name"]
        PyConfig.write(self)
        self.convirt_generated = True


    def is_convirt_generated(self):
        return self.convirt_generated

    # Get raw file content    
    def get_contents(self):
        f = self.managed_node.node_proxy.open(self.filename)
        lines = f.readlines()
        f.close()
        contents = "".join(lines)
        return contents

    # Write raw contents
    def write_contents(self, contents):
        """Writes the settings out to the filename specified during
        initialization"""
        outfile = self.managed_node.node_proxy.open(self.filename, 'w')
        outfile.write(contents)
        outfile.close()



    # Validate the config. Actual details to be supplied by the
    # derived class
    
    def validate(self):
        """Attempts to validate that the settings are not going to lead to
        any errors when the dom is started, and returns a list of the
        errors as strings"""

        result = []
        return result

    def is_hvm(self):
       if self["builder"] == "hvm" :
          return True
       else:
          return False

    # assumes valid disk entries. (not tempate ones)
    # For each disk that is exposed via host system,
    # update the size.
    def update_storage_stats(self):
       storage_stat =  self.get_storage_stats()
       storage_stat.update_stats()
       #import pprint; pprint.pprint(storage_stat.storage_stats)

    # map containing disk name / filename in dom0 and if it is remote or not.
    def set_remote(self, remote_map):
       storage_stat = self.get_storage_stats()
       for d in self.getDisks():
          d_name = d.filename
          is_remote = remote_map.get(d_name)
          if is_remote is not None:
             storage_stat.set_remote(d_name, is_remote)
             

    def get_storage_stats(self):
       if self.storage_stats is None:
          self.storage_stats =  VMStorageStat(self)

       return self.storage_stats
       

class VMStats:
    """
    represents statatistics/measurements for a dom. (CPU, I/O etc)
    This is abstracted out so we can cut over to some other source for
    runtime statastics/measurements
    """

    def __init__(self, vm):
        """
        constructor, dom for which the stats are to be obtained.
        """
        self.vm = vm
        self.stat = {}

    def get_snapshot(self):
        # to be implemented by derived class
        return self.stat
