

from __future__ import absolute_import

import errno

import os
import re
import pwd
import glob
import grp
import logging
from collections import namedtuple
import platform
import pprint as pp
import threading
import time

from itertools import chain
from subprocess import list2cmdline

from vdsm import constants
from vdsm.common import cache
from vdsm.common import errors

from vdsm.storage import devicemapper
from vdsm.storage import constants as sc
from vdsm.storage import exception as se
from vdsm.storage import misc
from vdsm.storage import multipath
from vdsm.storage import rwlock
from vdsm.storage.constants import VG_EXTENT_SIZE_MB, SUPPORTED_BLOCKSIZE

from vdsm.config import config

log = logging.getLogger("storage.LVM")

PV_FIELDS = ("uuid,name,size,vg_name,vg_uuid,pe_start,pe_count,"
             "pe_alloc_count,mda_count,dev_size,mda_used_count")
PV_FIELDS_LEN = len(PV_FIELDS.split(","))

VG_FIELDS = ("uuid,name,attr,size,free,extent_size,extent_count,free_count,"
             "tags,vg_mda_size,vg_mda_free,lv_count,pv_count,pv_name")
VG_FIELDS_LEN = len(VG_FIELDS.split(","))

LV_FIELDS = "uuid,name,vg_name,attr,size,seg_start_pe,devices,tags"
LV_FIELDS_LEN = len(LV_FIELDS.split(","))

VG_ATTR_BITS = ("permission", "resizeable", "exported",
                "partial", "allocation", "clustered")
LV_ATTR_BITS = ("voltype", "permission", "allocations", "fixedminor", "state",
                "devopen", "target", "zero")

UNKNOWN = "[unknown]"

PV = namedtuple("PV", PV_FIELDS + ",guid")
VG = namedtuple("VG", VG_FIELDS + ",writeable,partial")
VG_ATTR = namedtuple("VG_ATTR", VG_ATTR_BITS)
LV = namedtuple("LV", LV_FIELDS + ",writeable,opened,active")
LV_ATTR = namedtuple("LV_ATTR", LV_ATTR_BITS)
Stub = namedtuple("Stub", "name, stale")


class InvalidOutputLine(errors.Base):
    msg = "Invalid {self.command} command ouptut line: {self.line!r}"

    def __init__(self, command, line):
        self.command = command
        self.line = line


class Unreadable(Stub):
    __slots__ = ()

    def __getattr__(self, attrName):
        log.warning("%s can't be reloaded, please check your storage "
                    "connections.", self.name)
        raise AttributeError("Failed reload: %s" % self.name)

VG_OK = "OK"
VG_PARTIAL = "PARTIAL"

SEPARATOR = "|"
LVM_NOBACKUP = ("--autobackup", "n")
LVM_FLAGS = ("--noheadings", "--units", "b", "--nosuffix", "--separator",
             SEPARATOR, "--ignoreskippedcluster")

PV_PREFIX = "/dev/mapper"
re_pvName = re.compile(PV_PREFIX + '[^\s\"]+', re.MULTILINE)

PVS_CMD = ("pvs",) + LVM_FLAGS + ("-o", PV_FIELDS)
VGS_CMD = ("vgs",) + LVM_FLAGS + ("-o", VG_FIELDS)
LVS_CMD = ("lvs",) + LVM_FLAGS + ("-o", LV_FIELDS)

USER_GROUP = constants.DISKIMAGE_USER + ":" + constants.DISKIMAGE_GROUP

LVMCONF_TEMPLATE = """
devices {
 preferred_names=["^/dev/mapper/"]
 ignore_suspended_devices=1
 write_cache_state=0
 disable_after_error_count=3
 filter=%(filter)s
}
global {
 locking_type=%(locking_type)s
 prioritise_write_locks=1
 wait_for_locks=1
 use_lvmetad=0
}
backup {
 retain_min=50
 retain_days=0
}
"""

USER_DEV_LIST = filter(None, config.get("irs", "lvm_dev_whitelist").split(","))


def _buildFilter(devices):
    devices = set(d.strip() for d in chain(devices, USER_DEV_LIST))
    devices.discard('')
    if devices:
        devices = sorted(d.replace(r'\x', r'\\x') for d in devices)
        pattern = "|".join("^{}$".format(d) for d in devices)
        accept = '"a|{}|", '.format(pattern)
    else:
        accept = ''
    return '[{}"r|.*|"]'.format(accept)


def _buildConfig(dev_filter, locking_type):
    conf = LVMCONF_TEMPLATE % {
        "filter": dev_filter,
        "locking_type": locking_type,
    }
    return conf.replace("\n", " ").strip()


def _normalizeargs(args=None):
    if args is None:
        args = []
    elif not hasattr(args, "__iter__"):
        args = [args]

    return args


def _tags2Tuple(sTags):
    """
    Tags comma separated string as a list.

    Return an empty tuple for sTags == ""
    """
    return tuple(sTags.split(",")) if sTags else tuple()


def makePV(*args):
    guid = os.path.basename(args[1])
    args += (guid,)
    return PV(*args)


def makeVG(*args):
    args = list(args)
    tags = _tags2Tuple(args[VG._fields.index("tags")])
    args[VG._fields.index("tags")] = tags
    sAttr = args[VG._fields.index("attr")]
    attr_values = tuple(sAttr[:len(VG_ATTR._fields)])
    attrs = VG_ATTR(*attr_values)
    args[VG._fields.index("attr")] = attrs
    args[VG._fields.index("pv_name")] = \
        tuple(args[VG._fields.index("pv_name")])
    return VG(*args)


def makeLV(*args):
    args = list(args)
    tags = _tags2Tuple(args[LV._fields.index("tags")])
    args[LV._fields.index("tags")] = tags
    sAttr = args[LV._fields.index("attr")]
    attr_values = tuple(sAttr[:len(LV_ATTR._fields)])
    attrs = LV_ATTR(*attr_values)
    args[LV._fields.index("attr")] = attrs
    return LV(*args)


@cache.memoized
def _on_x86_64():
    try:
        return platform.machine() == "x86_64"
    except Exception as e:
        log.warning("Failed to get platform machine: %s", e)
        return False


