
from __future__ import absolute_import

import os
import logging

from vdsm import constants
from vdsm import utils
from vdsm.common import fileutils
from vdsm.common.marks import deprecated
from vdsm.common.threadlocal import vars
from vdsm.config import config
from vdsm.storage import blockdev
from vdsm.storage import constants as sc
from vdsm.storage import exception as se
from vdsm.storage import lvm
from vdsm.storage import qemuimg
from vdsm.storage import resourceManager as rm
from vdsm.storage import task
from vdsm.storage import volume
from vdsm.storage.sdc import sdCache
from vdsm.storage.volumemetadata import VolumeMetadata


QCOW_OVERHEAD_FACTOR = 1.1

MIN_PADDING = constants.MEGAB

log = logging.getLogger('storage.Volume')


class BlockVolumeManifest(volume.VolumeManifest):

    DISK_TYPE = "block"

    align_size = sc.VG_EXTENT_SIZE_MB * constants.MEGAB

    def __init__(self, repoPath, sdUUID, imgUUID, volUUID):
        volume.VolumeManifest.__init__(self, repoPath, sdUUID, imgUUID,
                                       volUUID)
        self.lvmActivationNamespace = rm.getNamespace(
            sc.LVM_ACTIVATION_NAMESPACE, self.sdUUID)

    @classmethod
    def is_block(cls):
        return True

    def chunked(self):
        return self.getFormat() == sc.COW_FORMAT

    def getMetadataId(self):
        return (self.sdUUID, self.getMetaSlot())

    def getMetaSlot(self):
        try:
            md = getVolumeTag(self.sdUUID, self.volUUID, sc.TAG_PREFIX_MD)
        except se.MissingTagOnLogicalVolume:
            self.log.error("missing offset tag on volume %s/%s",
                           self.sdUUID, self.volUUID, exc_info=True)
            raise se.VolumeMetadataReadError(
                "missing offset tag on volume %s/%s" %
                (self.sdUUID, self.volUUID))
        else:
            return int(md)

    def getMetadata(self, metaId=None):
        if not metaId:
            metaId = self.getMetadataId()

        _, slot = metaId
        sd = sdCache.produce_manifest(self.sdUUID)
        try:
            lines = sd.read_metadata_block(slot).splitlines()
        except Exception as e:
            self.log.error(e, exc_info=True)
            raise se.VolumeMetadataReadError("%s: %s" % (metaId, e))

        md = VolumeMetadata.from_lines(lines)
        return md

    def validateImagePath(self):
        manifest = sdCache.produce_manifest(self.sdUUID)
        imageDir = manifest.getImageDir(self.imgUUID)

        if os.path.islink(imageDir) and not os.path.exists(imageDir):
            self.log.warning("Removing stale image directory link %r",
                             imageDir)
            os.unlink(imageDir)

        if not os.path.isdir(imageDir):
            self.log.info("Creating image directory %r", imageDir)
            try:
                os.mkdir(imageDir, 0o755)
            except Exception:
                self.log.exception("Unexpected error")
                raise se.ImagePathError(imageDir)
        self._imagePath = imageDir

    def validateVolumePath(self):
        if not self._imagePath:
            self.validateImagePath()
        volPath = os.path.join(self._imagePath, self.volUUID)
        if not os.path.lexists(volPath):
            srcPath = lvm.lvPath(self.sdUUID, self.volUUID)
            self.log.info("Creating symlink from %s to %s", srcPath, volPath)
            os.symlink(srcPath, volPath)
        self._volumePath = volPath

    def validate(self):
        try:
            lv = lvm.getLV(self.sdUUID, self.volUUID)
        except se.LogicalVolumeDoesNotExistError:
            raise se.VolumeDoesNotExist(self.volUUID)
        else:
            if sc.TEMP_VOL_LVTAG in lv.tags:
                self.log.warning("Tried to produce a volume artifact: %s/%s",
                                 self.sdUUID, self.volUUID)
                raise se.VolumeDoesNotExist(self.volUUID)
        volume.VolumeManifest.validate(self)

    def getVolumeTag(self, tagPrefix):
        return getVolumeTag(self.sdUUID, self.volUUID, tagPrefix)

    def getParentTag(self):
        return self.getVolumeTag(sc.TAG_PREFIX_PARENT)

    def getParentMeta(self):
        return self.getMetaParam(sc.PUUID)

    def getParent(self):
        return self.getParentTag()

    def getChildren(self):
        lvs = lvm.lvsByTag(self.sdUUID,
                           "%s%s" % (sc.TAG_PREFIX_PARENT, self.volUUID))
        return tuple(lv.name for lv in lvs)

    def getImage(self):
        return self.getVolumeTag(sc.TAG_PREFIX_IMAGE)

    def getDevPath(self):
        return lvm.lvPath(self.sdUUID, self.volUUID)

    def getVolumeSize(self):
        manifest = sdCache.produce_manifest(self.sdUUID)
        return manifest.getVSize(self.imgUUID, self.volUUID)

    def getVolumeTrueSize(self):
        return self.getVolumeSize()

    def setMetadata(self, meta, metaId=None, **overrides):
        if not metaId:
            metaId = self.getMetadataId()

        try:
            self._putMetadata(metaId, meta, **overrides)
        except Exception as e:
            self.log.error(e, exc_info=True)
            raise se.VolumeMetadataWriteError("%s: %s" % (metaId, e))

    def _setrw(self, rw):
        lvm.setrwLV(self.sdUUID, self.volUUID, rw)

    @classmethod
    def _putMetadata(cls, metaId, meta, **overrides):
        sd = sdCache.produce_manifest(meta.domain)

        _, slot = metaId

        data = meta.storage_format(sd.getVersion(), **overrides)
        data += "\0" * (sc.METADATA_SIZE - len(data))
        sd.write_metadata_block(slot, data)

    def changeVolumeTag(self, tagPrefix, uuid):

        if tagPrefix not in sc.VOLUME_TAGS:
            raise se.LogicalVolumeWrongTagError(tagPrefix)

        oldTag = ""
        for tag in lvm.getLV(self.sdUUID, self.volUUID).tags:
            if tag.startswith(tagPrefix):
                oldTag = tag
                break

        if not oldTag:
            raise se.MissingTagOnLogicalVolume(self.volUUID, tagPrefix)

        newTag = tagPrefix + uuid
        if oldTag != newTag:
            lvm.replaceLVTag(self.sdUUID, self.volUUID, oldTag, newTag)

    def setParentMeta(self, puuid):
        self.setMetaParam(sc.PUUID, puuid)

    def setParentTag(self, puuid):
        self.changeVolumeTag(sc.TAG_PREFIX_PARENT, puuid)

    def setImage(self, imgUUID):
        self.changeVolumeTag(sc.TAG_PREFIX_IMAGE, imgUUID)
        self.setMetaParam(sc.IMAGE, imgUUID)

    def removeMetadata(self, metaId):
        _, slot = metaId
        sdCache.produce_manifest(self.sdUUID).clear_metadata_block(slot)

    @classmethod
    def newVolumeLease(cls, metaId, sdUUID, volUUID):
        cls.log.debug("Initializing volume lease volUUID=%s sdUUID=%s, "
                      "metaId=%s", volUUID, sdUUID, metaId)
        _, slot = metaId
        sd = sdCache.produce_manifest(sdUUID)
        sd.create_volume_lease(slot, volUUID)

    def refreshVolume(self):
        lvm.refreshLVs(self.sdUUID, (self.volUUID,))

    def _share(self, dstImgPath):
        dstPath = os.path.join(dstImgPath, self.volUUID)

        self.log.info("Share volume %s to %s", self.volUUID, dstImgPath)
        os.symlink(self.getDevPath(), dstPath)

    @classmethod
    def getImageVolumes(cls, sdUUID, imgUUID):
        lvs = lvm.lvsByTag(sdUUID, "%s%s" % (sc.TAG_PREFIX_IMAGE, imgUUID))
        return [lv.name for lv in lvs]

    @classmethod
    def calculate_volume_alloc_size(
            cls, preallocate, capacity, initial_size):
        if initial_size and preallocate == sc.PREALLOCATED_VOL:
            log.error("Initial size is not supported for preallocated volumes")
            raise se.InvalidParameterException("initial size",
                                               initial_size)

        if initial_size:
            max_size = cls.max_size(capacity, sc.COW_FORMAT)
            if initial_size > max_size:
                log.error("The requested initial %s is bigger "
                          "than the max size %s", initial_size, max_size)
                raise se.InvalidParameterException("initial_size",
                                                   initial_size)

        if preallocate == sc.SPARSE_VOL:
            if initial_size:
                alloc_size = int(initial_size * QCOW_OVERHEAD_FACTOR)
            else:
                chunk_size_mb = config.getint("irs",
                                              "volume_utilization_chunk_mb")
                alloc_size = chunk_size_mb * constants.MEGAB
        else:
            alloc_size = capacity

        return alloc_size

    def llPrepare(self, rw=False, setrw=False):
        if setrw:
            self.setrw(rw=rw)
        access = rm.EXCLUSIVE if rw else rm.SHARED
        activation = rm.acquireResource(self.lvmActivationNamespace,
                                        self.volUUID, access)
        activation.autoRelease = False

    @classmethod
    def teardown(cls, sdUUID, volUUID, justme=False):
        cls.log.info("Tearing down volume %s/%s justme %s"
                     % (sdUUID, volUUID, justme))
        lvmActivationNamespace = rm.getNamespace(sc.LVM_ACTIVATION_NAMESPACE,
                                                 sdUUID)
        rm.releaseResource(lvmActivationNamespace, volUUID)
        if not justme:
            try:
                pvolUUID = getVolumeTag(sdUUID, volUUID, sc.TAG_PREFIX_PARENT)
            except Exception as e:
                pvolUUID = sc.BLANK_UUID
                cls.log.warn("Failure to get parent of volume %s/%s (%s)"
                             % (sdUUID, volUUID, e))

            if pvolUUID != sc.BLANK_UUID:
                cls.teardown(sdUUID=sdUUID, volUUID=pvolUUID, justme=False)

    def optimal_size(self):
        if self.getFormat() == sc.RAW_FORMAT:
            virtual_size = self.getCapacity()
            self.log.debug("RAW format, using virtual size: %s", virtual_size)
            return virtual_size

        check = qemuimg.check(self.getVolumePath(), qemuimg.FORMAT.QCOW2)
        actual_size = check['offset']

        if self.isLeaf():
            chnuk_size_mb = int(config.get("irs",
                                           "volume_utilization_chunk_mb"))
            padding = chnuk_size_mb * constants.MEGAB
            self.log.debug("Leaf volume, using padding: %s", padding)

            potential_optimal_size = actual_size + padding

        else:
            padding = MIN_PADDING
            self.log.debug("Internal volume, using padding: %s", padding)

            potential_optimal_size = actual_size + padding

            potential_optimal_size = max(sc.MIN_CHUNK, potential_optimal_size)

        max_size = self.max_size(self.getCapacity(), self.getFormat())
        optimal_size = min(potential_optimal_size, max_size)
        self.log.debug("COW format, actual_size: %s, max_size: %s, "
                       "optimal_size: %s",
                       actual_size, max_size, optimal_size)
        return optimal_size


