#!/usr/bin/env python3

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

import os, re, pwd, grp, argparse, logging
from subprocess import run, Popen, PIPE, CalledProcessError
from multiprocessing import Process, Queue

"""
oracle-install - Install Oracle Database/Grid SW
Copyright (c) 2023 - Bart Sjerps <bart@dirty-cache.com>
License: GPLv3+
"""

tmpdir = '/u01/tmp'
staging = os.path.join(tmpdir, 'staging')
grid_staging = os.path.join(tmpdir, 'grid-staging')
database_staging = os.path.join(tmpdir, 'database-staging')

logging.basicConfig(level=logging.INFO,
    format="%(levelname)-8s: %(message)s",
    datefmt='%Y-%m-%d %H:%M:%S')

def switchuser(user):
    if not os.getuid() == 0:
        return
    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)

zipfiles = {
    '11.2.0.4.0': {
        'grid': {
            'tgtdir': '/u01/tmp/staging',
            'zipfiles': [ 'p13390677_112040_Linux-x86-64_1of7.zip', 'p13390677_112040_Linux-x86-64_2of7.zip' ]
        },
        'database': {
            'tgtdir': '/u01/tmp/staging',
            'zipfiles': [ 'p13390677_112040_Linux-x86-64_3of7.zip', 'p13390677_112040_Linux-x86-64_4of7.zip' ]
        },
    },
    '12.1.0.2.0': {
        'grid': { 
            'tgtdir': '/u01/tmp/staging',
            'zipfiles': [ 'p17694377_121020_Linux-x86-64_1of8.zip', 'p17694377_121020_Linux-x86-64_2of8.zip' ]
        },
        'database': {
            'tgtdir': '/u01/tmp/staging',
            'zipfiles': ['p17694377_121020_Linux-x86-64_3of8.zip', 'p17694377_121020_Linux-x86-64_4of8.zip' ]
        }
    },
    '12.2.0.1.0': {
        'grid': {
            'tgtdir': '/u01/tmp/grid-staging',
            'zipfiles': [ 'linuxx64_12201_grid_home.zip' ]
        },
        'database': {
            'tgtdir': '/u01/tmp/database-staging',
            'zipfiles': [ 'linuxx64_12201_database.zip' ]
        }
    },
    '18.0.0.0.0': {
        'grid': {
            'tgtdir': '/u01/tmp/grid-staging',
            'zipfiles': [ 'LINUX.X64_180000_grid_home.zip' ]
        },
        'database': {
            'tgtdir': '/u01/tmp/database-staging',
            'zipfiles': [ 'LINUX.X64_180000_db_home.zip' ]
        }
    },
    '19.0.0.0.0': {
        'grid': {
            'tgtdir': '/u01/tmp/grid-staging',
            'zipfiles': [ 'LINUX.X64_193000_grid_home.zip' ]
        },
        'database': {
            'tgtdir': '/u01/tmp/database-staging',
            'zipfiles':  [ 'LINUX.X64_193000_db_home.zip' ],
        },
    }
}

def destroy(args):
    if not args.force:
        raise ValueError("Must use -f (force) option")
     
    proc = Popen(['pgrep','-u', 'grid'], stdout=PIPE, stderr=PIPE)
    out, err = proc.communicate()
    if out:
        raise ValueError("grid processes still active (stop clusterware?)")

    run(['/bin/rm', '-f',
        '/usr/local/bin/oraenv', '/usr/local/bin/coraenv', '/usr/local/bin/dbhome',
        '/etc/init/oracle-ohasd.conf', '/etc/init.d/ohasd', '/etc/init.d/init.ohasd', '/etc/oratab', '/etc/oraInst.loc'])
    run(['/bin/rm', '-rf', '/etc/oracle', '/opt/ORCLfmap', '/var/tmp/.oracle' ])
    run(['/bin/rm', '-rf', '/u01/app', '/u01/tmp'])

    for (root,dirs,files) in os.walk('/etc/rc.d'):
        for file in files:
            if file.endswith('ohasd'):
                path = os.path.join(root, file)
                os.unlink(path)

def unzip_sub(args, user, path, tgtdir):
    switchuser(user)
    if os.path.isdir(tgtdir):
        logging.error(f"Path already exists: {tgtdir}")
        return
    cmd = ['/usr/bin/unzip', '-n', path, '-d', tgtdir]
    if not args.verbose:
        cmd.insert(1, '-q')
    run(cmd, check=True)

def unzip(args):
    os.makedirs('/u01/tmp', exist_ok=True)
    os.makedirs('/u01/app', exist_ok=True)
    os.chown('/u01/tmp', 500,501)
    os.chown('/u01/app', 500,501)
    os.chmod('/u01/tmp', 0o6775)
    os.chmod('/u01/app', 0o2775)

    jobs = []
    if not args.database:
        meta = zipfiles[args.version]['grid']
        for zipfile in meta['zipfiles']:
            path = os.path.join(args.zipdir, zipfile)
            tgtdir = meta['tgtdir']
            logging.info(f'Unpacking {path} to {tgtdir}')
            proc = Process(target=unzip_sub, args=(args, 'grid', path, tgtdir))
            proc.start()
            jobs.append(proc)

    if not args.grid:
        meta = zipfiles[args.version]['database']
        for zipfile in meta['zipfiles']:
            path = os.path.join(args.zipdir, zipfile)
            tgtdir = meta['tgtdir']
            logging.info(f'Unpacking {path} to {tgtdir}')
            proc = Process(target=unzip_sub, args=(args, 'oracle', path, tgtdir))
            proc.start()
            jobs.append(proc)
    for proc in jobs:
        proc.join()
    logging.info('Unpack done')