class LVMCache(object):

    MAX_COMMANDS = 10

    READ_ONLY_RETRIES = 4
    RETRY_DELAY = 0.1
    RETRY_BACKUP_OFF = 2

    SUPPRESS_WARNINGS = re.compile(
        br"|".join([
            br"WARNING: This metadata update is NOT backed up",
            (br"Scan of VG \S+ from \S+ found metadata seqno \d+ vs previous "
             br"\d+"),
        ]),
        re.IGNORECASE)

    def __init__(self):
        self._read_only_lock = rwlock.RWLock()
        self._read_only = True
        self._filter = None
        self._filterStale = True
        self._filterLock = threading.Lock()
        self._lock = threading.Lock()
        self._cmd_sem = threading.BoundedSemaphore(self.MAX_COMMANDS)
        self._stalepv = True
        self._stalevg = True
        self._stalelv = True
        self._pvs = {}
        self._vgs = {}
        self._lvs = {}

    def set_read_only(self, value):
        with self._read_only_lock.exclusive:
            if self._read_only != value:
                log.info("Switching to read_only=%s", value)
                self._read_only = value

    def is_read_only(self):
        return self._read_only

    def _getCachedFilter(self):
        with self._filterLock:
            if self._filterStale:
                self._filter = _buildFilter(multipath.getMPDevNamesIter())
                self._filterStale = False
            return self._filter

    def _addExtraCfg(self, cmd, devices=tuple(), read_only=None):
        if read_only is None:
            read_only = self._read_only

        newcmd = [constants.EXT_LVM, cmd[0]]

        if devices:
            dev_filter = _buildFilter(devices)
        else:
            dev_filter = self._getCachedFilter()

        conf = _buildConfig(
            dev_filter=dev_filter,
            locking_type="4" if read_only else "1")

        newcmd += ["--config", conf]

        if len(cmd) > 1:
            newcmd += cmd[1:]

        return newcmd

    def invalidateFilter(self):
        self._filterStale = True

    def invalidateCache(self):
        self.invalidateFilter()
        self.flush()

    def cmd(self, cmd, devices=tuple(), read_only=None, wants_output=False):
        with self._cmd_sem, self._read_only_lock.shared:
            if read_only is None:
                read_only = self._read_only
            elif read_only != self._read_only:
                log.info("Overriding read_only mode current=%s override=%s",
                         self._read_only, read_only)

            full_cmd = self._addExtraCfg(
                cmd, devices=devices, read_only=read_only)
            rc, out, err = self._run_lvm(full_cmd)
            if rc == 0 and (out or not wants_output):
                return rc, out, err

            self.invalidateFilter()
            wider_cmd = self._addExtraCfg(cmd, read_only=read_only)
            if wider_cmd != full_cmd:
                log.warning(
                    "Command with specific filter failed or returned no data, "
                    "retrying with a wider filter, cmd=%r rc=%r out=%r err=%r",
                    full_cmd, rc, out, err)
                full_cmd = wider_cmd
                rc, out, err = self._run_lvm(full_cmd)
                if rc == 0:
                    return rc, out, err

            if rc != 0 and read_only:
                delay = self.RETRY_DELAY
                for retry in range(1, self.READ_ONLY_RETRIES + 1):
                    log.warning(
                        "Retry %d failed, retrying in %.2f seconds, cmd=%r "
                        "rc=%r err=%r",
                        retry, delay, full_cmd, rc, err)

                    time.sleep(delay)
                    delay *= self.RETRY_BACKUP_OFF

                    rc, out, err = self._run_lvm(full_cmd)
                    if rc == 0:
                        return rc, out, err

            return rc, out, err

    def _run_lvm(self, cmd):
        rc, out, err = misc.execCmd(cmd, sudo=True)

        err = [s for s in err if not self.SUPPRESS_WARNINGS.search(s)]

        if rc == 0 and err:
            log.warning("Command %s succeeded with warnings: %s", cmd, err)

        return rc, out, err

    def __str__(self):
        return ("PVS:\n%s\n\nVGS:\n%s\n\nLVS:\n%s" %
                (pp.pformat(self._pvs),
                 pp.pformat(self._vgs),
                 pp.pformat(self._lvs)))

    def bootstrap(self):
        self._reloadpvs()
        self._reloadvgs()
        self._reloadAllLvs()

    def _reloadpvs(self, pvName=None):
        cmd = list(PVS_CMD)

        pvNames = _normalizeargs(pvName)
        if pvNames:
            selection = " || ".join("pv_name = {}".format(n) for n in pvNames)
            cmd.append("--select")
            cmd.append(selection)

        read_only = None if _on_x86_64() else False
        rc, out, err = self.cmd(cmd, read_only=read_only, wants_output=True)

        with self._lock:
            updatedPVs = {}

            if rc != 0:
                log.error(
                    "Reloading PVs failed: pvs=%r rc=%r out=%r err=%r",
                    pvNames, rc, out, err)
                pvNames = pvNames if pvNames else self._pvs
                for p in pvNames:
                    if isinstance(self._pvs.get(p), Stub):
                        pv = Unreadable(self._pvs[p].name, True)
                        self._pvs[p] = pv
                        updatedPVs[p] = pv

                return updatedPVs

            for line in out:
                fields = [field.strip() for field in line.split(SEPARATOR)]
                if len(fields) != PV_FIELDS_LEN:
                    raise InvalidOutputLine("pvs", line)

                pv = makePV(*fields)
                if pv.name == UNKNOWN:
                    log.error("Missing pv: %s in vg: %s", pv.uuid, pv.vg_name)
                    continue
                self._pvs[pv.name] = pv
                updatedPVs[pv.name] = pv

            stalePVs = [name for name in (pvNames or self._pvs)
                        if name not in updatedPVs]
            for name in stalePVs:
                if name in self._pvs:
                    log.warning("Removing stale PV %s", name)
                    del self._pvs[name]

            if not pvName:
                self._stalepv = False

        return updatedPVs

    def _getVGDevs(self, vgNames):
        devices = []
        with self._lock:
            for name in vgNames:
                try:
                    devices = tuple()
                else:
                    devices.extend(pvs)
                devices = tuple(devices)
        return devices

    def _reloadvgs(self, vgName=None):
        cmd = list(VGS_CMD)

        vgNames = _normalizeargs(vgName)
        if vgNames:
            selection = " || ".join("vg_name = {}".format(n) for n in vgNames)
            cmd.append("--select")
            cmd.append(selection)

        rc, out, err = self.cmd(
            cmd, self._getVGDevs(vgNames), wants_output=True)

        with self._lock:
            updatedVGs = {}

            if rc != 0:
                log.error(
                    "Reloading VGs failed vgs=%r rc=%r out=%r err=%r",
                    vgNames, rc, out, err)
                for v in (vgNames or self._vgs):
                    if isinstance(self._vgs.get(v), Stub):
                        vg = Unreadable(self._vgs[v].name, True)
                        self._vgs[v] = vg
                        updatedVGs[v] = vg

            vgsFields = {}
            for line in out:
                fields = [field.strip() for field in line.split(SEPARATOR)]
                if len(fields) != VG_FIELDS_LEN:
                    raise InvalidOutputLine("vgs", line)

                uuid = fields[VG._fields.index("uuid")]
                pvNameIdx = VG._fields.index("pv_name")
                pv_name = fields[pvNameIdx]
                if pv_name == UNKNOWN:
                    continue
                if uuid not in vgsFields:
                    vgsFields[uuid] = fields
                else:
                    vgsFields[uuid][pvNameIdx].append(pv_name)
            for fields in vgsFields.itervalues():
                vg = makeVG(*fields)
                if int(vg.pv_count) != len(vg.pv_name):
                    log.error("vg %s has pv_count %s but pv_names %s",
                              vg.name, vg.pv_count, vg.pv_name)
                self._vgs[vg.name] = vg
                updatedVGs[vg.name] = vg

            staleVGs = [name for name in (vgNames or self._vgs)
                        if name not in updatedVGs]
            for name in staleVGs:
                if name in self._vgs:
                    log.warning("Removing stale VG %s", name)
                    removeVgMapping(name)
                    del self._vgs[name]

            if not vgName:
                self._stalevg = False

        return updatedVGs

    def _reloadlvs(self, vgName, lvNames=None):
        cmd = list(LVS_CMD)

        selection = "vg_name = {}".format(vgName)
        lvNames = _normalizeargs(lvNames)
        if lvNames:
            lvs = " || ".join("lv_name = {}".format(n) for n in lvNames)
            selection += " && ({})".format(lvs)

        cmd.append("--select")
        cmd.append(selection)

        rc, out, err = self.cmd(
            cmd, self._getVGDevs((vgName,)), wants_output=True)

        with self._lock:
            updatedLVs = {}

            if rc != 0:
                log.error(
                    "Reloading LVs failed vg=%r lvs=%r rc=%s out=%r err=%r",
                    vgName, lvNames, rc, out, err)
                if not lvNames:
                    lvNames = (lvn for vgn, lvn in self._lvs if vgn == vgName)
                for lvName in lvNames:
                    if isinstance(self._lvs.get((vgName, lvName)), Stub):
                        lv = Unreadable(lvName, True)
                        self._lvs[(vgName, lvName)] = lv
                        updatedLVs[(vgName, lvName)] = lv

                return updatedLVs

            for line in out:
                fields = [field.strip() for field in line.split(SEPARATOR)]
                if len(fields) != LV_FIELDS_LEN:
                    raise InvalidOutputLine("lvs", line)

                lv = makeLV(*fields)
                if lv.seg_start_pe == "0":
                    self._lvs[(lv.vg_name, lv.name)] = lv
                    updatedLVs[(lv.vg_name, lv.name)] = lv

            if lvNames:
                staleLVs = (lvName for lvName in lvNames
                            if (vgName, lvName) not in updatedLVs.iterkeys())
            else:
                staleLVs = (lvName for v, lvName in self._lvs.keys()
                            if (v == vgName) and
                            ((vgName, lvName) not in updatedLVs.iterkeys()))

            for lvName in staleLVs:
                if (vgName, lvName) in self._lvs:
                    log.warning("Removing stale lv: %s/%s", vgName, lvName)
                    del self._lvs[(vgName, lvName)]

            log.debug("lvs reloaded")

        return updatedLVs

    def _reloadAllLvs(self):
        """
        Used only during bootstrap.
        """
        cmd = list(LVS_CMD)
        rc, out, err = self.cmd(cmd)
        if rc == 0:
            updatedLVs = set()
            for line in out:
                fields = [field.strip() for field in line.split(SEPARATOR)]
                if len(fields) != LV_FIELDS_LEN:
                    raise InvalidOutputLine("lvs", line)

                lv = makeLV(*fields)
                if lv.seg_start_pe == "0":
                    self._lvs[(lv.vg_name, lv.name)] = lv
                    updatedLVs.add((lv.vg_name, lv.name))

            for vgName, lvName in self._lvs.keys():
                if (vgName, lvName) not in updatedLVs:
                    self._lvs.pop((vgName, lvName), None)
                    log.error("Removing stale lv: %s/%s", vgName, lvName)
            self._stalelv = False
        return dict(self._lvs)

    def _invalidatepvs(self, pvNames):
        pvNames = _normalizeargs(pvNames)
        with self._lock:
            for pvName in pvNames:
                self._pvs[pvName] = Stub(pvName, True)

    def _invalidatevgpvs(self, vgName):
        with self._lock:
            for pv in self._pvs.values():
                if not isinstance(pv, Stub) and pv.vg_name == vgName:
                    self._pvs[pv.name] = Stub(pv.name, True)

    def _invalidateAllPvs(self):
        with self._lock:
            self._stalepv = True
            self._pvs.clear()

    def _invalidatevgs(self, vgNames):
        vgNames = _normalizeargs(vgNames)
        with self._lock:
            for vgName in vgNames:
                self._vgs[vgName] = Stub(vgName, True)

    def _invalidateAllVgs(self):
        with self._lock:
            self._stalevg = True
            self._vgs.clear()

    def _invalidatelvs(self, vgName, lvNames=None):
        lvNames = _normalizeargs(lvNames)
        with self._lock:
            if lvNames:
                for lvName in lvNames:
                    self._lvs[(vgName, lvName)] = Stub(lvName, True)
            else:
                for lv in self._lvs.values():
                    if not isinstance(lv, Stub):
                        if lv.vg_name == vgName:
                            self._lvs[(vgName, lv.name)] = Stub(lv.name, True)

    def _invalidateAllLvs(self):
        with self._lock:
            self._stalelv = True
            self._lvs.clear()

    def flush(self):
        self._invalidateAllPvs()
        self._invalidateAllVgs()
        self._invalidateAllLvs()

    def getPv(self, pvName):
        pv = self._pvs.get(pvName)
        if not pv or isinstance(pv, Stub):
            pvs = self._reloadpvs(pvName)
            pv = pvs.get(pvName)
        return pv

    def getAllPvs(self):
        if self._stalepv:
            pvs = self._reloadpvs()
        else:
            pvs = dict(self._pvs)
            stalepvs = [pv.name for pv in pvs.itervalues()
                        if isinstance(pv, Stub)]
            if stalepvs:
                for name in stalepvs:
                    del pvs[name]
                reloaded = self._reloadpvs(stalepvs)
                pvs.update(reloaded)
        return pvs.values()

    def getPvs(self, vgName):
        stalepvs = []
        pvs = []
        vg = self.getVg(vgName)
        for pvName in vg.pv_name:
            pv = self._pvs.get(pvName)
            if pv is None or isinstance(pv, Stub):
                stalepvs.append(pvName)
            else:
                pvs.append(pv)

        if stalepvs:
            reloadedpvs = self._reloadpvs(pvName=stalepvs)
            pvs.extend(reloadedpvs.values())
        return pvs

    def getVg(self, vgName):
        vg = self._vgs.get(vgName)
        if not vg or isinstance(vg, Stub):
            vgs = self._reloadvgs(vgName)
            vg = vgs.get(vgName)
        return vg

    def getVgs(self, vgNames):
        return [vg for vgName, vg in self._reloadvgs(vgNames).iteritems()
                if vgName in vgNames]

    def getAllVgs(self):
        if self._stalevg:
            vgs = self._reloadvgs()
        else:
            vgs = dict(self._vgs)
            stalevgs = [vg.name for vg in vgs.itervalues()
                        if isinstance(vg, Stub)]
            if stalevgs:
                for name in stalevgs:
                    del vgs[name]
                reloaded = self._reloadvgs(stalevgs)
                vgs.update(reloaded)
        return vgs.values()

    def getLv(self, vgName, lvName=None):

        if lvName:
            lv = self._lvs.get((vgName, lvName))
            if not lv or isinstance(lv, Stub):
                lvs = self._reloadlvs(vgName)
                lv = lvs.get((vgName, lvName))
                if not lv:
                    log.warning("lv: %s not found in lvs vg: %s response",
                                lvName, vgName)
            res = lv
        else:
            if self._stalelv or any(isinstance(lv, Stub)
                                    for lv in self._lvs.values()):
                lvs = self._reloadlvs(vgName)
            else:
                lvs = dict(self._lvs)
            lvs = [lv for lv in lvs.values()
                   if not isinstance(lv, Stub) and (lv.vg_name == vgName)]
            res = lvs
        return res

    def getAllLvs(self):
        if self._stalelv or any(isinstance(lv, Stub)
                                for lv in self._lvs.values()):
            lvs = self._reloadAllLvs()
        else:
            lvs = dict(self._lvs)
        return lvs.values()

