# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
# 
#   http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
'''
Created on May 17, 2011

'''
from OvmCommonModule import *
from OvmDiskModule import *
from OvmVifModule import *
from OvmHostModule import OvmHost
from string import Template
from OVSXXenVMConfig import *
from OVSSiteVM import start_vm, stop_vm, reset_vm
from OVSSiteCluster import *
from OvmStoragePoolModule import OvmStoragePool
from OVSXXenStore import xen_get_vm_path, xen_get_vnc_port
from OVSDB import db_get_vm
from OVSXMonitor import xen_get_vm_perf_metrics, xen_get_xm_info
from OVSXXenVM import xen_migrate_vm
from OVSSiteRMVM import unregister_vm, register_vm, set_vm_status
from OVSSiteVMInstall import install_vm_hvm
from OVSSiteRMServer import get_master_ip
from OVSXXenVMInstall import xen_change_vm_cdrom
from OVSXAPIUtil import XenAPIObject, session_login, session_logout


logger = OvmLogger("OvmVm")

class OvmVmDecoder(json.JSONDecoder):
    def decode(self, jStr):
        deDict = asciiLoads(jStr)
        vm = OvmVm()
        setAttrFromDict(vm, 'cpuNum', deDict, int)
        setAttrFromDict(vm, 'memory', deDict, long)
        setattr(vm, 'rootDisk', toOvmDisk(deDict['rootDisk']))
        setattr(vm, 'vifs', toOvmVifList(deDict['vifs']))
        setattr(vm, 'disks', toOvmDiskList(deDict['disks']))
        setAttrFromDict(vm, 'name', deDict)
        setAttrFromDict(vm, 'uuid', deDict)
        setAttrFromDict(vm, 'bootDev', deDict)
        setAttrFromDict(vm, 'type', deDict)
        return vm

class OvmVmEncoder(json.JSONEncoder):
    def default(self, obj):
        if not isinstance(obj, OvmVm): raise Exception("%s is not instance of OvmVm"%type(obj))
        dct = {}
        safeDictSet(obj, dct, 'cpuNum')
        safeDictSet(obj, dct, 'memory')
        safeDictSet(obj, dct, 'powerState')
        safeDictSet(obj, dct, 'name')
        safeDictSet(obj, dct, 'type')
        vifs = fromOvmVifList(obj.vifs)
        dct['vifs'] = vifs
        rootDisk = fromOvmDisk(obj.rootDisk)
        dct['rootDisk'] = rootDisk
        disks = fromOvmDiskList(obj.disks)
        dct['disks'] = disks
        return dct
        
def toOvmVm(jStr):
    return json.loads(jStr, cls=OvmVmDecoder)

def fromOvmVm(vm):
    return normalizeToGson(json.dumps(vm, cls=OvmVmEncoder))

