#!/usr/bin/env python

# This script checks local filesystems managed by LVM for those that
# are close to their limits. Ones that are are automatically enlarged 
#
# Defaults are configured in /usr/local/etc/qexpand.conf, or in the 
# argument to the "-c/--conffile" switch.
#
# By default, all logical volumes in all volume groups are checked. If
# any are more than 80% full or have less than 512MiB remaining, they are
# increased in size by 10% or 512MiB (whichever is larger).

import getopt, os, popen2, re, sys, tempfile, types

def usage():
    print "Usage: sys.argv[0]"+""" [-h][-c|--conffile=filename]"""


def getVolumeGroup(name):
    name = name.strip()
    if volumeGroup.all.has_key(name):
        return volumeGroup.all[name]
    return volumeGroup(name)

class volumeGroup:
    all = {} # dict of all VGs

    def __init__(self, name):
        name = name.strip()
        if self.all.has_key(name):
            raise ValueError("volumeGroup already exists: "+name)
        self.name = name
        self.all[name] = self
        self.logicalVolumes = []
        """p = popen2.Popen3((sbin+"lvs","--noheadings","-o","lv_name"))
        lvs = p.fromchild.readlines()
        if p.wait():
            raise OSError
        self.logicalVolumes = {}
        for lv in lvs:
            lv = lv.strip()
            self.logicalVolumes[lv] = getLogicalVolume(lv,self.name)"""
    def free(self):
        p = popen2.Popen3((sbin+"vgs","--units","m","--noheadings",
                      "-o","vg_free","--nosuffix",self.name))
        f = int(float(p.fromchild.readline().strip()))
        if p.wait():
            raise OSError
        #print self.name,"free:",f
        return f

def getLogicalVolume(lv,vg = None):
    if not vg:
        cn = lv.strip()
    else:
        cn = vg.strip()+"/"+lv.strip()
        
    if logicalVolume.all.has_key(cn):
        return logicalVolume.all[cn]
    return logicalVolume(cn)

class logicalVolume:
    all = {} # Dict of all LVs. Keys are canonical names (vg/lv)
    volumeGroup = None

    def __init__(self, name, vg=None):
        if type(name) != type(""):
            raise TypeError
        name = name.strip()
        if vg:
            if type(vg) == type(""):
                self.volumeGroup = getVolumeGroup(vg)
            else:
                self.volumeGroup = vg
        pcs = name.split("/")
        if len(pcs)>1:
            if pcs[1] == "dev":
                # Device format
                if pcs[2] == "mapper":
                    # Form /dev/mapper/vgname-lvname
                    # hiphens in vgname or lvname are doubled, so we need
                    # to split on a hiphen that is not followed or preceeded
                    # by another hiphen.
                    (vgn,lvn) = re.split(r"(?<!-)-(?!-)",pcs[3])
                    vgn = re.sub("--","-",vgn)
                    lvn = re.sub("--","-",lvn)
                else:
                    # Form /dev/vgname/lvname
                    (vgn,lvn) = pcs[2:4]
            else:
                (vgn,lvn) = pcs

            self.name = lvn
            if self.volumeGroup:
                if self.volumeGroup.name != vgn:
                    raise ValueError(
                        "Volume group ("+self.volumeGroup.name+
                        ") inconsistent with logical volume name ("+
                        name+")" )
            else:
                self.volumeGroup = getVolumeGroup(vgn)
        else:
            if not self.volumeGroup:
                raise ValueError(
                    "VG not provided and cannot be determined from name")
            self.name = name
        self.volumeGroup.logicalVolumes.append(self)
        self.cn = self.volumeGroup.name+"/"+self.name
        if logicalVolume.all.has_key(self.cn) and \
                logicalVolume.all[self.cn] != None:
            raise ValueError("logicalVolume already exists: "+self.cn)
        logicalVolume.all[self.cn] = self
        try:
            self.device=os.readlink("/dev/"+self.cn)
        except OSError:
            self.device="/dev/mapper/"+\
                re.sub("-","--",self.volumeGroup.name)+"/"+\
                re.sub("-","--",self.name)
        p = popen2.Popen3(("/usr/bin/file","-Ls",self.device))
        line = p.fromchild.readlines()[0]
        if p.wait():
            raise OSError
        self.type = line.split(":",1)[1].strip()
        if self.type.find("SGI XFS filesystem data")>-1:
            def __growFS(self):
                umount = 0
                mp = self.mount()
                if os.spawnlp(os.P_WAIT,"xfs_growfs","xfs_growfs",mp):
                    raise OSError("Failed to grow XFS filesystem "+
                                  self.device+" mounted on "+mp)
                if umount:
                    if os.spawnlp(os.P_WAIT,"umount","umount",mp):
                        raise OSError("Failed to umount "+self.device+
                                      " from "+mp)
                    os.rmdir(mp)
            self.growFS = __growFS
        elif self.type.find("ext3 filesystem data") > -1:
            def __growFS(self):
		if not self.mountpoint():
                    if os.spawnlp(os.P_WAIT,"e2fsck","e2fsck","-f",self.device):
                        raise OSError("Failed to fsck"+self.device)
                if os.spawnlp(os.P_WAIT,"resize2fs","resize2fs",self.device):
                    raise OSError("Failed to resize e3fs "+self.device)
            self.growFS = __growFS
        else:
            self.growFS = None
        self.__tmpMount = None
    def mountpoint(self):
        for mount in open("/proc/mounts","r").readlines():
            dev,mp,rest = mount.split(None,2)
            if dev == self.device or dev == "/dev/"+self.cn:
                return mp
        return None
    def mount(self):
        mp = self.mountpoint()
        if not mp:
            mp = tempfile.mkdtemp()
            if os.spawnlp(os.P_WAIT,"mount","mount",self.device,mp):
                raise OSError("Failed to mount "+self.device+" on "+mp)
            self.__tmpMount = mp
        return mp
    def umount(self):
        if self.__tmpMount:
            if os.spawnlp(os.P_WAIT,"umount","umount",self.__tmpMount):
                raise OSError("Failed to umount "+self.device+
                              " from "+self.__tmpMount)
            os.rmdir(self.__tmpMount)
            self.__tmpMount = None
    def __repr__(self):
        return self.cn
    def usage(self):
        self.mount()
        p = popen2.Popen3(("/bin/df","-P","-B","1M",self.device))
        (dev,size,used,free,pct,mountpoint) = \
            p.fromchild.readlines()[-1].split()
        if p.wait():
            self.umount()
            raise OSError
        self.umount()
        return (int(size),int(used))
    def grow(self,amount):
        report(("Trying to grow",self.cn,"by",amount))
        if not self.growFS:
            raise TypeError("LV does not support growth")
        if self.volumeGroup.free() < amount:
            raise ValueError("Insufficent space in VG")
        print self.cn,"growing by",amount
        if os.spawnlp(os.P_WAIT, 
                      "lvextend","lvextend","-L","+"+str(amount),self.cn):
            raise OSError("Failed to expand logical volume "+self.cn+" by "+
                          str(amount))
        self.growFS(self)

