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

# Manages shared storage definitions
# Each storage definition represents the shared storage as seen from the
# clients perspective.
# In future this would be enhanced to
# a. model the server view as well
# b. Separate SharedServer entity to which each of the storage definition
#    would belong




from datetime import datetime
from convirt.core.utils.utils import copyToRemote, get_path, XMConfig, uuidToString, randomUUID, mkdir2
from convirt.core.utils.constants import *
import os
import pprint

def ct_time():
    return datetime.now().ctime()

UNKNOWN = "UNKNOWN"
IN_SYNC = "IN_SYNC"
OUT_OF_SYNC = "OUT_OF_SYNC"


NFS = "nfs"
iSCSI = "iscsi"
AOE = "aoe"

class StorageDef:
    def __init__(self,id, name, type, description,
                 connection_props, creds_required = False, stats = {}):
        self.id = id
        if self.id is None:
            self.id = uuidToString(randomUUID())
        
        self.type = type
        self.creds_required = creds_required
        self.name = name
        self.description = description

        self.connection_props = connection_props
        self.creds = {}
        self.total_size = 0
        self.stats = stats # map containing TOTAL, USED and AVAIL

    def get_connection_props(self):
        return self.connection_props

    def set_connection_props(self, cp):
        self.connetion_props = cp

    def get_creds(self):
        return self.creds

    def get_creds(self):
        return self.creds
        
    def creds_required(self):
        return self.creds_required

    def set_creds(self, creds):
        self.creds = creds

    def set_stats(self, stats):
        self.stats = stats

    def get_stats(self):
        return self.stats


    def set_status(self, status):
        self.status = status

    def sanitized_creds(self):
        if self.creds and self.creds.get("password"):
            new_creds = self.creds.copy()
            new_creds["password"] = None
            return new_creds
        return self.creds

    def __repr__(self):
        return str({"id":self.id,
                    "type":self.type,
                    "name":self.name,
                    "description":self.description,
                    "connection_props" : self.connection_props,
                    "creds_required":self.creds_required,
                    "creds" : self.sanitized_creds(),
                    "stats" : self.stats
                    })

# keep track of group x storage definition level
class GSInfo:
    
    def __init__(self, group_id, def_id, status=UNKNOWN, oos_count = 0,
                 dt_time=None):
        self.group_id = group_id
        self.def_id = def_id
        self.status = status
        self.oos_count = oos_count
        if dt_time:
            self.dt_time = dt_time
        else:
            self.dt_time = ct_time()

    def __repr__(self):
        return str( {"group_id":self.group_id,
                     "def_id" : self.def_id,
                     "status" : self.status,
                     "oos_count" : self.oos_count,
                     "dt_time" : self.dt_time
                     }
                    )

    def dump(self):
        print "%s|%s|%s|%s|%s" % (self.group_id,
                                     self.def_id,
                                     self.status,
                                     self.oos_count,
                                     self.dt_time)
                                     

                    
# Keep track of infromation at the node level for a particulat Storage def
class NSInfo:
    def __init__(self, node_id, group_id, def_id, status, dt_time,details):
        self.node_id = node_id
        self.group_id = group_id
        self.def_id = def_id
        self.status = status
        self.dt_time = dt_time
        self.details = details

    def __repr__(self):
        return str( {"node_id":self.node_id,
                     "group_id":self.group_id,
                     "def_id" : self.def_id,
                     "status" : self.status,
                     "dt_time" : self.dt_time,
                     "details" : self.details
                     }
                    )
    
    def dump(self):
        print "%s|%s|%s|%s|%s|%s" % (self.node_id,
                                     self.group_id,
                                     self.def_id,
                                     self.status,
                                     self.dt_time,
                                     self.details)
                                     