def runinstaller(component, orahome, respfile):
    env = { 'TMPDIR': tmpdir, 'TEMPDIR': tmpdir, 'CV_ASSUME_DISTID': 'OEL7.8', 'PATH': '/usr/bin:/usr/sbin'}
    try:
        if component == 'grid':
            switchuser('grid')
            cmd = os.path.join(orahome, 'gridSetup.sh')
            run([cmd, '-silent', '-skipPrereqs', '-waitforcompletion', '-responsefile', respfile], check=True, env=env)
        elif component == 'database':
            switchuser('oracle')
            cmd = os.path.join(orahome, 'runInstaller')
            run([cmd, '-silent', '-ignorePrereqFailure', '-waitforcompletion', '-responsefile', respfile], check=True, env=env)
    except CalledProcessError:
        sys.exit(5)

def install_component(component, args):
    shortversion = '.'.join(args.version.split('.')[:3])
    respfile  = os.path.join('/usr/share/ora-tools', f'{component}-{shortversion}.rsp')
    if not os.path.isfile(respfile):
        raise ValueError(f"Response file {respfile} not found")
    with open(respfile) as f:
        data = f.read()
        rb = re.search(r'^ORACLE_BASE=(\S+)', data, re.M)
        rh = re.search(r'^ORACLE_HOME=(\S+)', data, re.M)
        ri = re.search(r'^INVENTORY_LOCATION=(\S+)', data, re.M)
        orabase = rb.group(1)
        orahome = rh.group(1)
        orainv  = ri.group(1)

    logging.info(f'Component: {component}')
    logging.info(f'Response file: {respfile}')
    logging.info(f'Oracle Inventory: {orainv}')
    logging.info(f'Oracle Base: {orabase}')
    logging.info(f'Oracle Home: {orahome}')

    os.makedirs(orabase, exist_ok=True)
    os.makedirs(orahome, exist_ok=True)
    if component == 'grid':
        uid = pwd.getpwnam('grid').pw_uid
    elif component == 'database':
        uid = pwd.getpwnam('oracle').pw_uid

    os.chown(orabase, uid, grp.getgrnam('oinstall').gr_gid)
    os.chown(orahome, uid, grp.getgrnam('oinstall').gr_gid)

    if component == 'grid':
        r = run(['rpm', '-q', '--quiet', 'cvuqdisk'])
        if not r.returncode == 0:
            run(['yum', '-y', 'localinstall', '/u01/tmp/grid-staging/cv/rpm/cvuqdisk-1.0.10-1.rpm'], check=True)

        if os.path.isdir(grid_staging):
            os.rename(grid_staging, orahome)

    elif component == 'database':
        if os.path.isdir(database_staging):
            os.rename(database_staging, orahome)

    proc = Process(target=runinstaller, args=(component, orahome, respfile))
    proc.start()
    proc.join()
    if proc.exitcode:
        logging.warning(f'Installer failed, rc={proc.exitcode}')
    else:
        logging.info(f'Installer completed')

    invroot  = os.path.join(orainv, 'orainstRoot.sh')
    homeroot = os.path.join(orahome, 'root.sh')

    if not os.path.exists(invroot):
        raise ValueError(f'No {invroot}')
    if not os.path.exists(homeroot):
        raise ValueError(f'No {homeroot}')

    logging.info(f'Running {invroot}')
    run([invroot], check=True)
    logging.info(f'Running {homeroot}')
    run([homeroot], check=True)

def install(args):
    unzip(args)
    if args.grid:
        install_component('grid', args)
    elif args.database:
        install_component('database', args)
    else:
        install_component('grid', args)
        install_component('database', args)

if __name__ == '__main__':
    formatter  = lambda prog: argparse.HelpFormatter(prog, max_help_position=40)
    parser     = argparse.ArgumentParser(formatter_class=formatter)
    subparsers = parser.add_subparsers(title='commands', dest='cmd')
    parent     = argparse.ArgumentParser(add_help=False)

    parser.add_argument('-v', '--verbose', action="store_true")
    parent.add_argument('-d', '--zipdir', metavar='zipdir', help='location of installer zip files')
    parent.add_argument('version', choices=('11.2.0.4.0', '12.1.0.2.0', '12.2.0.1.0', '18.0.0.0.0', '19.0.0.0.0'), metavar='version')

    parser_unzip   = subparsers.add_parser('unzip',   parents=[parent], help='Unzip archives', description='Unzip')
    parser_install = subparsers.add_parser('install', parents=[parent], help='Install',        description='Install')
    parser_destroy = subparsers.add_parser('destroy', help='Destroy Oracle', description='Destroy')

    parser_unzip.set_defaults(func=unzip)
    parser_install.set_defaults(func=install)
    parser_destroy.set_defaults(func=destroy)

    group = parser.add_mutually_exclusive_group()
    group.add_argument('--grid', action="store_true", help='grid only')
    group.add_argument('--database', action="store_true", help='database only')

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

    args = parser.parse_args()
    try:
        if args.cmd is None:
            logging.error("Bad command")
        else:
            args.func(args)
    except ValueError as e:
        logging.error(e)
        sys.exit(10)

