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


# This class represents a node with a virtualized platform
# 
#
from convirt.core.utils.utils import XMConfig, Poller
import convirt.core.utils.constants
from ManagedNode import ManagedNode

constants = convirt.core.utils.constants
import traceback
import pprint

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

    """
    
    ## Here is a list of methods in the interface
    def connect(self)
    def disconnect(self)
    def is_in_error(self)

    ## VM list 
    def get_VM_count(self)
    def get_dom_names(self)
    def get_doms(self)
    def get_dom(self)
    def refresh(self)

    def add_dom_config(self, filename)
    def remove_dom_config(self, filename)

    # create/start new VMs
    def create_dom(self, config)
    def create_dom_from_file(self, filename)


    # Xen legacy
    def isDom0(self)
    def isDomU(self)

    # VM ops
    def get_state(self, name)
    def is_resident(self, name)
    def state_dom(self,name)
    def pause_dom(self, name)
    def resume_dom(self,name)
    def shutdown_dom(self, name)
    def destroy_dom(self,name)
    def reboot_dom(self, name)

    
    def restore_dom(self, name)
    def migrate_dom(self, name, dest, live, bw=None)


   ## NOTE : Migration checks interfaces are likely to change
   # return (errlist, warnlist)
   # err/warn list = list of tuple(category, message) 
   def migration_checks(self, vm_list, dest_node, live, bw=None)

   # Metrics
   def get_metrics(self)

   # vmm capabilities and host information as seen by vmm.
   # Note  : this information is exposed via __getitem__
   # the default behavior calls the vmm.info()
   def get_vmm_info(self)
   
   ## TBD : Make vmm info  more explicit. VMMInfo which can describe the values
            as well as has metadata like categories, display names etc. 
      

   # directory to store config files
   def get_auto_config_dir() - directory where the config files should be stored
                           for them to come up when machine boots
   def get_config_dir() - directoy to store vm configs.
   
    
    """

    """
    This class represents a concrete implementation of the inteface
    which makes it easy to add new platform. It is not necessary to
    follow the same implementation pattern.
    """

    """
    If you wish to leverage the implementation, you can leverage the default
    implementation of the vm ops that delegates the ops to the vm.

    -- Concrete Node should implement
    # Factory methods
    def new_config(self, filename)
    def new_vm_from_config(self, config)
    def mew_vm_from_info(self, info)

    def get_running_vms(self)    
    -- VMM
        Return a handle to the vmm proxy. The handle via which all the
        vm ops can be done.
        
       def _init_vmm(self)
       def get_vmm(self) - default implementation caches the value given
                           by _init_vmm

       Expects vmm to implement
        -- info() - information about the vmm
        -- is_in_error() - if the vmm is in some error state.
        -- connect() - connect to the vmm (if not connected)
        -- disconnect() - disconnect from the vmm


    -- The default implementation of get_metrics provides a metric cache for
       storing the last collected value, and using polling facility to
       update it. To use this, implement the
       def get_metric_snapshot(self)
       
    -- For the migration operation as a whole do a bunch of checks.
       Return values same as migration_checks
          def migration_op_checks(self, vm_list, dest_node,live)
          
    -- For migrating a given vm, do a bunch of checks
       Return values same as migration_checks
         def migration_vm_checks(self, vm_name, dest_node, live)

    -- can this server handle hvm images ? /Fully virtualized VMs
         def is_hvm(self)

    -- return true/false if the particular node can handle a given image or not.
       This DOES NOT take care of the run time aspects like mem or cpu.
          def is_image_compatible(self, image):
    
    """

    def __init__(self,
                 platform,
                 store,
                 hostname = None,
                 username= "root",
                 password=None,
                 isRemote=False,
                 ssh_port = 22,
                 helper = None,
                 use_keys = False,
                 address = None):

        ManagedNode.__init__(self, hostname,
                             ssh_port,
                             username, password,
                             isRemote,
                             helper,
                             use_keys,
                             address)
        self.platform = platform
        self._managed_domfiles = None
        self._vmm = None
        self.store = store

        self.dom_list = self.get_dom_list()

        self.metrics_poller = None
        self.POLLING_INTERVAL = 5.0 # default no.of seconds between each metric fetch
        self.MAX_POLLS = 4 # default number of times metrics poller iterates

    # directory where the config files should be stored
    # for them to come up when machine boots
    def get_auto_config_dir(self):
        return ""
    # directoy to store vm configs.
    def get_config_dir(self):
        return "/var/cache/convirt/vm_configs"
    
    def get_platform(self):
        return self.platform
    
    def get_dom_list(self):
        if self.dom_list is None:
            self.dom_list = DomListHelper(self)
        return self.dom_list

    """ derived class to provide information about vmm capabilities
        and node as seen by vmm.
    """
    def get_vmm_info(self):
        return self.get_vmm().info()

    def get_managed_domfiles(self):
        if self._managed_domfiles is None:
            mdf = self.store.get(self.hostname,
                                 constants.prop_domfiles,)
            if mdf is None:
                ## This is only done to migrate vm's from remote to local config file
                mdf = self.config.get(XMConfig.APP_DATA,
                                      constants.prop_domfiles)
                if mdf is None:
                    self._managed_domfiles = []
                else:
                    self._managed_domfiles = eval(mdf)
                    
                    if self.hostname in self.store.getHosts():
                        self.store.set(self.hostname,constants.prop_domfiles,
                                       repr(self._managed_domfiles)
                                       )
                    self.config.set(XMConfig.APP_DATA,
                                    constants.prop_domfiles,"")
            else:
                self._managed_domfiles = eval(mdf)
        return self._managed_domfiles

    def _init_vmm(self):
        return None

    def get_vmm(self):
        if self._vmm is None:
            self._vmm = self._init_vmm()
        return self._vmm

    # return a dictionary of running VMs
    def get_running_vms(self):
        return {}

    # implement lazy initialization.
    def __getattr__(self, name):
        if name == 'managed_domfiles':
            return self.get_managed_domfiles()
        elif name == 'node_info': # should probably be changed to VMM info.
            return self.get_vmm_info()
        else:
            return ManagedNode.__getattr__(self,name)


    # encapsulate the Virtualized Platform related info,
    def __getitem__(self, param):
        vmm_info = self.get_vmm_info()
        if vmm_info is not None:
            if vmm_info.has_key(param):
                return vmm_info[param]

        #return ManagedNode.__getitem__(self, param)
        return None


    # override, as we need to be more specific
    def is_in_error(self):
        return self.is_server_in_error() or self.is_vmm_in_error()

    def is_server_in_error(self):
        return ManagedNode.is_in_error(self)

    def is_vmm_in_error(self):
        return self.get_vmm().is_in_error()

    def connect(self):
        ManagedNode.connect(self)
        self.get_vmm().connect()
        self.refresh()

    def disconnect(self):
        if self._vmm is not None:
            self.get_vmm().disconnect()
            self._vmm = None
        ManagedNode.disconnect(self)

    # Factory and helpers to create VMs
    # Given a filename, return concrete config object
    def new_config(self, filename):
        return None

    # Given config, return a concrete VM
    def new_vm_from_config(self, config):
        return None

    # Given runtime info, return a concrete VM
    def new_vm_from_info(self, info):
        return None

    # public methods
    def get_VM_count(self):
        return len(self.dom_list)# includes Dom0

    def get_Managed_VM_count(self, store):
        mdf = store.get(self.hostname,constants.prop_domfiles,)
        if mdf is not None:
            return len(eval(mdf))
        else:
            return 0

    ## Add the platform info to the environment
    def _init_environ(self):
        node_env = ManagedNode._init_environ(self)
        if node_env["platform_info"] is None:
            node_env["platform_info"] = self.get_platform_info()
        return node_env


    def get_platform_info(self):
        return None

    def get_platform_info_display_names(self):
        return None


    def get_dom_ids(self):
        return self.dom_list.iterkeys()

    def get_dom_names(self):
        """
        return lists containing names of doms running on this node.
        exceptions :
        """
        names = []
        for k in self.dom_list.iterkeys():
            names.append(self.get_dom(k).name)
        return names


    def get_doms(self):
        """
        returns list containing information about nodes running on
        this node.
        returns list of Dom objects
        exceptions :
        NOTE: in most cases, get_dom_names() is a better option.
        """
        return self.dom_list

    def get_dom(self, name):
        return self.dom_list[name]


    def isDom0(self, name):
        if self.get_dom(name):
            return self.get_dom(name).isDom0()
        else:
            return False

    def isDomU(self, name):
        if self.get_dom(name):
            return self.get_dom(name).isDomU()
        else:
            return False

    def get_state(self, name):
        if self.get_dom(name):
            return self.get_dom(name).get_state()
        else:
            return None

    def is_resident(self, name):
        if self.get_dom(name):
            return self.get_dom(name).is_resident()
        else:
            return False


    def create_dom(self, config):
        """
        create a new dom given a particular config.
        exceptions:
        """
        if config.filename is None:
            raise Exception("filename must be set in the config.")
        #config.write()
        #dom_name = self.add_dom_config(config.filename)
        #self.start_dom(dom_name)
        new_vm = self.new_vm_from_config(config)
        new_vm._start(config)
        #self.get_dom(dom_name)._start(config)
        self.refresh()
        return config.name

    def create_dom_from_file(self, filename):
        config = self.new_config(filename)
        new_vm = self.new_vm_from_config(config)
        new_vm._start(config)
        #dom_name = self.add_dom_config(filename)
        #self.start_dom(dom_name)
        self.refresh()
        return config.dom_name

    def refresh(self):
        self.dom_list.refresh()

        # Manage external files.
    def add_dom_config(self, filename):
        return self.dom_list.add_dom_config(filename)

    def remove_dom_config(self, filename):
        return self.dom_list.remove_dom_config(filename)


        # Metrics
    def get_metrics(self, refresh=False):
        if refresh:
            self.metrics = self.get_metric_snapshot()
        else:
            if self.metrics is None:
                # first time get_metrics has been called
                self.get_metrics(refresh=True)

            if self.metrics_poller is None or not self.metrics_poller.isAlive():
                # kick off ascynchronous metrics polling
                # ... MAX_POLLS polls at POLLING_INTERVAL second intervals
                self.metrics_poller = Poller(self.POLLING_INTERVAL,self.get_metrics,
                                             args=[True],max_polls=self.MAX_POLLS)
                self.metrics_poller.start()

        return self.metrics

        # return the current snapshot of all running vms.
        # Map of a Map containing stats for each VM.
        # TBD: Document the keys expected.
    def get_metric_snapshot(self):
        return None

        # dom operations
    def start_dom(self, name):
        """
        start the given dom
        exceptions:
        """
        # deligate to the dom itself
        self.get_dom(name)._start()


    def pause_dom(self, name):
        """
        pause a running dom
        exceptions:
        """
        self.get_dom(name)._pause()


    def resume_dom(self, name):
        """
        pause a running dom
        exceptions:
        """
        self.get_dom(name)._resume()


    def shutdown_dom(self, name):
        """
        shutdown a running dom.
        """
        self.get_dom(name)._shutdown()


    def destroy_dom(self, name):
        """
        destroy dom
        """
        self.get_dom(name)._destroy()
        self.refresh()

    def reboot_dom(self, name):
        """
        reboot dom
        """
        self.get_dom(name)._reboot()

    def restore_dom(self, filename):
        """
        restore from snapshot file
        """
        vmm = self.get_vmm()
        if vmm:
            vmm.restore(filename)
        self.refresh()

    def migrate_dom(self, name, dest_node, live):
        """
        destroy dom
        """
        dom = self.get_dom(name)
        if dom.isDom0():
            return Exception(name + " can not be migrated.")
        self.get_dom(name)._migrate(dest_node, live,
                                    port=dest_node.migration_port)

        self.refresh()


    def migration_checks(self, vm_list, dest_node,live):
       err_list = []
       warn_list = []

       (op_e_list, op_w_list) = self.migration_op_checks(vm_list,dest_node, live)
       if op_e_list is not None:
          err_list = err_list + op_e_list
       if op_w_list is not None:
          warn_list = warn_list + op_w_list
          
       for vm in vm_list:
          (vm_e_list, vm_w_list) = self.migration_vm_checks(vm.name, dest_node, live)
          if vm_e_list is not None:
             err_list = err_list + vm_e_list
          if vm_w_list is not None:
             warn_list = warn_list + vm_w_list

       return (err_list, warn_list)


    def augment_storage_stats(self, dom_name, dom_frame):
        # augment snapshot_dom with the storage stats
        dom = self.get_dom(dom_name)
        if dom:
            cfg = dom.get_config()
            if cfg is not None:
                ss = cfg.get_storage_stats()
                if ss:
                    total_shared = ss.get_shared_total()
                    total_local = ss.get_local_total()
                    dom_frame[constants.VM_SHARED_STORAGE] = total_shared
                    dom_frame[constants.VM_LOCAL_STORAGE] = total_local
                    dom_frame[constants.VM_TOTAL_STORAGE] = total_local + total_shared



    def update_storage_totals(self, frame):
        total_shared = 0.0
        total_local = 0.0
        ## for d_f in frame.itervalues():
        ##    if isinstance(d_f, dict):
        ##        ss = d_f.get(constants.VM_SHARED_STORAGE)
        ##        if ss:
        ##            total += ss

        # count storage used by non-running VMs as well.
        for dom in self.get_doms():
            cfg = dom.get_config()
            if cfg:
                ss = cfg.get_storage_stats()
                if ss:
                    total_shared += ss.get_shared_total()
                    total_local += ss.get_local_total()

        frame[constants.VM_SHARED_STORAGE] = total_shared
        frame[constants.VM_LOCAL_STORAGE] = total_local
        frame[constants.VM_TOTAL_STORAGE] = total_shared + total_local


    # 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 = []

        return (err_list, warn_list)




    ## 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 = []

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


        # the platform check
        node_platform = dest_node.get_platform()
        vm_platform = vm.get_platform()
        if vm_platform != node_platform:
            err_list.append(("Platform", "The destination node does not support required platform (%s)" % (vm_platform,)))


        vm_conf = vm.get_config()
        # config file and all vbds are available.
        if vm_conf is not None and vm_conf.filename is not None:
            des = vm_conf.getDisks()
            if des:
                for de in des:
                    if not dest_node.node_proxy.file_exists(de.filename):
                        err_list.append(("Disk ", 
                                         "VM Disk %s not found on the destination node %s" % (de.filename, dest_node.hostname)))
                
        return (err_list, warn_list)

    # can handle hvm images ? /Fully virtualized VMs
    def is_hvm(self):
        raise Exception("is_hvm not implemented : ", self.__class__)

    # return true/false if the particular node can handle a given image or not.
    # this DOES NOT take care of the run time aspects like mem or cpu.
    def is_image_compatible(self, image):
        raise Exception("is_image_compatible not implemented : ", self.__class__)


    # Make is atomic or cutover to run netstat -nat and parsing.
    def get_unused_display(self):
        last_vnc = self.store.getHostProperty(constants.prop_last_vncdisplay,
                                              self.hostname)
        if last_vnc == None:
            last_vnc = "20"
        else:
            last_vnc = str((int(last_vnc) + 1) % 100 )

        self.store.setHostProperty(constants.prop_last_vncdisplay,
                                   last_vnc, self.hostname)
        return last_vnc