def report(message):
    return
    for m in message:
        print m,
    print
 
# location of lv binaries
sbin="/sbin/"
def main():
    # A file from which to read the default configuration.
    confFile = "/usr/local/etc/qexpand.conf"
    # A list of LVs to check.
    volumes = None
    # Minimum free space, expressed as a percentage. If free space of a
    # filesystem goes below this amount, allocation will occur.
    freePC = 20
    # Minimum free space, expressed in mebibytes. Note that if either the
    # percentage or the absolute spacee gets too low, allocation occurs.
    freeMB = 512
    # Growth rate, expressed as a percentage.
    growPC = 10
    # Growth rate in MiB. Real growth will be the larger of growPC and growMB
    growMB = 512
    # List of LVs to not try to grow (regular expression tries to match
    # against both the name and cn)
    lvIgnore = ["/swap[0-9]*","/.*-[0-9]-.*"]
 
    try:
        opts, args = getopt.gnu_getopt(sys.argv[1:],
                                       "hc:v:f:g:",
                                       ["help","conffile=","volumegroup=",
                                        "vg=","free=","minfree=","grow="])
    except getopt.GetoptError:
        usage()
        sys.exit(2)

    # Check flags first to see if help was requested or if an alternate
    # conffile was provided.
    for o,a in opts:
        if o in ("-h","--help"):
            usage()
            sys.exit()
        elif o in ("-c","--conffile"):
            confFile = a

    try:
        confFP = open(confFile)
    except:
        pass
    else:
        exec(confFP)

    # Now process the rest of the arguments so they can override the 
    # configuration file
    for o,a in opts:
        if o in ("-v","--volumes"):
            volumes = [ logicalVolume(i) for i in a.split(",")]
        elif o in ("-f","--free","--minfree"):
            if a[-1] == "%":
                freePC = int(a[:-1])
            else:
                freeMB = int(a)
        elif o in ("-g","--grow"):
            if a[-1] == "%":
                growPC = int(a[:-1])

            else:
                growMB = int(a)
    
    if not volumes:
        p = popen2.Popen3((sbin+"lvs","--noheadings","-o","vg_name,lv_name",
                      "--separator","/"))
        lines = [ l.strip() for l in p.fromchild.readlines() ]
        volumes = [ getLogicalVolume(l) for l in lines]
        if p.wait():
            raise OSError

    for lv in volumes:
        try:
            for nameRE in lvIgnore:
                if re.search(nameRE,lv.name) or re.search(nameRE,lv.cn):
		    report(("Ignoring:",lv))
                    raise StopIteration
        except StopIteration:
            continue
        report(("Considering:",lv,"of type",lv.type,"mounted on",lv.mountpoint()))
        size,used = lv.usage()
        report(("\tused:",used,"/",size))
        if size - used < freeMB or 100-100*used/size < freePC:
            gpc = growPC * size / 100
            if gpc < growMB:
                grow = growMB
            else:
                grow = gpc
            try:
                lv.grow(grow)
            except TypeError:
                continue

if __name__ == "__main__":
    main()