class StorageManager:
    (src_path, s_src_scripts_location) = get_path("storage/scripts")
    s_scripts_location = "/var/cache/convirt/storage"

    storage_types = [NFS, iSCSI, AOE]

    
    def __init__(self, store):
        self.store = store

        self.storage_processors = { NFS : self.nfs_processor,
                                    iSCSI : self.iscsi_processor,
                                    AOE : self.aoe_processor
                                    }
                               
        self.defs = {}
        self.group_sds = []
        self.node_sds = []
        
        self._read_sds() # read defs
        self._read_group_sds() # read group and sd relations
        self._read_node_sds()  # read node and sd relation/status
        
    # read storage definitions from store
    def _read_sds(self):
        self.defs = {}
        storage_defs = self.store.get(XMConfig.APP_DATA,
                                      prop_storage_defs)
        s_defs = {}
        if storage_defs:
            s_defs = eval(storage_defs)

        for k, s_def in s_defs.iteritems():
            pprint.pprint(s_def)
            sd = StorageDef(s_def["id"],
                            s_def["name"],
                            s_def["type"],
                            s_def["description"],
                            s_def["connection_props"],
                            s_def["creds_required"]
                            )

            if s_def.get("stats"):
                sd.set_stats(s_def.get("stats"))

            sd.set_creds(s_def["creds"])
            self.defs[sd.id] = sd

    def save_sds(self):
        self.store.set(XMConfig.APP_DATA, prop_storage_defs, str(self.defs))

    # return the storage defintions for a given group    
    def get_sds(self, group_id):
        sds = [ self.defs[g_sd.def_id] for g_sd in self.group_sds \
                if g_sd.group_id == group_id ]

        return sds

    # execute the GET_DISK on the give node and return the results
    def get_sd_details(self, sd, node, group):
        result_processor = self.storage_processors[sd.type]
        return self.sync_node_sd(node,group.id, sd, op="GET_DISKS",
                                 refresh=True, processor=result_processor)

    def get_sd_summary(self, sd, node, group):
        result_processor = self.storage_processors[sd.type]
        return self.sync_node_sd(node,group.id, sd, op="GET_DISKS_SUMMARY",
                                 refresh=True, processor=result_processor)
        
    
    # return the definition ids for a given group
    def get_sd_ids(self, group_id):
        ids = [ g_sd.def_id for g_sd in self.group_sds \
                if g_sd.group_id == group_id ]
        return ids

    def get_pool_stats(self, group_id):
        # return summarize information about storage associated
        # with a group.
        # For each type, return TOTAL, AVAILABLE
        sds = self.get_sds(group_id)
        g_stats = {}
        total = 0
        for sd in sds:
            stat = sd.get_stats()
            if stat and stat.get("TOTAL"):
                total += stat.get("TOTAL")
        g_stats["TOTAL"] = total
        return g_stats

    def get_sd(self, id):
        return self.defs.get(id)
        
    # add a storage definition to a group.
    def add_sd(self, sd, group):
        name = sd.name
        for d in self.defs.itervalues():
            if d.name == name:
                if d.id in self.get_sd_ids(group.id):
                    raise Exception("Storage definition with the same name exist, in this Server Pool")
        self.defs[sd.id] = sd
        self.group_sds.append(GSInfo(group.id, sd.id))
        self.save_sds()
        self._save_group_sds()
        self.on_add_sd(sd, group)

    # a new storage definition got added
    def on_add_sd(self, sd, group):
        # kick of client connection and setup from all nodes in a
        # group
        for node in group.getNodeList().itervalues():
            self.sync_node_sd(node, group.id, sd, op="ATTACH", refresh=True)
                

    def update_sd(self, sd, group):
        if not self.defs.get(sd.id):
            raise Exception("sd with id %s , name %s does not exist" % (sd.id,
                                                                        sd.name))
        self.defs[sd.id] = sd
        self.save_sds()
        self.on_update_sd(sd, group) # resync

    def on_update_sd(self,sd, group):
        self.on_add_sd(sd, group) # assumes that old definition does not need to be
                               # removed.

    def remove_sd(self, sd, group):
        def_id = sd.id
        group_id = group.id

        if self.defs.get(def_id):
            del self.defs[def_id]
            self.save_sds()
            self.on_sd_removed(sd, group)

    def on_sd_removed(self, sd, group):
        for node in group.getNodeList().itervalues():
            self.sync_node_sd(node, group.id, sd, op="DETACH", refresh=True)
        for g_sd in self.group_sds:
            if g_sd.def_id == sd.id:
                self.group_sds.remove(g_sd)
            
        for n_sd in self.node_sds:
            if n_sd.def_id == sd.id:
                self.node_sds.remove(n_sd)

        self._save_group_sds()
        self._save_node_sds()


    # read groups x storage definition map
    def _read_group_sds(self):
        self.group_sds = []
        grp_sds = self.store.get(XMConfig.APP_DATA,
                                       prop_group_sds)

        group_sd_info = []
        if grp_sds:
            group_sd_info = eval(grp_sds)
            
        for g_sdi in group_sd_info:
            g_sd = GSInfo(g_sdi["group_id"],
                          g_sdi["def_id"],
                          g_sdi["status"],
                          g_sdi["oos_count"],
                          g_sdi["dt_time"])

            
            self.group_sds.append(g_sd)

    def _save_group_sds(self):
        self.store.set(XMConfig.APP_DATA, prop_group_sds, str(self.group_sds))

    def dump_group_sds(self):
        for g_sd in self.group_sds:
            g_sd.dump()
        
    # read nodes x storage definition status
    def _read_node_sds(self):
        self.node_sds = []
        n_sd = self.store.get(XMConfig.APP_DATA,
                                       prop_node_sds)
        node_sd_info = []
        if n_sd:
            node_sd_info = eval(n_sd)
            
        for n_sdi in node_sd_info:
            n_sd = NSInfo(n_sdi["node_id"],
                          n_sdi["group_id"],
                          n_sdi["def_id"],
                          n_sdi["status"],
                          n_sdi["dt_time"],
                          n_sdi["details"])
            
            self.node_sds.append(n_sd)

    def _save_node_sds(self):
        self.store.set(XMConfig.APP_DATA, prop_node_sds, str(self.node_sds))
        
    def dump_node_sds(self):
        for n_sd in self.node_sds:
            n_sd.dump()

        

    # update/add the node x storage defintion record.
    # also rollup the group x storage definition status.
    def update_nsd(self, nsd):
        updated = False
        oos_count = 0 # out of sync count
        for n in self.node_sds:
            if n.node_id == nsd.node_id and n.group_id == nsd.group_id and \
                               n.def_id == nsd.def_id:

                n.status = nsd.status
                n.dt_time = nsd.dt_time
                n.details = nsd.details
                self._save_node_sds()
                updated = True

            # gather the out of sync ops, used for updating group status
            if  n.group_id == nsd.group_id and \
                               n.def_id == nsd.def_id and n.status != IN_SYNC:
                oos_count += 1



        if not updated:    
            # did not find the match
            self.node_sds.append(nsd)
            if nsd.status != IN_SYNC:
                oos_count += 1
            self._save_node_sds()

        # update the corresponding GSInfo
        if oos_count > 0 :
            g_status = OUT_OF_SYNC
        else:
            g_status = IN_SYNC

        self.update_group_sd(nsd.group_id, nsd.def_id, oos_count, g_status)

        
    def update_group_sd(self, group_id, def_id, oos_count = 0, g_status =None):
        if g_status is None:
            oos_count = 0
            for n in self.node_sds:
                # gather the out of sync ops, used for updating group status
                if  n.group_id == group_id and \
                                   n.def_id == def_id and n.status != IN_SYNC:
                    oos_count += 1

        
        for g in self.group_sds:
            if g.group_id == group_id and g.def_id == def_id:
                g.status = g_status
                g.dt_time = ct_time()
                self._save_group_sds()
                return
            
        self.group_sds.append(GSInfo(group_id, sd, def_id, g_status,
                                     oos_count))
        self._save_group_sds()

    # when a group is deleted.
    def on_delte_group(self, group):
        # cleanup any state from group
        group_id = group.id
        modified = False
        for gsd in self.group_sds:
            if gsd.group_id == group_id:
                self.group_sds.remove(gsd)
                modified = True

        if modified:
            self._save_group_sds()

    # when a new group is added
    def on_add_group(self, group_id):
        # add tracking entry : we can be lazy...
        pass

    # when a node is removed from a group
    def on_remove_node(self, node, group):
        # node is gettig removed from the group
        # remove all storage definitions for this group
        resulte = []
        for nsd in self.node_sds:
            if nsd.node_id == node.hostname and \
                                 nsd.group_id == group.id:
                sd = sef.defs[nsd.def_id]
                # Kick of task to remove SD from the node.
                self.sync_node_sd(node,group.id, sd, op="DETACH")
                self.node_sds.remove(sd)
                self._save_node_sds()
                self.update_group_sd(group.id, sd.id)
        return results

                 
                 
    # when a new node is getting added to the group.         
    def on_add_node(self, node, group):
        # node is gettig added
        # add all storage definitions from this group
        resulte = []
        for gsd in self.group_sds:
            if gsd.group_id == group.group_id:
                sd = sef.defs[gsd.def_id]
                self.sync_node_sd(node,group.id, sd, op="ATTATCH",
                                  refresh=True)
                 
        return results

    # we can go fine grained, but not expecting too many files here
    # just copy the whole tree..
    def prepare_scripts(self, dest_node, type):
        copyToRemote(self.s_src_scripts_location, dest_node,
                     self.s_scripts_location)
        common_function_script_name = os.path.join(self.s_src_scripts_location,"storage_functions")
        dest_location = os.path.join(self.s_scripts_location,"scripts")
        copyToRemote(common_function_script_name, dest_node,
                     dest_location, "storage_functions")

    def props_to_cmd_param(self, props):
        cp = ""
        for p,v in props.iteritems():
            if v :
                if cp:
                    cp += "|"
                cp += "%s=%s" % (p,v)
        cp = "'%s'" % (cp, )
        return cp


    def parse_output(self,output, result):
       Lista = []
       for i in output.splitlines():
           d={}
           if not i:
               continue
           i = i.strip()
           
           if i.find("OUTPUT") !=0:
                   continue
           for j in i.split('|'):
               nameAndValue = j.lstrip().split('=')
               d[nameAndValue[0]]= nameAndValue[1]
           del d['OUTPUT']
           Lista.append(d)
       return Lista

    def parse_summary(self,output, result):
       for i in output.splitlines():
           d={}
           if not i:
               continue
           i = i.strip()
           
           if i.find("SUMMARY") !=0:
                   continue
           for j in i.split('|'):
               nameAndValue = j.lstrip().split('=')
               d[nameAndValue[0]]= nameAndValue[1]
           del d['SUMMARY']
           return d
       return None


    # storage processor
    def nfs_processor(self, op, output, result):
        print "nfs processor called with \n", output
        if op == "GET_DISKS_SUMMARY":
            result["SUMMARY"] = self.parse_summary(output,result)
        else:
            result["DETAILS"] = self.parse_output(output,result)
            result["SUMMARY"] = self.parse_summary(output,result)
            
    def iscsi_processor(self, op, output, result):
        print "iscsi processor called with \n", output
        if op == "GET_DISKS_SUMMARY":
            result["SUMMARY"] = self.parse_summary(output,result)
        else:
           result["DETAILS"] = self.parse_output(output,result)
           result["SUMMARY"] = self.parse_summary(output,result)


    def aoe_processor(self, op, output, result):
        print "aoe processor called with \n", output
        if op == "GET_DISKS_SUMMARY":
            result["SUMMARY"] = self.parse_summary(output,result)
        else:
           result["DETAILS"] = self.parse_output(output,result)
           result["SUMMARY"] = self.parse_summary(output,result)


    # Sync the storage definition with the node
    # update the NSD record.
    # update the GSD record.
    # return code, output, structured output
    
    # op = GET_DISKS : connect refresh the exported volumes and
    #                return them
    # op = DETACH    : Detach the shared storage from the pool
    #
    # op = ATTACH : Just attach : No need to fetch volumes
    #
    def sync_node_sd(self, node, group_id, sd, op="GET_DISKS", refresh=True,
                     processor = None):
        # for now as we are not doing syncup
        if not (op in ["GET_DISKS", "GET_DISKS_SUMMARY"] ): 
            return 
        type = sd.type
        self.prepare_scripts(node, type)
        script_name = os.path.join(self.s_scripts_location,"scripts",
                                   type, "storage.sh")
        
        log_dir = node.config.get(XMConfig.PATHS,
                                   prop_log_dir)
        if log_dir is None or log_dir == '':
            log_dir = DEFAULT_LOG_DIR


        log_filename = os.path.join(log_dir, "storage/scripts",
                                     type, "storage_sh.log")

        # create the directory for log files
        mkdir2(node,os.path.dirname(log_filename))

        cp = self.props_to_cmd_param(sd.connection_props)
        creds_str = self.props_to_cmd_param(sd.creds)

        # help script find its location
        script_loc = os.path.join(self.s_scripts_location,"scripts")

         
        script_args= " -t " + type + \
                     " -c " + cp + \
                     " -p " + creds_str + \
                     " -o " + op + \
                     " -s " + script_loc + \
                     " -l " + log_filename  
        
        cmd = script_name +  script_args


        # execute the script
        if op in ["GET_DISKS", "GET_DISKS_SUMMARY"] : # for now as we are not doing syncup
            print "executing ",cmd
            (output, exit_code) = node.node_proxy.exec_cmd(cmd)
        else:
            output = "Success"
            exit_code = 0
        
        print "EXIT CODE:", exit_code 
        if exit_code != 0:
            raise Exception(output)

        if exit_code:
            status = OUT_OF_SYNC
        else:
            status = IN_SYNC
            
        dt_time =  ct_time() # convirt TZ for now.
        details= "%d:%s" % (exit_code, output)

        if not op in [ "GET_DISKS", "GET_DISKS_SUMMARY"] :        
            # add new nsd record # update or add record.
            self.update_nsd(NSInfo(node.hostname,
                                   group_id,
                                   sd.id,
                                   status,
                                   dt_time,
                                   details))
            if exit_code != 0:
                raise Exception(details)
            else:
                return { "output" :output,
                         "exit_code" : exit_code }
        else:
            # lets process the output in to corresponding
            # details structure
            result = {}
            result["type"] = sd.type
            result["id"] = sd.id
            result["op"] = op
            result["name"] = sd.name
            
            # some more common things here
            if processor:
                processor(op,output, result)
                return result
            else:
                raise Exception("Can not process output. %s" % op )


