#!/usr/bin/env python3

import sys
sys.dont_write_bytecode = True

import os, re, pwd, grp, argparse
from time import sleep
from subprocess import run, PIPE, CalledProcessError
from prettytable import PrettyTable

"""
clusterware - Wrapper for managing Oracle clusterware
Copyright (c) 2023 - Bart Sjerps <bart@dirty-cache.com>
License: GPLv3+
"""

class OraEnv():
    __slots__ = ('orahome', 'orabin', 'orabase')
    def __init__(self):
        try:
            with open('/etc/oraInst.loc') as f:
                orainst = f.read()
                r = re.match(r'^inventory_loc=(\S+)', orainst)
                if not r:
                    print('Inventory not found')
                    sys.exit()
            invdir = r.group(1)
            with open(os.path.join(invdir, 'ContentsXML', 'inventory.xml')) as f:
                inv = f.read()
            for d in re.findall(r'^\<HOME NAME="\S+"\s+LOC="(.*)"\s+TYPE=.*', inv, re.M):
                if os.path.exists(os.path.join(d, 'bin', 'crsctl.bin')):
                    self.orahome = d
                    self.orabin  = os.path.join(self.orahome, 'bin')
                    r = run(os.path.join(self.orabin, 'orabase'), env={ 'ORACLE_HOME': self.orahome}, check=True, stdout=PIPE, encoding='utf-8')
                    self.orabase = r.stdout.strip()
                    
        except Exception as e:
            print("Oracle Environment initialization failed")
            raise

    @property
    def diagdir(self):
        return os.path.join(self.orabase, 'diag')

    def shell(self, cmd):
        try:
            env = { 'ORACLE_HOME': self.orahome, 'ORACLE_BASE': self.orabase, 'ORACLE_SID': '+ASM', 'PATH': f'/usr/sbin:/usr/bin:{self.orahome}/bin' }
            print(f'Running: {cmd}')
            r = run(cmd, env=env, check=True, shell=True)
        
        except CalledProcessError:
            print('Command failed')
            sys.exit(10)
    
    def run(self, cmd):
        try:
            env = { 'ORACLE_HOME': self.orahome, 'ORACLE_BASE': self.orabase, 'ORACLE_SID': '+ASM', 'PATH': f'/usr/sbin:/usr/bin:{self.orahome}/bin' }
            r = run(cmd, env=env, check=True, stdout=PIPE, encoding='utf-8')
            return r.stdout
        except CalledProcessError:
            print('Command failed')
            sys.exit(10)

def sugrid():
    user   = 'grid'
    uid    = pwd.getpwnam(user).pw_uid
    gid    = pwd.getpwnam(user).pw_gid
    groups = [gid] + [g.gr_gid for g in grp.getgrall() if user in g.gr_mem]
    os.setgid(gid)
    os.setgroups(groups)
    os.setuid(uid)

def deconfigure(oraenv, args):
    oraenv.shell("$ORACLE_HOME/perl/bin/perl -I $ORACLE_HOME/perl/lib -I $ORACLE_HOME/crs/install $ORACLE_HOME/crs/install/roothas.pl -deconfig -force")

def setupcrs(oraenv, args):
    oraenv.shell("$ORACLE_HOME/perl/bin/perl -I $ORACLE_HOME/perl/lib -I $ORACLE_HOME/crs/install $ORACLE_HOME/crs/install/roothas.pl")
    oraenv.shell("srvctl add listener")
    oraenv.shell("srvctl start listener")
    sleep(1)
    status(oraenv, args)