_lvminfo = LVMCache()


def bootstrap(skiplvs=()):
    _lvminfo.bootstrap()

    skiplvs = set(skiplvs)

    for vg in _lvminfo.getAllVgs():
        deactivate = []

        pattern = "{}/{}/*/*".format(sc.P_VDSM_STORAGE, vg.name)
        prepared = frozenset(os.path.basename(n) for n in glob.iglob(pattern))

        for lv in _lvminfo.getLv(vg.name):
            if lv.active:
                if lv.name in skiplvs:
                    log.debug("Skipping active lv: vg=%s lv=%s",
                              vg.name, lv.name)
                elif lv.name in prepared:
                    log.debug("Skipping prepared volume lv: vg=%s lv=%s",
                              vg.name, lv.name)
                elif lv.opened:
                    log.debug("Skipping open lv: vg=%s lv=%s", vg.name,
                              lv.name)
                else:
                    deactivate.append(lv.name)

        if deactivate:
            log.info("Deactivating lvs: vg=%s lvs=%s", vg.name, deactivate)
            try:
                _setLVAvailability(vg.name, deactivate, "n")
            except se.CannotDeactivateLogicalVolume:
                log.error("Error deactivating lvs: vg=%s lvs=%s", vg.name,
                          deactivate)
            _lvminfo._invalidatelvs(vg.name, deactivate)


