#!/usr/bin/env python3

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

import os, argparse
from subprocess import run

"""
diskgroup - Manage Oracle ASM diskgroups
Copyright (c) 2023 - Bart Sjerps <bart@dirty-cache.com>
License: GPLv3+
"""

try:
    from asm import asm
    from switchuser import switchuser
except ImportError as e:
    print("Import failed:", e)
    sys.exit(20)

def getdevs(args):
    devs = []
    for dev in args.devices:
        if not dev.startswith('/'):
            devs.append(os.path.join(args.diskstring, dev))
        else:
            devs.append(dev)
    return ', '.join([f"'{x}'" for x in devs])

def list(args):
    rc, out = asm.sqlplus('SELECT NAME as DISKGROUP FROM V$ASM_DISKGROUP')
    print(out.strip())

def show(args):
    q = """\
SET HEADING ON
SET FEEDBACK OFF
COL DISKGROUP FORMAT A10      head 'Diskgroup'
COL AU        FORMAT A8       head 'AU Size'
COL STATE     FORMAT A10      head 'State'
COL TOTAL_MB  FORMAT 999999   head 'Size (MB)'
COL FREE_MB   LIKE TOTAL_MB   head 'Free (MB)'
COL USED      LIKE TOTAL_MB   head 'Used (MB)'
COL PCTUSED   FORMAT 999.99   head 'Used %'
SELECT NAME as DISKGROUP
, allocation_unit_size/1024/1024 || 'MB' AS AU
, state
, total_mb-free_mb as used
, free_mb
, total_mb
, 100*(total_mb-free_mb)/NULLIF(total_mb,0) as PCTUSED
FROM v$asm_diskgroup
-- where STATE='MOUNTED'
/"""
    asm.run(q)

def disks(args):
    q = """\
SET FEEDBACK OFF
COL DISKGROUP FORMAT A10 head 'Diskgroup'
COL DISKNAME  FORMAT A14 head 'Disk'
COL STATUS    FORMAT A10 head 'Status'
COL PATH      FORMAT A40 head 'Path'
SELECT dg.name  AS DISKGROUP
, d.name        AS DISKNAME
, header_status AS STATUS
, path
FROM gv$asm_disk d
LEFT OUTER JOIN v$asm_diskgroup dg ON d.group_number = dg.group_number AND dg.group_number <> 0
JOIN v$instance ON inst_id=instance_number
ORDER BY diskgroup, diskname, path
/"""
    asm.run(q)

def candidates(args):
    asm.run("set heading off\nselect path from v$asm_disk where header_status in ('FORMER','CANDIDATE');")

def wipe(args):
    _, out = asm.sqlplus("set heading off\nselect path from v$asm_disk where header_status in ('FORMER', 'CANDIDATE');")
    for dev in out.strip().splitlines():
        if args.full:
            fd = os.open(dev, os.O_RDONLY)
            sz = round(os.lseek(fd, 0, os.SEEK_END)/1048576)
            run(['/usr/bin/dd', 'if=/dev/zero', 'bs=1M', f'count={sz}', f'of={dev}'], check=True)
        else:
            run(['/usr/bin/dd', 'if=/dev/zero', 'bs=1M', 'count=1', f'of={dev}'], check=True)

def create(args):
    _, out = asm.sqlplus("set head off\nSELECT substr(version,1,4) FROM V$INSTANCE;")
    compat = out.strip()
    devices = getdevs(args)
    attribs = f"'au_size'='{args.ausize}M', 'compatible.rdbms' = '{compat}', 'compatible.asm'='{compat}'"
    asm.run(f"CREATE DISKGROUP {args.dg} EXTERNAL REDUNDANCY DISK {devices} ATTRIBUTE {attribs};")
    asm.run(f"ALTER DISKGROUP {args.dg} ALTER TEMPLATE ONLINELOG ATTRIBUTES (UNPROTECTED FINE);")

def addvol(args):
    devices = getdevs(args)
    asm.run(f"ALTER DISKGROUP {args.dg} ADD DISK {devices};")

def dropvol(args):
    disks = ', '.join([f"'{x}'" for x in args.disks])
    asm.run(f"ALTER DISKGROUP {args.dg} DROP DISK {disks};")

def drop(args):
    asm.run(f"DROP DISKGROUP {args.dg};")