class BlockVolume(volume.Volume):
    manifestClass = BlockVolumeManifest

    def refreshVolume(self):
        self._manifest.refreshVolume()

    @classmethod
    def halfbakedVolumeRollback(cls, taskObj, sdUUID, volUUID, volPath):
        cls.log.info("sdUUID=%s volUUID=%s volPath=%s" %
                     (sdUUID, volUUID, volPath))
        try:
            tags = lvm.getLV(sdUUID, volUUID).tags
        except se.LogicalVolumeDoesNotExistError:
        else:
            if sc.TAG_VOL_UNINIT in tags:
                try:
                    lvm.removeLVs(sdUUID, volUUID)
                except se.CannotRemoveLogicalVolume as e:
                    cls.log.warning("Remove logical volume failed %s/%s %s",
                                    sdUUID, volUUID, str(e))

                if os.path.lexists(volPath):
                    cls.log.info("Unlinking half baked volume: %s", volPath)
                    os.unlink(volPath)

    @classmethod
    def createVolumeMetadataRollback(cls, taskObj, sdUUID, slot):
        cls.log.info("Metadata rollback for sdUUID=%s slot=%s", sdUUID, slot)
        sd = sdCache.produce_manifest(sdUUID)
        sd.clear_metadata_block(slot)

    @classmethod
    def _create(cls, dom, imgUUID, volUUID, capacity, volFormat, preallocate,
                volParent, srcImgUUID, srcVolUUID, volPath, initial_size=None):

        lv_size = cls.calculate_volume_alloc_size(
            preallocate, capacity, initial_size)
        lv_size_mb = (utils.round(lv_size, constants.MEGAB) // constants.MEGAB)

        lvm.createLV(dom.sdUUID, volUUID, lv_size_mb, activate=True,
                     initialTags=(sc.TAG_VOL_UNINIT,))

        fileutils.rm_file(volPath)
        lvPath = lvm.lvPath(dom.sdUUID, volUUID)
        cls.log.info("Creating volume symlink from %r to %r", lvPath, volPath)
        os.symlink(lvPath, volPath)

        if not volParent:
            cls.log.info("Request to create %s volume %s with capacity = %s",
                         sc.type2name(volFormat), volPath, capacity)
            if volFormat == sc.COW_FORMAT:
                operation = qemuimg.create(volPath,
                                           size=capacity,
                                           format=sc.fmt2str(volFormat),
                                           qcow2Compat=dom.qcow2_compat())
                operation.run()
        else:
            cls.log.info("Request to create snapshot %s/%s of volume %s/%s "
                         "with capacity %s",
                         imgUUID, volUUID, srcImgUUID, srcVolUUID, capacity)
            volParent.clone(volPath, volFormat, capacity)

        with dom.acquireVolumeMetadataSlot(volUUID) as slot:
            mdTags = ["%s%s" % (sc.TAG_PREFIX_MD, slot),
                      "%s%s" % (sc.TAG_PREFIX_PARENT, srcVolUUID),
                      "%s%s" % (sc.TAG_PREFIX_IMAGE, imgUUID)]
            lvm.changeLVTags(dom.sdUUID, volUUID, delTags=[sc.TAG_VOL_UNINIT],
                             addTags=mdTags)

        try:
            lvm.deactivateLVs(dom.sdUUID, [volUUID])
        except se.CannotDeactivateLogicalVolume:
            cls.log.warn("Cannot deactivate new created volume %s/%s",
                         dom.sdUUID, volUUID, exc_info=True)

        return (dom.sdUUID, slot)

    @classmethod
    def calculate_volume_alloc_size(cls, preallocate, capacity, initial_size):
        return cls.manifestClass.calculate_volume_alloc_size(
            preallocate, capacity, initial_size)

    def removeMetadata(self, metaId):
        self._manifest.removeMetadata(metaId)

    def delete(self, postZero, force, discard):
        self.log.info("Request to delete LV %s of image %s in VG %s ",
                      self.volUUID, self.imgUUID, self.sdUUID)

        vol_path = self.getVolumePath()
        slot = self.getMetaSlot()

        sync = False
        for childID in self.getChildren():
            child = BlockVolume(self.repoPath, self.sdUUID, self.imgUUID,
                                childID)
            metaParent = child.getParentMeta()
            tagParent = child.getParentTag()
            if metaParent != tagParent:
                self.log.debug("Updating stale PUUID LV tag from %s to %s for "
                               "volume %s", tagParent, metaParent,
                               child.volUUID)
                child.setParentTag(metaParent)
                sync = True
        if sync:
            self.recheckIfLeaf()

        if not force:
            self.validateDelete()

        self.setLegality(sc.ILLEGAL_VOL)

        if postZero or discard:
            self.prepare(justme=True, rw=True, chainrw=force, setrw=True,
                         force=True)
            try:
                if postZero:
                    blockdev.zero(vol_path, task=vars.task)

                if discard:
                    blockdev.discard(vol_path)
            finally:
                self.teardown(self.sdUUID, self.volUUID, justme=True)

        eFound = se.CannotDeleteVolume(self.volUUID)
        puuid = None
        try:
            puuid = self.getParent()
            self.setParent(sc.BLANK_UUID)
            if puuid and puuid != sc.BLANK_UUID:
                pvol = BlockVolume(self.repoPath, self.sdUUID, self.imgUUID,
                                   puuid)
                pvol.recheckIfLeaf()
        except Exception as e:
            eFound = e
            self.log.warning("cannot finalize parent volume %s", puuid,
                             exc_info=True)

        manifest = sdCache.produce_manifest(self.sdUUID)
        manifest.markForDelVols(self.sdUUID, self.imgUUID, [self.volUUID],
                                sc.REMOVED_IMAGE_PREFIX)

        try:
            lvm.removeLVs(self.sdUUID, self.volUUID)
        except se.CannotRemoveLogicalVolume as e:
            self.log.exception("Failed to delete volume %s/%s. The "
                               "logical volume must be removed manually.",
                               self.sdUUID, self.volUUID)
        else:
            try:
                self.removeMetadata([self.sdUUID, slot])
            except se.VolumeMetadataWriteError as e:
                eFound = e
                self.log.exception("Failed to delete volume %s/%s metadata.",
                                   self.sdUUID, self.volUUID)

        try:
            self.log.info("Unlinking %s", vol_path)
            os.unlink(vol_path)
            return True
        except Exception as e:
            eFound = e
            self.log.error("cannot delete volume's %s/%s link path: %s",
                           self.sdUUID, self.volUUID, vol_path, exc_info=True)

        raise eFound

    def extend(self, new_size):
        self.log.info("Request to extend LV %s of image %s in VG %s with "
                      "size = %s", self.volUUID, self.imgUUID,
                      self.sdUUID, new_size)
        sizemb = utils.round(new_size, constants.MEGAB) // constants.MEGAB
        lvm.extendLV(self.sdUUID, self.volUUID, sizemb)

    def reduce(self, new_size, allowActive=False):
        self.log.info("Request to reduce LV %s of image %s in VG %s with "
                      "size = %s allowActive = %s", self.volUUID,
                      self.imgUUID, self.sdUUID, new_size, allowActive)
        sizemb = utils.round(new_size, constants.MEGAB) // constants.MEGAB
        lvm.reduceLV(self.sdUUID, self.volUUID, sizemb, force=allowActive)

    @classmethod
    def renameVolumeRollback(cls, taskObj, sdUUID, oldUUID, newUUID):
        try:
            cls.log.info("renameVolumeRollback: sdUUID=%s oldUUID=%s "
                         "newUUID=%s", sdUUID, oldUUID, newUUID)
            lvm.renameLV(sdUUID, oldUUID, newUUID)
        except Exception:
            cls.log.error("Failure in renameVolumeRollback: sdUUID=%s "
                          "oldUUID=%s newUUID=%s", sdUUID, oldUUID, newUUID,
                          exc_info=True)

    def rename(self, newUUID, recovery=True):
        self.log.info("Rename volume %s as %s ", self.volUUID, newUUID)
        if not self.imagePath:
            self._manifest.validateImagePath()

        if os.path.lexists(self.getVolumePath()):
            self.log.info("Unlinking volume symlink %r", self.getVolumePath())
            os.unlink(self.getVolumePath())

        if recovery:
            name = "Rename volume rollback: " + newUUID
            vars.task.pushRecovery(task.Recovery(
                name, "blockVolume", "BlockVolume", "renameVolumeRollback",
                [self.sdUUID, newUUID, self.volUUID]))

        metadataId = self.getMetadataId()

        lvm.renameLV(self.sdUUID, self.volUUID, newUUID)

        self.renameLease(metadataId, newUUID, recovery=recovery)

        self._manifest.volUUID = newUUID
        self._manifest.volumePath = os.path.join(self.imagePath, newUUID)

    def getDevPath(self):
        return self._manifest.getDevPath()

    @classmethod
    def shareVolumeRollback(cls, taskObj, volPath):
        cls.log.info("Volume rollback for volPath=%s", volPath)
        fileutils.rm_file(volPath)

    def getVolumeTag(self, tagPrefix):
        return self._manifest.getVolumeTag(tagPrefix)

    def changeVolumeTag(self, tagPrefix, uuid):
        return self._manifest.changeVolumeTag(tagPrefix, uuid)

    def getParentMeta(self):
        return self._manifest.getParentMeta()

    def getParentTag(self):
        return self._manifest.getParentTag()

    def setParentMeta(self, puuid):
        return self._manifest.setParentMeta(puuid)

    def setParentTag(self, puuid):
        return self._manifest.setParentTag(puuid)

    def getMetaSlot(self):
        return self._manifest.getMetaSlot()

    def _extendSizeRaw(self, new_capacity):
        new_capacity_mb = (utils.round(new_capacity, constants.MEGAB) //
                           constants.MEGAB)
        lvm.extendLV(self.sdUUID, self.volUUID, new_capacity_mb)


def getVolumeTag(sdUUID, volUUID, tagPrefix):
    tags = lvm.getLV(sdUUID, volUUID).tags
    if sc.TAG_VOL_UNINIT in tags:
        log.warning("Reloading uninitialized volume %s/%s", sdUUID, volUUID)
        lvm.invalidateVG(sdUUID)
        tags = lvm.getLV(sdUUID, volUUID).tags
        if sc.TAG_VOL_UNINIT in tags:
            log.error("Found uninitialized volume: %s/%s", sdUUID, volUUID)
            raise se.VolumeDoesNotExist("%s/%s" % (sdUUID, volUUID))

    for tag in tags:
        if tag.startswith(tagPrefix):
            return tag[len(tagPrefix):]
    else:
        log.error("Missing tag %s in volume: %s/%s. tags: %s",
                  tagPrefix, sdUUID, volUUID, tags)
        raise se.MissingTagOnLogicalVolume(volUUID, tagPrefix)