def invalidateCache():
    _lvminfo.invalidateCache()


def _fqpvname(pv):
    if pv[0] == "/":
        return pv
    else:
        return os.path.join(PV_PREFIX, pv)


def _createpv(devices, metadataSize, options=tuple()):
    cmd = ["pvcreate"]
    if options:
        cmd.extend(options)
    if metadataSize != 0:
        cmd.extend(("--metadatasize", "%sm" % metadataSize,
                    "--metadatacopies", "2",
                    "--metadataignore", "y"))
    cmd.extend(devices)
    rc, out, err = _lvminfo.cmd(cmd, devices, read_only=False)
    return rc, out, err


def _initpvs(devices, metadataSize, force=False):

    def _initpvs_removeHolders():
        for device in devices:
            try:
                devicemapper.removeMappingsHoldingDevice(
                    os.path.basename(device))
            except OSError as e:
                if e.errno == errno.ENODEV:
                    raise se.PhysDevInitializationError("%s: %s" %
                                                        (device, str(e)))
                else:
                    raise

    if force is True:
        options = ("-y", "-ff")
        _initpvs_removeHolders()
    else:
        options = tuple()

    rc, out, err = _createpv(devices, metadataSize, options)
    _lvminfo._invalidatepvs(devices)
    if rc != 0:
        log.error("pvcreate failed with rc=%s", rc)
        log.error("%s, %s", out, err)
        raise se.PhysDevInitializationError(str(devices))