class OvmVm(OvmObject):
    cpuNum = 0
    memory = 0
    rootDisk = None
    vifs = []
    disks = []
    powerState = ''
    name = ''
    bootDev = ''
    type = ''
        
    def _getVifs(self, vmName):
        vmPath = OvmHost()._vmNameToPath(vmName)
        domId = OvmHost()._getDomainIdByName(vmName)
        vifs = successToMap(xen_get_vifs(vmPath))
        lst = []
        for k in vifs:
            v = vifs[k]
            vifName = 'vif' + domId + '.' + k[len('vif'):]
            vif = OvmVif()
            (mac, bridge, type) = v.split(',')
            safeSetAttr(vif, 'name', vifName)
            safeSetAttr(vif, 'mac', mac)
            safeSetAttr(vif, 'bridge', bridge)
            safeSetAttr(vif, 'type', type)
            lst.append(vif)
            
        return lst
    
    def _getVifsFromConfig(self, vmPath):
        vifs = successToMap(xen_get_vifs(vmPath))
        lst = []
        for k in vifs:
            v = vifs[k]
            vif = OvmVif()
            (mac, bridge, type) = v.split(',')
            safeSetAttr(vif, 'name', k)
            safeSetAttr(vif, 'mac', mac)
            safeSetAttr(vif, 'bridge', bridge)
            safeSetAttr(vif, 'type', type)
            lst.append(vif)
        return lst
    
    def _getIsoMountPath(self, vmPath):
        vmName = basename(vmPath)
        priStoragePath = vmPath.rstrip(join('running_pool', vmName))
        return join(priStoragePath, 'iso_pool', vmName)
    
    def _getVmTypeFromConfigFile(self, vmPath):
        vmType = successToMap(xen_get_vm_type(vmPath))['type']
        return vmType.replace('hvm', 'HVM').replace('para', 'PV')
    
    def _tapAOwnerFile(self, vmPath):
        # Create a file with name convention 'host_ip_address' in vmPath
        # Because xm list doesn't return vm that has been stopped, we scan
        # primary storage for stopped vm. This file tells us which host it belongs
        # to. The file is used in OvmHost.getAllVms()
        self._cleanUpOwnerFile(vmPath)
        ownerFileName = makeOwnerFileName()
        fd = open(join(vmPath, ownerFileName), 'w')
        fd.write(ownerFileName)
        fd.close()
    
    def _cleanUpOwnerFile(self, vmPath):
        for f in os.listdir(vmPath):
            fp = join(vmPath, f)
            if isfile(fp) and f.startswith(OWNER_FILE_PREFIX):
                os.remove(fp)
    
    @staticmethod
    def create(jsonString):    
        def dumpCfg(vmName, cfgPath):
            cfgFd = open(cfgPath, 'r')
            cfg = cfgFd.readlines()
            cfgFd.close()
            logger.info(OvmVm.create, "Start %s with configure:\n\n%s\n"%(vmName, "".join(cfg)))
        
        def setVifsType(vifs, type):
            for vif in vifs:
                vif.type = type
                
        def hddBoot(vm, vmPath):
            vmType = vm.type
            if vmType == "FROMCONFIGFILE":
                vmType = OvmVm()._getVmTypeFromConfigFile(vmPath)
                
            cfgDict = {}
            if vmType == "HVM":
                cfgDict['builder'] = "'hvm'"
                cfgDict['acpi'] = "1"
                cfgDict['apic'] = "1"
                cfgDict['device_model'] = "'/usr/lib/xen/bin/qemu-dm'"
                cfgDict['kernel'] = "'/usr/lib/xen/boot/hvmloader'"
                vifType = 'ioemu'
            else:
                cfgDict['bootloader'] = "'/usr/bin/pygrub'"
                vifType = 'netfront'
            
            cfgDict['name'] = "'%s'"%vm.name
            cfgDict['disk'] = "[]"
            cfgDict['vcpus'] = "''"
            cfgDict['memory'] = "''"
            cfgDict['on_crash'] = "'destroy'"
            cfgDict['on_reboot'] = "'restart'"
            cfgDict['vif'] = "[]"
            
            items = []
            for k in cfgDict.keys():
                item = " = ".join([k, cfgDict[k]])
                items.append(item)
            vmSpec = "\n".join(items)
                
            vmCfg = open(join(vmPath, 'vm.cfg'), 'w')
            vmCfg.write(vmSpec)
            vmCfg.close()
            
            setVifsType(vm.vifs, vifType)
            raiseExceptionIfFail(xen_set_vcpus(vmPath, vm.cpuNum))
            raiseExceptionIfFail(xen_set_memory(vmPath, BytesToM(vm.memory)))
            raiseExceptionIfFail(xen_add_disk(vmPath, vm.rootDisk.path, mode=vm.rootDisk.type))
            vifs = [OvmVif.toXenString(v) for v in vm.vifs]
            for vif in vifs:
                raiseExceptionIfFail(xen_set_vifs(vmPath, vif))
                
            for disk in vm.disks:
                raiseExceptionIfFail(xen_add_disk(vmPath, disk.path, mode=disk.type))

            raiseExceptionIfFail(xen_set_vm_vnc_password(vmPath, ""))
            cfgFile = join(vmPath, 'vm.cfg')
            # only HVM supports attaching cdrom
            if vmType == 'HVM':
                # Add an empty "hdc:cdrom" entry in config. Fisrt we set boot order to 'd' that is cdrom boot,
                # then 'hdc:cdrom' entry will be in disk list. Second, change boot order to 'c' which
                # is harddisk boot. VM can not start with an empty 'hdc:cdrom' when boot order is 'd'.
                # it's tricky !
                raiseExceptionIfFail(xen_config_boot_sequence(vmPath, 'd'))
                raiseExceptionIfFail(xen_config_boot_sequence(vmPath, 'c'))
                
            raiseExceptionIfFail(xen_correct_cfg(cfgFile, vmPath))
            xen_correct_qos_cfg(cfgFile)
            dumpCfg(vm.name, cfgFile)
            server = successToMap(get_master_ip())['ip']
            raiseExceptionIfFail(start_vm(vmPath, server))
            rs = SUCC()
            return rs
        
        def cdBoot(vm, vmPath):
            isoMountPath = None
            try:
                cdrom = None
                for disk in vm.disks:
                    if disk.isIso == True:
                        cdrom = disk
                        break
                if not cdrom: raise Exception("Cannot find Iso in disks")
                
                isoOnSecStorage = dirname(cdrom.path)
                isoName = basename(cdrom.path)
                isoMountPath = OvmVm()._getIsoMountPath(vmPath)
                OvmStoragePool()._mount(isoOnSecStorage, isoMountPath)
                isoPath = join(isoMountPath, isoName)
                if not exists(isoPath):
                    raise Exception("Cannot found iso %s at %s which mounts to %s"%(isoName, isoOnSecStorage, isoMountPath))
                
                stdout = run_cmd(args=['file', isoPath])
                if not stdout.strip().endswith("(bootable)"): raise Exception("ISO %s is not bootable"%cdrom.path)
                
                #now alter cdrom to correct path
                cdrom.path = isoPath
                if len(vm.vifs) != 0:
                    vif = vm.vifs[0]
                    #ISO boot must be HVM
                    vifCfg = ','.join([vif.mac, vif.bridge, 'ioemu'])
                else:
                    vifCfg = ''
                
                rootDiskSize = os.path.getsize(vm.rootDisk.path)
                rooDiskCfg = ':'.join([join(vmPath, basename(vm.rootDisk.path)), str(BytesToG(rootDiskSize)), 'True'])
                disks = [rooDiskCfg]
                for d in vm.disks:
                    if d.isIso: continue
                    size = os.path.getsize(d.path)
                    cfg = ':'.join([d.path, str(BytesToG(size)), 'True'])
                    disks.append(cfg)
                disksCfg = ','.join(disks)
                server = successToMap(get_master_ip())['ip']
                   
                raiseExceptionIfFail(install_vm_hvm(vmPath, BytesToM(vm.memory), vm.cpuNum, vifCfg, disksCfg, cdrom.path, vncpassword='', dedicated_server=server))
                rs = SUCC()
                return rs
            except Exception, e:
                if isoMountPath and OvmStoragePool()._isMounted(isoMountPath):
                    doCmd(['umount', '-f', isoMountPath])
                errmsg = fmt_err_msg(e)
                raise Exception(errmsg)
        
        try:
            vm = toOvmVm(jsonString)
            logger.debug(OvmVm.create, "creating vm, spec:%s"%jsonString)
            rootDiskPath = vm.rootDisk.path
            if not exists(rootDiskPath): raise Exception("Cannot find root disk %s"%rootDiskPath)
    
            rootDiskDir = dirname(rootDiskPath)
            vmPath = join(dirname(rootDiskDir), vm.name)
            if not exists(vmPath):
                doCmd(['ln', '-s', rootDiskDir, vmPath])
            vmNameFile = open(join(rootDiskDir, 'vmName'), 'w')
            vmNameFile.write(vm.name)
            vmNameFile.close()
            
            OvmVm()._tapAOwnerFile(rootDiskDir)
            # set the VM to DOWN before starting, OVS agent will check this status
            set_vm_status(vmPath, 'DOWN')
            if vm.bootDev == "HDD":
                return hddBoot(vm, vmPath)
            elif vm.bootDev == "CD":
                return cdBoot(vm, vmPath)
            else:
                raise Exception("Unkown bootdev %s for %s"%(vm.bootDev, vm.name))

        except Exception, e:
            errmsg = fmt_err_msg(e)
            logger.error(OvmVm.create, errmsg)
            raise XmlRpcFault(toErrCode(OvmVm, OvmVm.create), errmsg)
    
    @staticmethod
    def stop(vmName):
        try:
            try:
                OvmHost()._getDomainIdByName(vmName)
            except NoVmFoundException, e:
                logger.info(OvmVm.stop, "vm %s is already stopped"%vmName)
                return SUCC()
                
            logger.info(OvmVm.stop, "Stop vm %s"%vmName)
            try:
                vmPath = OvmHost()._vmNameToPath(vmName)
            except Exception, e:
                errmsg = fmt_err_msg(e)
                logger.info(OvmVm.stop, "Cannot find link for vm %s on primary storage, treating it as stopped\n %s"%(vmName, errmsg))
                return SUCC()
            # set the VM to RUNNING before stopping, OVS agent will check this status
            set_vm_status(vmPath, 'RUNNING')
            raiseExceptionIfFail(stop_vm(vmPath))
            return SUCC()
        except Exception, e:
            errmsg = fmt_err_msg(e)
            logger.error(OvmVm.stop, errmsg)
            raise XmlRpcFault(toErrCode(OvmVm, OvmVm.stop), errmsg)
    
    @staticmethod
    def reboot(vmName):
        try:
            #===================================================================
            # Xend has a bug of reboot. If reboot vm too quick, xend return success
            # but actually it refused reboot (seen from log)
            # vmPath = successToMap(xen_get_vm_path(vmName))['path']
            # raiseExceptionIfFail(reset_vm(vmPath))
            #===================================================================
            vmPath = OvmHost()._vmNameToPath(vmName)
            OvmVm.stop(vmName)
            raiseExceptionIfFail(start_vm(vmPath))
            vncPort= successToMap(xen_get_vnc_port(vmName))['vnc_port']
            logger.info(OvmVm.stop, "reboot vm %s, new vncPort is %s"%(vmName, vncPort))
            return toGson({"vncPort":str(vncPort)})
        except Exception, e:
            errmsg = fmt_err_msg(e)
            logger.error(OvmVm.reboot, errmsg)
            raise XmlRpcFault(toErrCode(OvmVm, OvmVm.reboot), errmsg)
    
    @staticmethod
    def getDetails(vmName):          
        try:
            vm = OvmVm()
            
            try:
                OvmHost()._getDomainIdByName(vmName)
                vmPath = OvmHost()._vmNameToPath(vmName)
                vifsFromConfig = False
            except NoVmFoundException, e:
                vmPath = OvmHost()._getVmPathFromPrimaryStorage(vmName)
                vifsFromConfig = True
                
            
            if not isdir(vmPath):
                # The case is, when vm starting was not completed at primaryStroageDownload or createVolume(e.g. mgmt server stop), the mgmt
                # server will keep vm state in staring, then a stop command will be sent. The stop command will delete bridges that vm attaches,
                # by retriving birdge info by OvmVm.getDetails(). In this case, the vm doesn't exists, so returns a fake object here.
                fakeDisk = OvmDisk()
                vm.rootDisk = fakeDisk
            else:
                if vifsFromConfig:
                    vm.vifs.extend(vm._getVifsFromConfig(vmPath))
                else:
                    vm.vifs.extend(vm._getVifs(vmName))
                    
                safeSetAttr(vm, 'name', vmName)
                disks = successToMap(xen_get_vdisks(vmPath))['vdisks'].split(',')
                rootDisk = None
                #BUG: there is no way to get type of disk, assume all are "w"
                for d in disks:
                    if vmName in d:
                        rootDisk = OvmDisk()
                        safeSetAttr(rootDisk, 'path', d)
                        safeSetAttr(rootDisk, 'type', "w")
                        continue
                    disk = OvmDisk()
                    safeSetAttr(disk, 'path', d)
                    safeSetAttr(disk, 'type', "w")
                    vm.disks.append(disk)
                if not rootDisk: raise Exception("Cannot find root disk for vm %s"%vmName)
                safeSetAttr(vm, 'rootDisk', rootDisk)
                vcpus = int(successToMap(xen_get_vcpus(vmPath))['vcpus'])
                safeSetAttr(vm, 'cpuNum', vcpus)
                memory = MtoBytes(int(successToMap(xen_get_memory(vmPath))['memory']))
                safeSetAttr(vm, 'memory', memory)
                vmStatus = db_get_vm(vmPath)
                safeSetAttr(vm, 'powerState',  vmStatus['status'])
                vmType = successToMap(xen_get_vm_type(vmPath))['type'].replace('hvm', 'HVM').replace('para', 'PV')
                safeSetAttr(vm, 'type', vmType)
                
            rs = fromOvmVm(vm)
            logger.info(OvmVm.getDetails, rs)
            return rs
        except Exception, e:
            errmsg = fmt_err_msg(e)
            logger.error(OvmVm.getDetails, errmsg)
            raise XmlRpcFault(toErrCode(OvmVm, OvmVm.getDetails), errmsg)
    
    @staticmethod
    def getVmStats(vmName):
        def getVcpuNumAndUtils():
            try:
                session = session_login()
                refs = session.xenapi.VM.get_by_name_label(vmName)
                if len(refs) == 0:
                    raise Exception("No ref for %s found in xenapi VM objects"%vmName)
                vm = XenAPIObject('VM', session, refs[0])
                VM_metrics = XenAPIObject("VM_metrics", session, vm.get_metrics())
                items = VM_metrics.get_VCPUs_utilisation().items()
                nvCpus = len(items)
                if nvCpus == 0:
                    raise Exception("vm %s has 0 vcpus !!!"%vmName)
                
                xmInfo = successToMap(xen_get_xm_info())
                nCpus = int(xmInfo['nr_cpus'])
                totalUtils = 0.0
                # CPU utlization of VM = (total cpu utilization of each vcpu) / number of physical cpu
                for num, util in items:
                    totalUtils += float(util)
                avgUtils = float(totalUtils/nCpus) * 100
                return (nvCpus, avgUtils)
            finally:
                session_logout()
                
                
        try:
            try:
                OvmHost()._getDomainIdByName(vmName)
                vmPath = OvmHost()._vmNameToPath(vmName)
                (nvcpus, avgUtils) = getVcpuNumAndUtils()
                vifs = successToMap(xen_get_vifs(vmPath))
                rxBytes = 0
                txBytes = 0
                vifs = OvmVm()._getVifs(vmName)
                for vif in vifs:
                    rxp = join('/sys/class/net', vif.name, 'statistics/rx_bytes')
                    txp = join("/sys/class/net/", vif.name, "statistics/tx_bytes")
                    if not exists(rxp): raise Exception('can not find %s'%rxp)
                    if not exists(txp): raise Exception('can not find %s'%txp)
                    rxBytes += long(doCmd(['cat', rxp])) / 1000
                    txBytes += long(doCmd(['cat', txp])) / 1000
            except NoVmFoundException, e:
                vmPath = OvmHost()._getVmPathFromPrimaryStorage(vmName)
                nvcpus = int(successToMap(xen_get_vcpus(vmPath))['vcpus'])
                avgUtils = 0
                rxBytes = 0
                txBytes = 0
            
            rs = toGson({"cpuNum":nvcpus, "cpuUtil":avgUtils, "rxBytes":rxBytes, "txBytes":txBytes})
            logger.debug(OvmVm.getVmStats, rs)
            return rs           
        except Exception, e:
            errmsg = fmt_err_msg(e)
            logger.error(OvmVm.getVmStats, errmsg)
            raise XmlRpcFault(toErrCode(OvmVm, OvmVm.getVmStats), errmsg)
    
    @staticmethod
    def migrate(vmName, targetHost):
        try:
            vmPath = OvmHost()._vmNameToPath(vmName)
            raiseExceptionIfFail(xen_migrate_vm(vmPath, targetHost))
            unregister_vm(vmPath)
            OvmVm()._cleanUpOwnerFile(vmPath)
            return SUCC()
        except Exception, e:
            errmsg = fmt_err_msg(e)
            logger.error(OvmVm.migrate, errmsg)
            raise XmlRpcFault(toErrCode(OvmVm, OvmVm.migrate), errmsg)
    
    @staticmethod
    def register(vmName):
        try:
            vmPath = OvmHost()._vmNameToPath(vmName)
            raiseExceptionIfFail(register_vm(vmPath))
            OvmVm()._tapAOwnerFile(vmPath)
            vncPort= successToMap(xen_get_vnc_port(vmName))['vnc_port']
            rs = toGson({"vncPort":str(vncPort)})
            logger.debug(OvmVm.register, rs)
            return rs
        except Exception, e:
            errmsg = fmt_err_msg(e)
            logger.error(OvmVm.register, errmsg)
            raise XmlRpcFault(toErrCode(OvmVm, OvmVm.register), errmsg)
    
    @staticmethod
    def getVncPort(vmName):
        try:
            vncPort= successToMap(xen_get_vnc_port(vmName))['vnc_port']
            rs = toGson({"vncPort":vncPort})
            logger.debug(OvmVm.getVncPort, rs)
            return rs
        except Exception, e:
            errmsg = fmt_err_msg(e)
            logger.error(OvmVm.getVncPort, errmsg)
            raise XmlRpcFault(toErrCode(OvmVm, OvmVm.getVncPort), errmsg)
        
    @staticmethod
    def detachOrAttachIso(vmName, iso, isAttach):
        try:
            if vmName in OvmHost.getAllVms():
                scope = 'both'
                vmPath = OvmHost()._vmNameToPath(vmName)
            else:
                scope = 'cfg'
                vmPath = OvmHost()._getVmPathFromPrimaryStorage(vmName)
            
            vmType = OvmVm()._getVmTypeFromConfigFile(vmPath)
            if vmType != 'HVM':
                raise Exception("Only HVM supports attaching/detaching ISO")
            
            if not isAttach:
                iso = ''
            else:
                isoName = basename(iso)
                isoMountPoint = OvmVm()._getIsoMountPath(vmPath)
                isoOnSecStorage = dirname(iso)
                OvmStoragePool()._mount(isoOnSecStorage, isoMountPoint)
                iso = join(isoMountPoint, isoName)
                       
            exceptionIfNoSuccess(xen_change_vm_cdrom(vmPath, iso, scope))
            return SUCC()
        except Exception, e:
            errmsg = fmt_err_msg(e)
            logger.error(OvmVm.detachOrAttachIso, errmsg)
            raise XmlRpcFault(toErrCode(OvmVm, OvmVm.detachOrAttachIso), errmsg)
        
if __name__ == "__main__":
    import sys
    print OvmVm.getDetails(sys.argv[1])
    #print OvmVm.getVmStats(sys.argv[1])