def setupasm(oraenv, args):
    createdir = os.path.join(pwd.getpwnam('grid').pw_dir, 'create')

    setuporatab(oraenv)
    sugrid()
    os.makedirs(createdir, exist_ok=True)
    os.makedirs(oraenv.diagdir, exist_ok=True)

    oraenv.shell(f'orapwd file=$ORACLE_HOME/dbs/orapw+ASM password={args.password} format=12')
    oraenv.shell('srvctl add asm')
    oraenv.shell('srvctl start asm')

    with open(os.path.join(createdir, 'asmpfile'), 'w') as f:
        f.write("asm_diskstring='/dev/oracleasm'\n"
                "asm_power_limit=5\n"
                f"diagnostic_dest='{oraenv.diagdir}'\n"
                "instance_type='asm'\n"
                "large_pool_size=12M\n"
                "remote_login_passwordfile='EXCLUSIVE'\n")

    with open(os.path.join(createdir, 'createasm.sql'), 'w') as f:
        f.write(f"create spfile='{oraenv.orahome}/dbs/spfile+ASM.ora' from pfile='/home/grid/create/asmpfile';\n"
                "create user asmsnmp identified by oracle#12;\n"
                "alter system set asm_diskstring='/dev/oracleasm';\n"
                "grant sysdba to asmsnmp;\n"
                "grant sysasm to asmsnmp;\n"
                "exit;\n")

    oraenv.shell("sqlplus -L -S / as sysasm @/home/grid/create/createasm.sql")
    oraenv.shell(f"srvctl modify asm -p {oraenv.orahome}/dbs/spfile+ASM.ora")

def setup(oraenv, args):
    setupcrs(oraenv, args)
    setupasm(oraenv, args)

def setuporatab(oraenv):
    with open('/etc/oratab') as f:
        oratab = f.read()

    if re.search(r'^(\+ASM)\:(.*)', oratab, re.M):
        return

    with open('/etc/oratab', 'a') as f:
        print("Adding oratab entry")
        oratab = f.write(f'+ASM:{oraenv.orahome}:N\n')

def stop(oraenv, args):
    oraenv.shell("crsctl stop has")

def start(oraenv, args):
    oraenv.shell("crsctl start has")

def status(oraenv, args):
    pattern = re.compile(r'\s+')
    stripwhite = lambda x: re.sub(pattern, '', x).strip().rstrip(',')

    data = oraenv.run(['crsctl', 'stat', 'res'])
    tbl = PrettyTable()

    tbl.field_names = ('Resource Name','Resource Type','Target','State')
    for r in re.finditer(r'NAME=(\S+)\nTYPE=(\S+)\nTARGET=(.*)\nSTATE=(.*)', data):
        res, rtype, target, state = r.groups()
        tbl.add_row((res, rtype, stripwhite(target), state))

    tbl.align = "r"
    tbl.align['Resource Name'] = 'l'
    print(tbl)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Manage Oracle Clusterware (single instance)")
    parser.add_argument('-D', '--debug', help="Debug", action="store_true")
    parser.add_argument("--password", help="ASM password", metavar='orapw', default='oracle#12')

    subparsers = parser.add_subparsers(title='commands')
    parser_status = subparsers.add_parser('status',     help='Status', description='Status D')
    parser_stop   = subparsers.add_parser('stop',       help='Stop clusterware')
    parser_start  = subparsers.add_parser('start',      help='Start clusterware')
    parser_setup  = subparsers.add_parser('setup',      help="Setup CRS and ASM for single instance")
    parser_setupc = subparsers.add_parser('setupcrs',   help="Setup CRS for single instance")
    parser_setupa = subparsers.add_parser('setupasm',   help="Setup ASM for single instance")
    parser_deconf = subparsers.add_parser('deconfig',   help='Deconfigure clusterware')

    parser_status.set_defaults(func=status)
    parser_stop.set_defaults(func=stop)
    parser_start.set_defaults(func=start)
    parser_setup.set_defaults(func=setup)
    parser_setupc.set_defaults(func=setupcrs)
    parser_setupa.set_defaults(func=setupasm)
    parser_deconf.set_defaults(func=deconfigure)

    args = parser.parse_args()

    try:
        oraenv = OraEnv()
        if hasattr(args, 'func'):
            args.func(oraenv, args)
        else:
            status(oraenv, args)
    except Exception as e:
        if args.debug:
            raise
        print(e)
        sys.exit(10)