def getLvDmName(vgName, lvName):
    return "%s-%s" % (vgName.replace("-", "--"), lvName)


def removeVgMapping(vgName):
    mappingPrefix = getLvDmName(vgName, "")
    mappings = devicemapper.getAllMappedDevices()

    for mapping in mappings:
        if not mapping.startswith(mappingPrefix):
            continue
        try:
            devicemapper.removeMapping(mapping)
        except Exception:
            pass


def _setVgAvailability(vgs, available):
    vgs = _normalizeargs(vgs)
    cmd = ["vgchange", "--available", available] + vgs
    rc, out, err = _lvminfo.cmd(cmd, _lvminfo._getVGDevs(vgs))
    for vg in vgs:
        _lvminfo._invalidatelvs(vg)
    if rc != 0:
        if available == "n":
            log.info("deactivate vg %s failed: rc %s - %s %s (ignored)" %
                     (vgs, rc, out, err))
        else:
            raise se.VolumeGroupActionError(
                "vgchange on vg(s) %s failed. %d %s %s" % (vgs, rc, out, err))


def changelv(vg, lvs, attrs):

    lvs = _normalizeargs(lvs)
    lvnames = tuple("%s/%s" % (vg, lv) for lv in lvs)
    cmd = ["lvchange"]
    cmd.extend(LVM_NOBACKUP)
    if isinstance(attrs[0], str):
        cmd.extend(attrs)
    else:
        for attr in attrs:
            cmd.extend(attr)
    cmd.extend(lvnames)
    rc, out, err = _lvminfo.cmd(tuple(cmd), _lvminfo._getVGDevs((vg, )))
    _lvminfo._invalidatelvs(vg, lvs)
    if rc != 0:
        raise se.StorageException("%d %s %s\n%s/%s" % (rc, out, err, vg, lvs))


def _setLVAvailability(vg, lvs, available):
    try:
        changelv(vg, lvs, ("--available", available))
    except se.StorageException as e:
        error = ({"y": se.CannotActivateLogicalVolumes,
                  "n": se.CannotDeactivateLogicalVolume}
                 .get(available, se.VolumeGroupActionError))
        raise error(str(e))



def getPV(pvName):
    pv = _lvminfo.getPv(_fqpvname(pvName))
    if pv is None:
        raise se.InaccessiblePhysDev((pvName,))
    return pv


def getAllPVs():
    return _lvminfo.getAllPvs()


def testPVCreate(devices, metadataSize):
    devs = tuple("%s/%s" % (PV_PREFIX, dev) for dev in devices)

    options = ("--test",)
    rc, out, err = _createpv(devs, metadataSize, options)
    if rc == 0:
        unusedDevs = set(devices)
        usedDevs = set()
    else:
        unusedDevs = set(re_pvName.findall("\n".join(out)))
        usedDevs = set(devs) - set(unusedDevs)
        log.debug("rc: %s, out: %s, err: %s, unusedDevs: %s, usedDevs: %s",
                  rc, out, err, unusedDevs, usedDevs)

    return unusedDevs, usedDevs


def resizePV(vgName, guid):
    pvName = _fqpvname(guid)
    cmd = ["pvresize", pvName]
    rc, out, err = _lvminfo.cmd(cmd, _lvminfo._getVGDevs((vgName, )))
    if rc != 0:
        raise se.CouldNotResizePhysicalVolume(pvName, err)
    _lvminfo._invalidatepvs(pvName)
    _lvminfo._invalidatevgs(vgName)


def movePV(vgName, src_device, dst_devices, read_only=None):
    pvName = _fqpvname(src_device)

    _lvminfo._invalidatepvs(pvName)
    pv = getPV(pvName)
    if pv.pe_alloc_count == "0":
        log.info("No data to move on pv %s (vg %s), considering as successful",
                 pvName, vgName)
        return

    cmd = ["pvmove", pvName]
    if dst_devices:
        cmd.extend(_fqpvname(pdev) for pdev in dst_devices)

    log.info(
        "Moving pv %s data (vg %s, read_only %s)", pvName, vgName, read_only)
    rc, out, err = _lvminfo.cmd(
        cmd, _lvminfo._getVGDevs((vgName, )), read_only=read_only)
    _lvminfo._invalidatepvs(pvName)
    _lvminfo._invalidatelvs(vgName)
    _lvminfo._invalidatevgs(vgName)
    if rc != 0:
        raise se.CouldNotMovePVData(pvName, vgName, err)


def getVG(vgName):
    if not vg:
        raise se.VolumeGroupDoesNotExist(vgName)
    else:
        return vg


def getVGs(vgNames):


def getAllVGs():


def getVGbyUUID(vgUUID):
    for vg in getAllVGs():
        try:
            if vg.uuid == vgUUID:
                return vg
        except AttributeError as e:
            log.debug("%s" % e.message, exc_info=True)
            continue
    raise se.VolumeGroupDoesNotExist("vg_uuid: %s" % vgUUID)


def getLV(vgName, lvName=None):
    lv = _lvminfo.getLv(vgName, lvName)
    if not lv:
        raise se.LogicalVolumeDoesNotExistError("%s/%s" % (vgName, lvName))
    else:
        return lv



def set_read_only(read_only):
    _lvminfo.set_read_only(read_only)


def is_read_only():
    return _lvminfo.is_read_only()



def createVG(vgName, devices, initialTag, metadataSize, force=False):
    pvs = [_fqpvname(pdev) for pdev in _normalizeargs(devices)]
    _checkpvsblksize(pvs)

    _initpvs(pvs, metadataSize, force)

    read_only = False

    cmd = ["pvchange", "--metadataignore", "n"]
    cmd.append(pvs[0])
    rc, out, err = _lvminfo.cmd(cmd, tuple(pvs), read_only=read_only)
    if rc != 0:
        raise se.PhysDevInitializationError(pvs[0])

    options = ["--physicalextentsize", "%dm" % VG_EXTENT_SIZE_MB]
    if initialTag:
        options.extend(("--addtag", initialTag))
    cmd = ["vgcreate"] + options + [vgName] + pvs
    rc, out, err = _lvminfo.cmd(cmd, tuple(pvs), read_only=read_only)
    if rc == 0:
        _lvminfo._invalidatepvs(pvs)
        _lvminfo._invalidatevgs(vgName)
        log.debug("Cache after createvg %s", _lvminfo._vgs)
    else:
        raise se.VolumeGroupCreateError(vgName, pvs)


