#!/usr/bin/env python3

"""\
ASMDisks - Manage Linux disks using UDEV
Copyright (c) 2023 - Bart Sjerps <bart@dirty-cache.com>
URL: https://github.com/bsjerps/asmdisks
License: GPLv3+
"""

__version__   = '2.0.1'

helptext = """
asm is a replacement for the 'oracleasm' command provided via Oracle ASMlib.
It creates and manages devices using Linux UDEV, by default created as symlinks
in /dev/oracleasm with owner 'grid', group 'asmdba' and mode 0660.
The device configuration is stored in a sqlite database.

"""

import sys
sys.dont_write_bytecode = True
sys.path.append('/usr/lib/asmdisks')

import os, re, json, argparse, pwd, grp
from sqlite3 import OperationalError

from sqlitedb import Database
from rulesfile import RulesFile
from device import Device
from pretty import Pretty

sqlitedb   = '/etc/asmtab.db'
exportfile = '/tmp/asmdisks.json'
rulespath  = '/etc/udev/rules.d/98-asm.rules'

db = Database(sqlitedb)

def regsearch(pat, line):
    r = re.search(pat, line)
    if r:
        return r.group(1)

def rescan(args):
    print('Rescanning SCSI devices')
    Device.rescan()
    
def reload():
    print('Updating UDEV rules')
    RulesFile.reload()

def listdisks(args):
    header = ('Device','SCSI','Size','Type','Target')
    data = []
    for devpath in Device.getdisks():
        dev = Device(f'/dev/{devpath}')
        data.append((dev.dev, dev.scsi, dev.size, dev.contents, dev.symlink))
    
    p = Pretty(header, data)
    p.print(args)

def listvolumes(args):
    header, data = db.volumes.report('volname, disktype, identifier', orderby='volname')
    p = Pretty(header, data)
    p.print(args)

def createdisk(args):
    dev        = Device(args.dev)
    driver     = dev.driver
    identifier = dev.serial
    if not dev.contents in ('asm','blank'):
        raise ValueError("Disk is not empty or an ASM disk")
    db.volumes.insert(volname=args.vol, disktype=driver, identifier=identifier)
    rebuild(args)

def deletedisk(args):
    db.volumes.delete(volname=args.vol)
    rebuild(args)
   
def renamedisk(args):
    if not args.force:
        raise ValueError('Force flag required')
    db.volumes.update({ 'volname': args.newname }, volname=args.vol)
    rebuild(args)

def genvol():
    for i in range(1,99):
        newname = f'vol{i:02}'
        r = db.volumes.get(volname=newname)
        if not r:
            return newname

def exportall(args):
    info = {}
    info['application'] = 'asmdisks'
    info['version'] = __version__
    info['metadata'] = {}
    info['volumes'] = []
    r = db.metadata.get(id=0)
    info['metadata'] = { k: getattr(r, k) for k in r._fields}
    for r in db.volumes.select('volname,disktype,identifier'):
        info['volumes'].append({ k: getattr(r, k) for k in r._fields})

    with open(exportfile, 'w') as f:
        f.write(json.dumps(info, indent=2))

def importall(args):
    with open(exportfile) as f:
        info = json.loads(f.read())
    
    db.metadata.update(info['metadata'], id=0)
    for volinfo in info['volumes']:
        db.volumes.insert(**volinfo)

def importasm(args):
    for devpath in Device.getdisks():
        dev = Device(f'/dev/{devpath}')
        if dev.contents == 'asm':
            newvol = genvol()
            db.volumes.insert(volname=newvol, disktype=dev.driver, identifier=dev.serial)
    rebuild(args)

def importrules(args):
    with open(args.rulesfile) as f:
        data = f.read()
    for r, symlink in re.findall(r'OWNER=\S+, GROUP=\S+, MODE=\S+, (.*), SYMLINK\+="(.*)"', data, re.M):
        volname = symlink.replace('oracleasm/','')
        m1 = re.match('KERNEL=="sd\*", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL}=="(\S+)"', r)
        m2 = re.match('ENV{ID_WWN}=="(\S+)"', r)
        if m1:
            serial = m1.group(1)
            db.volumes.insert(volname=volname, disktype='sd', identifier=serial)
        elif m2:
            serial = m2.group(1)
            db.volumes.insert(volname=volname, disktype='blkext', identifier=serial)

def rebuild(args):
    rulesfile = RulesFile(db, rulespath)
    rulesfile.save()
    reload()

def dump(args):
    rulesfile = RulesFile(db, rulespath)
    rulesfile.dump()

def setup(args):
    db.schema()