class DomListHelper:
    """
    Class represent list of dom being tracked by this managed
    node. 
    """
    # for implementing domlist

    def __init__(self, node):
        self._dom_dict = None
        self.node = node

    def _init_dom_list(self):
        """ take the dominfo from the API and return list of doms
        """
        if self._dom_dict is None:
            self.refresh()
        return self._dom_dict

    def __getattr__(self, name):
        if name == 'dom_dict':
            return self._init_dom_list()


    def refresh(self):
        # get a list of VMs running
        vmm = self.node.get_vmm()
        current_dict = self.node.get_running_vms()

        # running doms, also add the doms for managed files.
        auto_dir = self.node.get_auto_config_dir()
        auto_list = []
        if self.node.node_proxy.file_exists(auto_dir):
            auto_list = self.node.node_proxy.listdir(auto_dir)

            #Add to managed_domfiles the auto_list files
            new_auto_node = False
            for filename in auto_list:
                if filename.startswith(".") : # for Gentoo
                    continue
                if filename.endswith(".xml") : # skip xml
                    continue

                auto_file_name = auto_dir + '/' + filename
                try:
                    i = self.node.managed_domfiles.index(auto_file_name)
                except ValueError:
                    self.node.managed_domfiles.append(auto_file_name)
                    new_auto_node = True
                    #When a new node is added the section doesn't exist
                    # so don't add it at this point
            if new_auto_node and self.node.store.has_section(self.node.hostname):
                self.node.store.set(self.node.hostname,constants.prop_domfiles,
                                    repr(self.node.managed_domfiles),)
        for filename in self.node.managed_domfiles:

            try:
                if filename.find('/') < 0:
                    filename = "%s/%s" % (auto_dir, filename)

                if self.node.node_proxy.file_exists(filename):
                    config = self.node.new_config(filename)
                    new_vm = self.node.new_vm_from_config(config)
                else:
                    continue
            except (Exception, StandardError), e:
                traceback.print_exc()
                print "Domain File %s is invalid. Reason: %s. Removing from list"\
                      % (filename, str(e))
                self.remove_dom_config(filename)
                continue

            id = new_vm.id  # or get_id
            if current_dict.has_key(id):
                current_dict[id].set_config(new_vm.get_config())
            else:
                current_dict[id] = new_vm

        self._dom_dict = current_dict

    def __getitem__(self, item):
        if not item: return None
        if type(item) is int:
            for name, dom in self.dom_dict.iteritems():
                if dom.is_resident() and dom.id == item:
                    return dom
        else:
            if self.dom_dict.has_key(item):
                return self.dom_dict[item]
            else:
                return None

    def __len__(self):
        return len(self.dom_dict)

    def __iter__(self):
        return self.dom_dict.itervalues()


    def iterkeys(self):
        return self.dom_dict.keys()

    def itervalues(self):
        return self.dom_dict.itervalues()

        # start tracking file
    def add_dom_config(self, filename):
        if filename in self.node.managed_domfiles:
            config = self.node.new_config(filename)
            return self.node.new_vm_from_config(config)['name']
        else:
            config = self.node.new_config(filename)
            config.update_storage_stats() # update storage stats
            config.write()
            new_dom = self.node.new_vm_from_config(config)
            self.node.managed_domfiles.append(filename)
            self.node.store.set(self.node.hostname,constants.prop_domfiles,
                                repr(self.node.managed_domfiles),
                    )
            self.dom_dict[new_dom.name] = new_dom

            return new_dom.name

    def remove_dom_config(self, filename):

        if filename in self.node.managed_domfiles:
            self.node.managed_domfiles.remove(filename)
            self.node.store.set(self.node.hostname,constants.prop_domfiles,
                                repr(self.node.managed_domfiles),
                    )
            # check if running, shutdown, remove, etc.
            for d in self.dom_dict.itervalues():
                if d.get_config() is not None and \
                       d.get_config().filename == filename:
                    del self.dom_dict[d.name]
                    return True

        return False

 