def removeVG(vgName):
    cmd = ["vgremove", "-f", vgName]
    rc, out, err = _lvminfo.cmd(
        cmd, _lvminfo._getVGDevs((vgName, )), read_only=False)
    pvs = tuple(pvName for pvName, pv in _lvminfo._pvs.iteritems()
                if not isinstance(pv, Stub) and pv.vg_name == vgName)
    _lvminfo._invalidatepvs(pvs)
    if rc != 0:
        _lvminfo._invalidatevgs(vgName)
        raise se.VolumeGroupRemoveError("VG %s remove failed." % vgName)
    else:
        _lvminfo._vgs.pop(vgName, None)


def removeVGbyUUID(vgUUID):
    vg = getVGbyUUID(vgUUID)
    if vg:
        removeVG(vg.name)


def extendVG(vgName, devices, force):
    pvs = [_fqpvname(pdev) for pdev in _normalizeargs(devices)]
    _checkpvsblksize(pvs, getVGBlockSizes(vgName))
    vg = _lvminfo.getVg(vgName)

    member_pvs = set(vg.pv_name).intersection(pvs)
    if member_pvs:
        log.error("Cannot extend vg %s: pvs already belong to vg %s",
                  vg.name, member_pvs)
        raise se.VolumeGroupExtendError(vgName, pvs)

    _initpvs(pvs, int(vg.vg_mda_size) / 2 ** 20, force)

    cmd = ["vgextend", vgName] + pvs
    devs = tuple(_lvminfo._getVGDevs((vgName, )) + tuple(pvs))
    rc, out, err = _lvminfo.cmd(cmd, devs, read_only=False)
    if rc == 0:
        _lvminfo._invalidatepvs(pvs)
        _lvminfo._invalidatevgs(vgName)
        log.debug("Cache after extending vg %s", _lvminfo._vgs)
    else:
        raise se.VolumeGroupExtendError(vgName, pvs)


def reduceVG(vgName, device, read_only=None):
    pvName = _fqpvname(device)
    log.info(
        "Removing pv %s from vg %s (read_only %s)", pvName, vgName, read_only)
    cmd = ["vgreduce", vgName, pvName]
    rc, out, err = _lvminfo.cmd(
        cmd, _lvminfo._getVGDevs((vgName, )), read_only=read_only)
    if rc != 0:
        raise se.VolumeGroupReduceError(vgName, pvName, err)
    _lvminfo._invalidatepvs(pvName)
    _lvminfo._invalidatevgs(vgName)


def chkVG(vgName):
    cmd = ["vgck", vgName]
    rc, out, err = _lvminfo.cmd(cmd, _lvminfo._getVGDevs((vgName, )))
    if rc != 0:
        _lvminfo._invalidatevgs(vgName)
        _lvminfo._invalidatelvs(vgName)
        raise se.StorageDomainAccessError("%s: %s" % (vgName, err))
    return True


def deactivateVG(vgName):
    _setVgAvailability(vgName, available="n")


def invalidateVG(vgName, invalidateLVs=True, invalidatePVs=False):
    _lvminfo._invalidatevgs(vgName)
    if invalidateLVs:
        _lvminfo._invalidatelvs(vgName)
    if invalidatePVs:
        _lvminfo._invalidatevgpvs(vgName)


def _getpvblksize(pv):
    dev = os.path.realpath(pv)
    return multipath.getDeviceBlockSizes(dev)


def _checkpvsblksize(pvs, vgBlkSize=None):
    for pv in pvs:
        pvBlkSize = _getpvblksize(pv)
        logPvBlkSize, phyPvBlkSize = pvBlkSize

        if logPvBlkSize not in SUPPORTED_BLOCKSIZE:
            raise se.DeviceBlockSizeError(pvBlkSize)

        if phyPvBlkSize < logPvBlkSize:
            raise se.DeviceBlockSizeError(pvBlkSize)

        if vgBlkSize is None:
            vgBlkSize = pvBlkSize

        if logPvBlkSize != vgBlkSize[0]:
            raise se.VolumeGroupBlockSizeError(vgBlkSize, pvBlkSize)


def checkVGBlockSizes(vgUUID, vgBlkSize=None):
    pvs = listPVNames(vgUUID)
    if not pvs:
        raise se.VolumeGroupDoesNotExist("vg_uuid: %s" % vgUUID)
    _checkpvsblksize(pvs, vgBlkSize)


def getVGBlockSizes(vgUUID):
    pvs = listPVNames(vgUUID)
    if not pvs:
        raise se.VolumeGroupDoesNotExist("vg_uuid: %s" % vgUUID)
    return _getpvblksize(pvs[0])



def createLV(vgName, lvName, size, activate=True, contiguous=False,
             initialTags=(), device=None, read_only=None):
    log.info("Creating LV (vg=%s, lv=%s, size=%sm, activate=%s, "
             "contiguous=%s, initialTags=%s, device=%s, read_only=%s)",
             vgName, lvName, size, activate, contiguous, initialTags, device,
             read_only)
    cont = {True: "y", False: "n"}[contiguous]
    cmd = ["lvcreate"]
    cmd.extend(LVM_NOBACKUP)
    cmd.extend(("--contiguous", cont, "--size", "%sm" % size))
    for tag in initialTags:
        cmd.extend(("--addtag", tag))
    cmd.extend(("--name", lvName, vgName))
    if device is not None:
        cmd.append(_fqpvname(device))
    rc, out, err = _lvminfo.cmd(
        cmd, _lvminfo._getVGDevs((vgName, )), read_only=read_only)

    if rc == 0:
        _lvminfo._invalidatevgs(vgName)
        _lvminfo._invalidatelvs(vgName, lvName)
    else:
        raise se.CannotCreateLogicalVolume(vgName, lvName, err)

    if activate:
        lv_path = lvPath(vgName, lvName)
        st = os.stat(lv_path)
        uName = pwd.getpwuid(st.st_uid).pw_name
        gName = grp.getgrgid(st.st_gid).gr_name
        if ":".join((uName, gName)) != USER_GROUP:
            cmd = [constants.EXT_CHOWN, USER_GROUP, lv_path]
            if misc.execCmd(cmd, sudo=True)[0] != 0:
                log.warning("Could not change ownership of one or more "
                            "volumes in vg (%s) - %s", vgName, lvName)
    else:
        _setLVAvailability(vgName, lvName, "n")