def configure(args):
    if args.show:
        meta = db.metadata.get(id=0)
        print(f"diskstring: /dev/{meta.diskstring}, user: {meta.user}, group: {meta.grp}, mode: {oct(meta.mode)[2:]}")
    elif args.reset:
        db.metadata.update({ 'user': 'grid', 'grp': 'asmdba', 'diskstring': 'oracleasm', 'mode': 0o660 }, id=0)
    elif args.user:
        try:
            pwd.getpwnam(args.user)
        except:
            raise ValueError('User does not exist')
        db.metadata.update({ 'user': args.user }, id=0)
    elif args.group:
        try:
            grp.getgrnam(args.group)
        except:
            raise ValueError('Group does not exist')
        db.metadata.update({ 'grp': args.group }, id=0)
    elif args.diskstring:
        if not args.diskstring.startswith('/dev'):
            raise ValueError("Must start with /dev")
        _, _, basename = args.diskstring.partition('/dev/')
        if not basename.replace('/','').isidentifier():
            raise ValueError(f"Bad dirname {basename}")
        db.metadata.update( { 'diskstring': basename }, id=0)

def destroy(args):
    if not args.force:
        raise ValueError('Force flag required')
    db.destroy()

def main():
    formatter = lambda prog: argparse.HelpFormatter(prog, max_help_position=40)
    parser = argparse.ArgumentParser(epilog = helptext, description='Manage disks and volumes for Oracle ASM', formatter_class=formatter)
    subparsers = parser.add_subparsers(title='commands', dest='cmd')

    parser_rescan    = subparsers.add_parser('rescan',      help='Rescan Linux devices')
    parser_list      = subparsers.add_parser('list',        help='List volumes', description='List items')
    parser_disks     = subparsers.add_parser('disks',       help='List disks', description='List Disks')
    parser_create    = subparsers.add_parser('createdisk',  help='Create UDEV disk')
    parser_delete    = subparsers.add_parser('deletedisk',  help='Delete UDEV disk')
    parser_rename    = subparsers.add_parser('renamedisk',  help='Rename UDEV disk')
    parser_export    = subparsers.add_parser('export',      help=f'Export to {exportfile}')
    parser_import    = subparsers.add_parser('import',      help=f'Import from {exportfile}')
    parser_imprules  = subparsers.add_parser('importrules', help=f'Import from rulesfile')
    parser_impasm    = subparsers.add_parser('importasm',   help='Import existing ASM disks')
    parser_rebuild   = subparsers.add_parser('rebuild',     help='Rebuild UDEV rules file')
    parser_dump      = subparsers.add_parser('dump',        help='Dump rulesfile')
    parser_setup     = subparsers.add_parser('setup',       help='Setup asmdisks database')
    parser_config    = subparsers.add_parser('configure',   help='Configure options')
    parser_destroy   = subparsers.add_parser('destroy',     help='Destroy database')
 
    parser_rescan.set_defaults(func=rescan)
    parser_list.set_defaults(func=listvolumes)
    parser_disks.set_defaults(func=listdisks)
    parser_create.set_defaults(func=createdisk)
    parser_delete.set_defaults(func=deletedisk)
    parser_rename.set_defaults(func=renamedisk)
    parser_export.set_defaults(func=exportall)
    parser_import.set_defaults(func=importall)
    parser_imprules.set_defaults(func=importrules)
    parser_impasm.set_defaults(func=importasm)
    parser_rebuild.set_defaults(func=rebuild)
    parser_dump.set_defaults(func=dump)
    parser_setup.set_defaults(func=setup)
    parser_config.set_defaults(func=configure)
    parser_destroy.set_defaults(func=destroy)
    
    group = parser_config.add_mutually_exclusive_group(required=True)
    group.add_argument('--show',       help="Show configuration", action="store_true")
    group.add_argument('--reset',      help="Switch to defaults", action="store_true")
    group.add_argument('--user',       help="ASM disk owner [grid]", metavar='username')
    group.add_argument('--group',      help="ASM disk group [asmdba]", metavar='groupname')
    group.add_argument('--diskstring', help="ASM diskstring [/dev/oracleasm]", metavar='path')

    parser.add_argument('-V', '--version', action="store_true", help="Version and copyright info")
    parser.add_argument('-t','--tabs',     action="store_true", help="Tab separated output")
    parser.add_argument('-n','--nohead',   action="store_true", help="No headings (if -t)")

    parser_create.add_argument('vol', help="Volume name")
    parser_create.add_argument('dev', help="Block device")

    parser_delete.add_argument('vol', help="Volume name")

    parser_rename.add_argument('vol', help="Volume name")
    parser_rename.add_argument('newname', help="New volume name")
    parser_rename.add_argument('--force',   action="store_true", help="Required")

    parser_imprules.add_argument('rulesfile')
    parser_import.add_argument('file')

    parser_destroy.add_argument('--force',   action="store_true", help="Required")

    args = parser.parse_args()
    if args.version:
        print(__doc__)
        print(f'Version {__version__}')
        sys.exit()

    try:
        if args.cmd is None:
            listdisks(args)
        else:
            args.func(args)
    except OperationalError as e:
        print(f"Database error: {e}")
    except ValueError as e:
        print(e)
        sys.exit(10)

if __name__ == '__main__':
    main()