def mount(args):
    asm.run(f"ALTER DISKGROUP {args.dg} MOUNT;")

def mountall(args):
    _, out = asm.sqlplus("set head off\nselect name from gv$asm_diskgroup where state != 'MOUNTED';")
    for dg in out.strip().splitlines():
        asm.run(f"ALTER DISKGROUP {dg} MOUNT;")

def umount(args):
    asm.run(f"ALTER DISKGROUP {args.dg} DISMOUNT;")

def ops(args):
    asm.run(f"SELECT * FROM V$ASM_OPERATION")

def rebalpower(args):
    asm.run(f"ALTER DISKGROUP {args.dg} REBALANCE POWER 8 NOWAIT")

def rename(args):
    cmd = f"renamedg phase=both dgname={args.dg} newdgname={args.newdg} verbose=true asm_diskstring={args.diskstring}"
    run(cmd.split(), env={'ORACLE_HOME': '/u01/app/19.0.0/grid', 'PATH': '/u01/app/19.0.0/grid/bin' }, check=True)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()

    parent = argparse.ArgumentParser(add_help=False)
    parent.add_argument("dg", metavar="DISKGROUP")

    subparsers = parser.add_subparsers(title='commands', dest='cmd')

    parser.add_argument("-D", "--debug", action="store_true")
    parser.add_argument(      "--diskstring", help="ASM_DISKSTRING", default="/dev/oracleasm", metavar='PATH')
    parser.add_argument(      "--ausize", help="AU Size", default=8, metavar='SIZE')

    parsers = []
    parser_list    = subparsers.add_parser('list',       help='List Diskgroups', description='List desc')
    parser_disks   = subparsers.add_parser('disks',      help='List ASM Disks', description='Disks List')
    parser_show    = subparsers.add_parser('show',       help='Show Details', description='Show DG details')
    parser_cand    = subparsers.add_parser('candidates', help='Show candidate disks')
    parser_wipe    = subparsers.add_parser('wipe',       help='Wipe former disks')
    parser_create  = subparsers.add_parser('create',     parents=[parent], help='Create Diskgroup', description='Create diskgroup')
    parser_drop    = subparsers.add_parser('drop',       parents=[parent], help='Drop Diskgroup', description='Drop diskgroup')
    parser_addvol  = subparsers.add_parser('addvol',     parents=[parent], help='Add volume', description='Add volume')
    parser_dropvol = subparsers.add_parser('dropvol',    parents=[parent], help='Drop volume', description='Drop volume')
    parser_mount   = subparsers.add_parser('mount',      parents=[parent], help='Mount DG', description='Mount diskgroup')
    parser_mounta  = subparsers.add_parser('mountall',   help='Mount all diskgroups', description='Mount all diskgroups')
    parser_umount  = subparsers.add_parser('unmount',    parents=[parent], help='Unmount DG', description='Unmount diskgroup')
    parser_rename  = subparsers.add_parser('rename',     parents=[parent], help='Rename DG', description='Rename diskgroup')
    parser_ops     = subparsers.add_parser('ops',        help='Show Ops', description='Show Operations')

    parser_list.set_defaults(func=list)
    parser_disks.set_defaults(func=disks)
    parser_show.set_defaults(func=show)
    parser_cand.set_defaults(func=candidates)
    parser_wipe.set_defaults(func=wipe)
    parser_create.set_defaults(func=create)
    parser_drop.set_defaults(func=drop)
    parser_addvol.set_defaults(func=addvol)
    parser_dropvol.set_defaults(func=dropvol)
    parser_mount.set_defaults(func=mount)
    parser_mounta.set_defaults(func=mountall)
    parser_umount.set_defaults(func=umount)
    parser_rename.set_defaults(func=rename)
    parser_ops.set_defaults(func=ops)

    parser_create.add_argument('devices', nargs='*')
    parser_addvol.add_argument('devices', nargs='*')
    parser_dropvol.add_argument('disks', nargs='*')
    parser_rename.add_argument('newdg', metavar="NEWNAME")
    parser_wipe.add_argument('-f', '--full', action="store_true")

    args = parser.parse_args()

    try:
        if args.cmd is None:
            switchuser('grid')
            list(args)
        else:
            switchuser('grid')
            args.func(args)
    except ValueError as e:
        print(e)
        sys.exit(10)