def removeLVs(vgName, lvNames, read_only=None):
    lvNames = _normalizeargs(lvNames)
    log.info("Removing LVs (vg=%s, lvs=%s, read_only=%s)",
             vgName, lvNames, read_only)
    for lvName in lvNames:
        if _isLVActive(vgName, lvName):
            log.warning("Removing active volume %s/%s" % (vgName, lvName))

    cmd = ["lvremove", "-f"]
    cmd.extend(LVM_NOBACKUP)
    for lvName in lvNames:
        cmd.append("%s/%s" % (vgName, lvName))
    rc, out, err = _lvminfo.cmd(
        cmd, _lvminfo._getVGDevs((vgName, )), read_only=read_only)
    if rc == 0:
        for lvName in lvNames:
            _lvminfo._lvs.pop((vgName, lvName), None)
            _lvminfo._invalidatevgs(vgName)
    else:
        _lvminfo._invalidatelvs(vgName, lvNames)
        raise se.CannotRemoveLogicalVolume(vgName, str(lvNames), err)


def extendLV(vgName, lvName, size_mb):
    log.info("Extending LV %s/%s to %s megabytes", vgName, lvName, size_mb)
    cmd = ("lvextend",) + LVM_NOBACKUP
    cmd += ("--size", "%sm" % (size_mb,), "%s/%s" % (vgName, lvName))
    rc, out, err = _lvminfo.cmd(cmd, _lvminfo._getVGDevs((vgName,)))
    if rc != 0:
        vg = getVG(vgName)
        lv = getLV(vgName, lvName)

        extent_size = int(vg.extent_size)
        lv_extents = int(lv.size) // extent_size
        requested_size = size_mb * constants.MEGAB
        requested_extents = (requested_size + extent_size - 1) // extent_size

        if lv_extents >= requested_extents:
            log.debug("LV %s/%s already extended (extents=%d, requested=%d)",
                      vgName, lvName, lv_extents, requested_extents)
            return

        needed_extents = requested_extents - lv_extents
        free_extents = int(vg.free_count)
        if free_extents < needed_extents:
            raise se.VolumeGroupSizeError(
                "Not enough free extents for extending LV %s/%s (free=%d, "
                "needed=%d)"
                % (vgName, lvName, free_extents, needed_extents))

        raise se.LogicalVolumeExtendError(vgName, lvName, "%sm" % (size_mb,))

    _lvminfo._invalidatevgs(vgName)
    _lvminfo._invalidatelvs(vgName, lvName)


def reduceLV(vgName, lvName, size_mb, force=False):
    log.info("Reducing LV %s/%s to %s megabytes (force=%s)",
             vgName, lvName, size_mb, force)
    cmd = ("lvreduce",) + LVM_NOBACKUP
    if force:
        cmd += ("--force",)
    cmd += ("--size", "%sm" % (size_mb,), "%s/%s" % (vgName, lvName))
    rc, out, err = _lvminfo.cmd(cmd, _lvminfo._getVGDevs((vgName,)))
    if rc != 0:
        vg = getVG(vgName)
        lv = getLV(vgName, lvName)

        extent_size = int(vg.extent_size)
        lv_extents = int(lv.size) // extent_size
        requested_size = size_mb * constants.MEGAB
        requested_extents = (requested_size + extent_size - 1) // extent_size

        if lv_extents <= requested_extents:
            log.debug("LV %s/%s already reduced (extents=%d, requested=%d)",
                      vgName, lvName, lv_extents, requested_extents)
            return

        raise se.LogicalVolumeExtendError(vgName, lvName, "%sm" % (size_mb,))

    _lvminfo._invalidatevgs(vgName)
    _lvminfo._invalidatelvs(vgName, lvName)


def activateLVs(vgName, lvNames, refresh=True):
    active = []
    inactive = []
    for lvName in lvNames:
        if _isLVActive(vgName, lvName):
            active.append(lvName)
        else:
            inactive.append(lvName)

    if refresh and active:
        log.info("Refreshing active lvs: vg=%s lvs=%s", vgName, active)
        _refreshLVs(vgName, active)

    if inactive:
        log.info("Activating lvs: vg=%s lvs=%s", vgName, inactive)
        _setLVAvailability(vgName, inactive, "y")


def deactivateLVs(vgName, lvNames):
    toDeactivate = [lvName for lvName in lvNames
                    if _isLVActive(vgName, lvName)]
    if toDeactivate:
        log.info("Deactivating lvs: vg=%s lvs=%s", vgName, toDeactivate)
        _setLVAvailability(vgName, toDeactivate, "n")


def renameLV(vg, oldlv, newlv):
    log.info("Renaming LV (vg=%s, oldlv=%s, newlv=%s)", vg, oldlv, newlv)
    cmd = ("lvrename",) + LVM_NOBACKUP + (vg, oldlv, newlv)
    rc, out, err = _lvminfo.cmd(cmd, _lvminfo._getVGDevs((vg, )))
    if rc != 0:
        raise se.LogicalVolumeRenameError("%s %s %s" % (vg, oldlv, newlv))

    _lvminfo._lvs.pop((vg, oldlv), None)
    _lvminfo._reloadlvs(vg, newlv)


def refreshLVs(vgName, lvNames):
    log.info("Refreshing LVs (vg=%s, lvs=%s)", vgName, lvNames)
    _refreshLVs(vgName, lvNames)


