diff --git a/plugins/modules/oracle_oratab.py b/plugins/modules/oracle_oratab.py new file mode 100755 index 0000000..4894137 --- /dev/null +++ b/plugins/modules/oracle_oratab.py @@ -0,0 +1,446 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +DOCUMENTATION = ''' +--- +module: oracle_oratab +short_description: Reads oratab to ansible_facts +description: + - Reads SID and ORACLE_HOME path from oratab, crs and running processes to ansible_facts +version_added: "1.0.0.0" +author: Ivan Brezina +options: + writable_only: + description: [Return only databases openned READ WRITE] + required: false + default: false + asm_only: + description: [Return only ASM instances] + required: false + default: false + running_only: + description: [Return only instances which are running] + required: false + default: false + open_only: + description: [Return only databases which are OPEN] + required: false + default: false +''' + +EXAMPLES = ''' +# Read oratab + - oracle_oratab: + writable_only: true + register: sid_list + + - debug: + var: sid_list + +# More datailed example is here: +# https://github.com/ibre5041/ansible_oracle_modules_example/blob/main/oracle_oratab.yml +# +''' + +import fcntl +import os +from pathlib import Path +import pwd +import subprocess +import re +import glob +import subprocess +import socket +from pwd import getpwuid + +from ansible.module_utils.basic import AnsibleModule + +class oracle_homes(): + + def __init__(self, module = None): + self.facts_item = {} + self.running_only = False + self.open_only = False + self.writable_only = False + self.oracle_restart = False + self.oracle_crs = False + self.oracle_standalone = False + self.oracle_install_type = None + self.crs_home = None + self.homes = {} + self.ora_inventory = None + self.orabase = None + self.crsctl = None + self.module = module # possible reference onto AnsibleModule + + # Check whether CRS/HAS is installed + try: + with open('/etc/oracle/ocr.loc') as f: + for line in f: + if line.startswith('local_only='): + (_, local_only,) = line.strip().split('=') + if local_only.upper() == 'TRUE': + self.oracle_install_type = 'RESTART' + if local_only.upper() == 'FALSE': + self.oracle_install_type = 'CRS' + except: + pass + + # Try to detect CRS_HOME + try: + with open('/etc/oracle/olr.loc') as f: + for line in f: + if line.startswith('crs_home='): + (_, crs_home,) = line.strip().split('=') + self.crs_home = crs_home + + crsctl = os.path.join(crs_home, 'bin', 'crsctl') + if os.access(crsctl, os.X_OK): + self.crsctl = crsctl + except: + pass + + # Try to parse inventory.xml file to get list of ORACLE_HOMEs + try: + with open('/etc/oraInst.loc') as f: + for line in f: + if line.startswith('inventory_loc='): + (_, oraInventory,) = line.strip().split('=') + self.ora_inventory = oraInventory + + from xml.dom import minidom + inv_tree = minidom.parse(os.path.join(self.ora_inventory, 'ContentsXML', 'inventory.xml')) + homes = inv_tree.getElementsByTagName('HOME') + for home in homes: + # TODO: skip for deleted ORACLE_HOME + self.add_home(home.attributes['LOC'].value) + except: + pass + + + def module_warn(msg): + if self.module: + module.warn(msg) + + + def module_fail_json(msg='', changed=False): + if self.module: + module.fail_json(msg=msg, changed=changed) + else: + os.exit(1) + + + def parse_oratab(self): + # Reads SID and ORACLE_HOME from oratab + with open('/etc/oratab', 'r') as oratab: + for line in oratab: + line = line.strip() + if not line: + continue + if line.startswith('#'): + continue + + ORACLE_SID, ORACLE_HOME, _ = line.split(':') + self.add_sid(ORACLE_SID=ORACLE_SID, ORACLE_HOME=ORACLE_HOME) + + def list_processes(self): + """ + # Emulate trick form tanelpoder + # https://tanelpoder.com/2011/02/28/finding-oracle-homes-with/ + # + # printf "%6s %-20s %-80s\n" "PID" "NAME" "ORACLE_HOME" + # pgrep -lf _pmon_ | + # while read pid pname y ; do + # printf "%6s %-20s %-80s\n" $pid $pname `ls -l /proc/$pid/exe | awk -F'>' '{ print $2 }' | sed 's/bin\/oracle$//' | sort | uniq` + # done + # + # It s basically looking up all PMON process IDs and then using /proc/PID/exe link to find out where is the oracle binary of a running process located + # + """ + for cmd_line_file in glob.glob('/proc/[0-9]*/cmdline'): + try: + with open(cmd_line_file) as x: + cmd_line = x.read().rstrip("\x00") + if not cmd_line.startswith('ora_pmon_') and not cmd_line.startswith('asm_pmon_'): + continue + _, _, ORACLE_SID = cmd_line.split('_', 2) + + piddir = os.path.dirname(cmd_line_file) + exefile = os.path.join(piddir, 'exe') + + try: + if self.facts_item[ORACLE_SID]['ORACLE_HOME']: + self.add_sid(ORACLE_SID, running=True) + continue + except: + pass + + try: + if not os.path.islink(exefile): + continue + oraclefile = os.readlink(exefile) + ORACLE_HOME = os.path.dirname(oraclefile) + ORACLE_HOME = os.path.dirname(ORACLE_HOME) + except: + # In case oracle binary is suid, ptrace does not work, + # stat/readlink /proc//exec does not work + # fails with: Permission denied + # Then try to query the same information from CRS (if possible) + ORACLE_HOME = None + + if self.crsctl: + if cmd_line.startswith('asm'): + dfiltertype = 'ora.asm.type' + ORACLE_HOME = self.crs_home + else: + dfiltertype = 'ora.database.type' + dfilter = '((TYPE = {}) and (GEN_USR_ORA_INST_NAME = {}))'.format(dfiltertype, ORACLE_SID) + proc = subprocess.Popen([self.crsctl, 'stat', 'res', '-p', '-w', dfilter], + stdout=subprocess.PIPE) + for line in iter(proc.stdout.readline, ''): + if line.decode('utf-8').startswith('ORACLE_HOME='): + (_, ORACLE_HOME,) = line.decode('utf-8').strip().split('=') + proc.poll() + if proc.returncode is not None: + break + pass + if not ORACLE_HOME: + own_uid = os.geteuid() + ora_uid = os.stat(cmd_line_file).st_uid + if self.crsctl: + crs_uid = os.stat(self.crsctl).st_uid + else: + crs_uid = None + if own_uid != 0 and own_uid != ora_uid: + module_fail_json(msg='I(uid={}) am not oracle process owner(uid={}) and I am not root'.format(own_uid, ora_uid), changed=False) + + self.add_sid(ORACLE_SID=ORACLE_SID, ORACLE_HOME=ORACLE_HOME, running=True) + + # except FileNotFoundError: # Python3 + except EnvironmentError as e: + # print("Missing file ignored: {} ({})".format(cmd_line_file, e)) + pass + + def list_crs_instances(self): + hostname = socket.gethostname().split('.')[0] + if self.crsctl: + for dfiltertype in ['ora.database.type']: # ora.asm.type does not report ORACLE_HOME + # for dfiltertype in ['ora.asm.type', 'ora.database.type']: + dfilter = '(TYPE = {})'.format(dfiltertype) + proc = subprocess.Popen([self.crsctl, 'stat', 'res', '-p', '-w', dfilter], stdout=subprocess.PIPE) + (ORACLE_HOME, ORACLE_SID) = (None, None) + for line in iter(proc.stdout.readline, ''): + line = line.decode('utf-8') + if not line.strip(): + (ORACLE_HOME, ORACLE_SID) = (None, None) + if 'SERVERNAME({})'.format(hostname) in line and line.startswith('GEN_USR_ORA_INST_NAME'): + (_, ORACLE_SID,) = line.strip().split('=') + if line.startswith('ORACLE_HOME='): + (_, ORACLE_HOME,) = line.strip().split('=') + if ORACLE_SID and ORACLE_HOME: + self.add_sid(ORACLE_SID=ORACLE_SID, ORACLE_HOME=ORACLE_HOME) + (ORACLE_HOME, ORACLE_SID) = (None, None) + proc.poll() + if proc.returncode is not None: + break + + def base_from_home(self, ORACLE_HOME): + """ execute $ORACLE_HOME/bin/orabase to get ORACLE_BASE """ + orabase = os.path.join(ORACLE_HOME, 'bin', 'orabase') + ORACLE_BASE = None + if os.access(orabase, os.X_OK): + proc = subprocess.Popen([orabase], stdout=subprocess.PIPE, env={'ORACLE_HOME': ORACLE_HOME}) + for line in iter(proc.stdout.readline, ''): + if line.strip(): + ORACLE_BASE = line.strip().decode() + else: + break + return ORACLE_BASE + + def add_home(self, ORACLE_HOME): + if ORACLE_HOME and ORACLE_HOME not in self.homes: + ORACLE_BASE = self.base_from_home(ORACLE_HOME) + self.homes[ORACLE_HOME] = {'ORACLE_HOME': ORACLE_HOME, 'ORACLE_BASE': ORACLE_BASE} + + def add_sid(self, ORACLE_SID, ORACLE_HOME=None, running=False): + if ORACLE_SID in self.facts_item: + sid = self.facts_item[ORACLE_SID] + if ORACLE_HOME: + sid['ORACLE_HOME'] = ORACLE_HOME + if running: + sid['running'] = running + if running is not None and sid['running'] is not True: + sid['running'] = running + else: + self.add_home(ORACLE_HOME) + if ORACLE_HOME and ORACLE_HOME in self.homes: + ORACLE_BASE = self.homes[ORACLE_HOME]['ORACLE_BASE'] + elif ORACLE_HOME: + ORACLE_BASE = self.base_from_home(ORACLE_HOME) + else: + ORACLE_BASE = None + self.facts_item[ORACLE_SID] = {'ORACLE_SID': ORACLE_SID + , 'ORACLE_HOME': ORACLE_HOME + , 'ORACLE_BASE': ORACLE_BASE + , 'running': running} + + def query_db_status(self, oracle_owner, oracle_home, oracle_sid): + sqlplus_path = Path(oracle_home, 'bin', 'sqlplus') + args = [str(sqlplus_path), '-S', '/', 'as', 'sysdba'] + pw_record = pwd.getpwnam(oracle_owner) + user_name = pw_record.pw_name + user_home_dir = pw_record.pw_dir + user_uid = pw_record.pw_uid + user_gid = pw_record.pw_gid + user_gids = os.getgrouplist(user_name, user_gid) + env = os.environ.copy() + env[ 'HOME' ] = user_home_dir + env[ 'LOGNAME' ] = user_name + env[ 'PWD' ] = '/' + env[ 'USER' ] = user_name + env[ 'ORACLE_HOME'] = oracle_home + env[ 'ORACLE_SID'] = oracle_sid + + if os.getuid() == 0: + process = subprocess.Popen(args, + preexec_fn=self.demote(user_uid, user_gid, user_gids), + cwd='/', env=env, stdout=subprocess.PIPE, stdin=subprocess.PIPE) + elif os.getuid() == user_uid: + process = subprocess.Popen(args, + cwd='/', env=env, stdout=subprocess.PIPE, stdin=subprocess.PIPE) + else: + self.module_fail_json(msg='Can not execute sqlplus(uid={})'.format(user_uid), changed=False) + return [] + + sql = """ + select status from v$instance; + select open_mode from v$database; + select count(*) as ora_dg_on from v$archive_dest where status = 'VALID' AND target = 'STANDBY'; + select database_role from v$database; + EXIT + """ + + header = None + delim = False + value = None + r = {} + out = process.communicate(input=sql.encode(), timeout=5) + for l in out[0].decode('utf-8').splitlines(): + #module_warn("{}:{}".format(oracle_sid,l.rstrip())) + if l.strip() in ('STATUS', 'OPEN_MODE', 'ORA_DG_ON', 'DATABASE_ROLE'): + header = l.strip() + delim = False + value = None + elif l.rstrip().startswith('-------'): + delim = True + value = None + elif l.rstrip() and header and delim and value is None: + value = l.strip() + r[header] = value + header = None + delim = False + value = False + #module_warn(str(r)) + #module_warn("Exit code {}".format(process.returncode)) + if oracle_sid.startswith('+ASM') and r['STATUS'] == 'STARTED': + return ['ASM', 'STARTED'] + elif oracle_sid.startswith('+ASM'): + return ['ASM'] + + if r['STATUS'] == 'STARTED': + return ['STARTED'] + elif r['STATUS'] == 'MOUNTED' and int(r['ORA_DG_ON']): + return ['MOUNTED', 'STANDBY'] + elif r['STATUS'] == 'MOUNTED': + return ['MOUNTED'] + elif r['STATUS'] == 'OPEN' and int(r['ORA_DG_ON']): + return ['OPEN', 'PRIMARY', r['OPEN_MODE']] + elif r['STATUS'] == 'OPEN': + return ['OPEN', r['OPEN_MODE']] + + @staticmethod + def demote(user_uid, user_gid, supplementary_groups): + def result(): + os.setgroups(supplementary_groups) + os.setgid(user_gid) + os.setegid(user_gid) + os.setuid(user_uid) + os.seteuid(user_uid) + return result + + +# Ansible code +def main(): + oracle_list = [] + module = AnsibleModule( + argument_spec = dict( + asm_only = dict(default=False, type="bool"), + running_only = dict(default=False, type="bool"), + open_only = dict(default=False, type="bool"), + writable_only = dict(default=False, type="bool"), + facts_item = dict() + ), + supports_check_mode=True + ) + asm_only = module.params['asm_only'] + running_only = module.params['running_only'] + open_only = module.params['open_only'] + writable_only = module.params['writable_only'] + + h = oracle_homes(module) + h.list_crs_instances() + h.list_processes() + h.parse_oratab() + + for sid in list(h.facts_item): + try: + sqlplus_path = Path(h.facts_item[sid]['ORACLE_HOME'], 'bin', 'oracle') + oracle_owner = getpwuid(os.stat(sqlplus_path).st_uid).pw_name + h.facts_item[sid]['owner'] = oracle_owner + except: + pass + + if h.facts_item[sid]["running"]: + status = h.query_db_status(oracle_owner = h.facts_item[sid]['owner'] + , oracle_home = h.facts_item[sid]['ORACLE_HOME'] + , oracle_sid = h.facts_item[sid]['ORACLE_SID']) + h.facts_item[sid]['status'] = status + else: + h.facts_item[sid]['status'] = ['DOWN'] + + if running_only: + for sid in list(h.facts_item): + if not h.facts_item[sid]["running"]: + del h.facts_item[sid] + module.warn('ORACLE_SID: {} is down'.format(sid)) + + if asm_only: + for sid in list(h.facts_item): + if 'ASM' not in h.facts_item[sid]["status"]: + del h.facts_item[sid] + module.warn('ORACLE_SID: {} is not ASM'.format(sid)) + + if running_only: + for sid in list(h.facts_item): + if not h.facts_item[sid]["running"]: + del h.facts_item[sid] + module.warn('ORACLE_SID: {} is down'.format(sid)) + + if open_only: + for sid in list(h.facts_item): + if 'OPEN' not in h.facts_item[sid]["status"]: + del h.facts_item[sid] + module.warn('ORACLE_SID: {} is not open'.format(sid)) + + if writable_only: + for sid in list(h.facts_item): + if 'READ WRITE' not in h.facts_item[sid]["status"]: + del h.facts_item[sid] + module.warn('ORACLE_SID: {} is not open'.format(sid)) + + #module.warn('uid {}'.format(os.getuid())) + module.exit_json(oracle_list=h.facts_item, changed=False) + + +if __name__ == '__main__': + main()