# Test          
if __name__ == '__main__':
    from ManagedNode import ManagedNode
    from Groups import ServerGroup
    local_node = ManagedNode(hostname = LOCALHOST,
                             isRemote = False,
                             helper = None) #creds_helper)
    store = local_node.config
    s_manager = StorageManager(store)
    server = "192.168.12.104"
    nfs_props = {"server" : server,
                 "share" : "/mnt/share",
                 "mount_point" : "/mnt/share",
                 "mount_options" : "rw, root_squash"
                 }
    iscsi_props = {"server" : server,
                   "target" : "shared_104:store"
                   }
    creds = { "username": "jd",
              "password" : "jd_scsi"
              }
    
    aoe_props = {}
    
    nfs_sd = StorageDef(None, "ISO Store", NFS, "Storage for iso", nfs_props,
                        creds_required = False)
    iscsi_sd = StorageDef(None,"SCSI Target", iSCSI, "SCSI Store", iscsi_props,
                          creds_required = True)
    iscsi_sd.set_creds(creds)
    
    aoe_sd = StorageDef(None,"AOE Target", AOE, "AOE Exports", aoe_props,
                        creds_required = False)

    group = ServerGroup("TestGroup", {'local_node': local_node})
    s_manager.add_sd(nfs_sd, group)
#   s_manager.add_sd(iscsi_sd, group)
#   s_manager.add_sd(aoe_sd, group)

    defs = s_manager.get_sds(group.id)
    for d in defs:
        print d

    s_manager.dump_group_sds()
    s_manager.dump_node_sds()

    #s_manager.remove_sd(aoe_sd,group)
    #nfs_sd.connection_props["mount_point"] = "/mnt/foobar"
    #s_manager.update_sd(nfs_sd, group)
    

    print "========== after update nfs and delete aoe ======"
    defs = s_manager.get_sds(group.id)
    for d in defs:
        print d

    s_manager.dump_group_sds()
    s_manager.dump_node_sds()

    defs = s_manager.get_sds(group.id)
    sd = defs[0]
    disk_details = s_manager.get_sd_details(sd, local_node, group)
    pprint.pprint(disk_details)
           
                 
                              
             
                              
             
         
         