def _refreshLVs(vgName, lvNames):
    cmd = ['lvchange', '--refresh']
    cmd.extend("%s/%s" % (vgName, lv) for lv in lvNames)
    rc, out, err = _lvminfo.cmd(cmd, _lvminfo._getVGDevs((vgName, )))
    _lvminfo._invalidatelvs(vgName, lvNames)
    if rc != 0:
        raise se.LogicalVolumeRefreshError("%s failed" % list2cmdline(cmd))


def addtag(vg, lv, tag):
    log.info("Add LV tag (vg=%s, lv=%s, tag=%s)", vg, lv, tag)
    lvname = "%s/%s" % (vg, lv)
    cmd = ("lvchange",) + LVM_NOBACKUP + ("--addtag", tag) + (lvname,)
    rc, out, err = _lvminfo.cmd(cmd, _lvminfo._getVGDevs((vg, )))
    _lvminfo._invalidatelvs(vg, lv)
    if rc != 0:
        raise se.MissingTagOnLogicalVolume("%s/%s" % (vg, lv), tag)


def changeLVTags(vg, lv, delTags=(), addTags=()):
    log.info("Change LV tags (vg=%s, lv=%s, delTags=%s, addTags=%s)",
             vg, lv, delTags, addTags)
    lvname = '%s/%s' % (vg, lv)
    delTags = set(delTags)
    addTags = set(addTags)
    if delTags.intersection(addTags):
        raise se.LogicalVolumeReplaceTagError(
            "Cannot add and delete the same tag lv: `%s` tags: `%s`" %
            (lvname, ", ".join(delTags.intersection(addTags))))

    cmd = ['lvchange']
    cmd.extend(LVM_NOBACKUP)

    for tag in delTags:
        cmd.extend(("--deltag", tag))

    for tag in addTags:
        cmd.extend(('--addtag', tag))

    cmd.append(lvname)

    rc, out, err = _lvminfo.cmd(cmd, _lvminfo._getVGDevs((vg, )))
    _lvminfo._invalidatelvs(vg, lv)
    if rc != 0:
        raise se.LogicalVolumeReplaceTagError(
            'lv: `%s` add: `%s` del: `%s` (%s)' %
            (lvname, ", ".join(addTags), ", ".join(delTags), err[-1]))


def addLVTags(vg, lv, addTags):
    changeLVTags(vg, lv, addTags=addTags)


def lvPath(vgName, lvName):
    return os.path.join("/dev", vgName, lvName)


def lvDmDev(vgName, lvName):
    lvp = lvPath(vgName, lvName)
    return os.path.basename(os.readlink(lvp))


def _isLVActive(vgName, lvName):
    return os.path.exists(lvPath(vgName, lvName))


def changeVGTags(vgName, delTags=(), addTags=(), read_only=None):
    log.info("Changing VG tags (vg=%s, delTags=%s, addTags=%s, read_only=%s)",
             vgName, delTags, addTags, read_only)
    delTags = set(delTags)
    addTags = set(addTags)
    if delTags.intersection(addTags):
        raise se.VolumeGroupReplaceTagError(
            "Cannot add and delete the same tag vg: `%s` tags: `%s`" %
            (vgName, ", ".join(delTags.intersection(addTags))))

    cmd = ["vgchange"]

    for tag in delTags:
        cmd.extend(("--deltag", tag))
    for tag in addTags:
        cmd.extend(("--addtag", tag))

    cmd.append(vgName)
    rc, out, err = _lvminfo.cmd(
        cmd, _lvminfo._getVGDevs((vgName, )), read_only=read_only)
    _lvminfo._invalidatevgs(vgName)
    if rc != 0:
        raise se.VolumeGroupReplaceTagError(
            "vg:%s del:%s add:%s (%s)" %
            (vgName, ", ".join(delTags), ", ".join(addTags), err[-1]))


def replaceVGTag(vg, oldTag, newTag, read_only=None):
    changeVGTags(vg, [oldTag], [newTag], read_only=read_only)


def getFirstExt(vg, lv):
    return getLV(vg, lv).devices.strip(" )").split("(")


def getVgMetadataPv(vgName):
    pvs = _lvminfo.getPvs(vgName)
    mdpvs = [pv for pv in pvs
             if not isinstance(pv, Stub) and _isMetadataPv(pv)]
    if len(mdpvs) != 1:
        raise se.UnexpectedVolumeGroupMetadata("Expected one metadata pv in "
                                               "vg: %s, vg pvs: %s" %
                                               (vgName, pvs))
    return mdpvs[0].name


def _isMetadataPv(pv):
    return pv.mda_used_count == '2'


def listPVNames(vgName):
    try:
        pvNames = _lvminfo._vgs[vgName].pv_name
    except (KeyError, AttributeError):
        pvNames = getVG(vgName).pv_name
    return pvNames


def setrwLV(vg_name, lv_name, rw=True):
    log.info(
        "Changing LV permissions (vg=%s, lv=%s, rw=%s)", vg_name, lv_name, rw)
    permission = {False: 'r', True: 'rw'}[rw]
    try:
        changelv(vg_name, lv_name, ("--permission", permission))
    except se.StorageException:
        lv = getLV(vg_name, lv_name)
        if lv.writeable == rw:
            return

        raise se.CannotSetRWLogicalVolume(vg_name, lv_name, permission)


def lvsByTag(vgName, tag):
    return [lv for lv in getLV(vgName) if tag in lv.tags]


def invalidateFilter():
    _lvminfo.invalidateFilter()


def replaceLVTag(vg, lv, deltag, addtag):
    log.info("Replacing LV tag (vg=%s, lv=%s, deltag=%s, addtag=%s)",
             vg, lv, deltag, addtag)
    lvname = "%s/%s" % (vg, lv)
    cmd = (("lvchange",) + LVM_NOBACKUP + ("--deltag", deltag) +
           ("--addtag", addtag) + (lvname,))
    rc, out, err = _lvminfo.cmd(cmd, _lvminfo._getVGDevs((vg, )))
    _lvminfo._invalidatelvs(vg, lv)
    if rc != 0:
        raise se.LogicalVolumeReplaceTagError("%s/%s" % (vg, lv),
                                              "%s,%s" % (deltag, addtag))
