From ab87f3ce423dbbadbe2d87d97b78f4d484d4ac4b Mon Sep 17 00:00:00 2001 From: duhlig <47413026+duhlig@users.noreply.github.com> Date: Fri, 28 Jun 2019 10:49:03 +0200 Subject: [PATCH 01/13] added stop_processes for remove_patch --- oracle_opatch | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/oracle_opatch b/oracle_opatch index f6210c5..d1b049d 100644 --- a/oracle_opatch +++ b/oracle_opatch @@ -337,7 +337,7 @@ def stop_process(module, oracle_home): if msg: module.fail_json(msg=msg, changed=False) -def remove_patch (module, msg, oracle_home, patch_base, patch_id, opatchauto, ocm_response_file,output): +def remove_patch (module, msg, oracle_home, patch_base, patch_id, opatchauto, ocm_response_file, stop_processes, output): ''' Removes the patch ''' @@ -350,6 +350,9 @@ def remove_patch (module, msg, oracle_home, patch_base, patch_id, opatchauto, oc command = '%s/OPatch/%s %s -oh %s ' % (oracle_home,opatch_cmd, patch_base, oracle_home) else: + if stop_processes: + stop_process(module, oracle_home) + opatch_cmd = 'opatch rollback' command = '%s/OPatch/%s -id %s -silent' % (oracle_home,opatch_cmd, patch_id) @@ -543,7 +546,7 @@ def main(): elif state == 'absent': if check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto): - if remove_patch(module, msg, oracle_home, patch_base, patch_id, opatchauto,ocm_response_file, output): + if remove_patch(module, msg, oracle_home, patch_base, patch_id, opatchauto,ocm_response_file, stop_processes, output): if patch_version is not None: msg = 'Patch %s (%s) successfully removed from %s' % (patch_id,patch_version, oracle_home) else: From 60787999288d904ca83393c118db854c1e7ff063 Mon Sep 17 00:00:00 2001 From: Dietmar Uhlig Date: Mon, 25 Nov 2019 17:34:07 +0100 Subject: [PATCH 02/13] Some interim patches are PSU/RU dependent. The patch ID is the same but content, dependencies, and UPI (unique patch ID) differ. These patches have to be rolled back in advance of applying the PSU/RU and must afterwards be installed with the correct version (=UPI). They should however not be rolled back if they are already there with the desired UPI. This helps to write more idempotent playbooks. So there is a new parameter exclude_upi that is evaluated only if state=absent. To keep changes in working code at a minimum the check for UPI could be acquired only by another "opatch lspatches" that costs some seconds. --- oracle_opatch | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/oracle_opatch b/oracle_opatch index d1b049d..dda9f99 100644 --- a/oracle_opatch +++ b/oracle_opatch @@ -37,6 +37,15 @@ options: required: False default: None aliases: ['version_added'] + exclude_upi: + description: + - The unique patch identifier that should not be rolled back + e.g 22990084 + - This option will be honored only when state=absent. + - It helps to write idempotent playbooks when changing interim patches that are dependent on a specific PSU/RU. + required: False + default: None + aliases: ['exclude_unique_patch_id'] opatch_minversion: description: - The minimum version of opatch needed @@ -155,7 +164,7 @@ def get_file_owner(module, msg, oracle_home): msg = 'Could not determine owner of %s ' % (checkfile) module.fail_json(msg=msg) -def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto): +def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): ''' Gets all patches already applied and compares to the intended patch @@ -169,8 +178,8 @@ def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatc (rc, stdout, stderr) = module.run_command(command) #module.exit_json(msg=stdout, changed=False) if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) else: if opatchauto: chk = '%s' % (patch_version) @@ -180,7 +189,17 @@ def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatc chk = '%s' % (patch_id) if chk in stdout: - return True + if exclude_upi is None: + return True + else: + command += ' -id %s' % patch_id + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + chk = 'unique_patch_id:%s' % exclude_upi + return (chk not in stdout) else: return False @@ -446,6 +465,7 @@ def main(): patch_base = dict(default=None, aliases = ['path','source','patch_source','phBaseDir']), patch_id = dict(default=None, aliases = ['id']), patch_version = dict(required=None, aliases = ['version']), + exclude_upi = dict(required=None, aliases = ['exclude_unique_patch_id']), opatch_minversion = dict(default=None, aliases = ['opmv']), opatchauto = dict(default='False', type='bool',aliases = ['autopatch']), rolling = dict(default='True', type='bool',aliases = ['rolling']), @@ -457,7 +477,7 @@ def main(): output = dict(default="short", choices = ["short","verbose"]), state = dict(default="present", choices = ["present", "absent", "opatchversion"]), hostname = dict(required=False, default = 'localhost', aliases = ['host']), - port = dict(required=False, default = 1521), + port = dict(required=False, type='int', default = 1521), @@ -469,6 +489,7 @@ def main(): patch_base = module.params["patch_base"] patch_id = module.params["patch_id"] patch_version = module.params["patch_version"] + exclude_upi = module.params["exclude_upi"] opatch_minversion = module.params["opatch_minversion"] opatchauto = module.params["opatchauto"] rolling = module.params["rolling"] @@ -529,7 +550,7 @@ def main(): module.exit_json(msg=opatch_version,changed=False) if state == 'present': - if not check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto): + if not check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, None): if apply_patch(module, msg, oracle_home, patch_base, patch_id,patch_version, opatchauto,ocm_response_file,offline,stop_processes,rolling,output): if patch_version is not None: msg = 'Patch %s (%s) successfully applied to %s' % (patch_id,patch_version, oracle_home) @@ -545,7 +566,7 @@ def main(): module.exit_json(msg=msg, changed=False) elif state == 'absent': - if check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto): + if check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): if remove_patch(module, msg, oracle_home, patch_base, patch_id, opatchauto,ocm_response_file, stop_processes, output): if patch_version is not None: msg = 'Patch %s (%s) successfully removed from %s' % (patch_id,patch_version, oracle_home) @@ -556,8 +577,9 @@ def main(): module.fail_json(msg=msg, changed=False) else: if patch_version is not None: - msg = 'Patch %s (%s) is not applied to %s' % (patch_id,patch_version, oracle_home) - + msg = 'Patch %s (%s) is not applied to %s' % (patch_id, patch_version, oracle_home) + elif exclude_upi is not None: + msg = 'Patch %s (UPI %s) has already been applied and will not be removed from %s' % (patch_id, exclude_upi, oracle_home) else: msg = 'Patch %s is not applied to %s' % (patch_id, oracle_home) From 42e65ec54a9813424104ff852238c63650841973 Mon Sep 17 00:00:00 2001 From: Dietmar Uhlig Date: Fri, 6 Dec 2019 16:31:10 +0100 Subject: [PATCH 03/13] oracle_opatch: Find running listener more robust Oracle_opatch searches the running listener with: ```ps -elf | grep "[0-9] $ORACLE_HOME/bin/tnslsnr"``` ...then splits the line by spaces and takes the last but one field. This does not work for a "ps -ef" result like this: ```0 S oracle 16921 1 0 80 0 - 43851 ep_pol 21:58 ? 00:00:00 /u01/app/oracle/product/12.1.0.2/ee/bin/tnslsnr LISTENER /dev/null -inherit``` In this case "linelist[-2]" deliveres "/dev/null" instead of the LISTENER name. My fix is to search the ps line for the listener executable and use the next field. --- oracle_opatch | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/oracle_opatch b/oracle_opatch index dda9f99..2c8f915 100644 --- a/oracle_opatch +++ b/oracle_opatch @@ -315,7 +315,8 @@ def stop_process(module, oracle_home): elif len(line.split(':')) >= 2 and line.split(':')[1] == oracle_home: # Find listener for ORACLE_HOME - p = subprocess.Popen('ps -elf| grep "[0-9] %s/bin/tnslsnr"' % (line.split(':')[1]) , shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + tnslsnr = "%s/bin/tnslsnr" % (line.split(':')[1]) + p = subprocess.Popen('ps -elf| grep "[0-9] %s"' % tnslsnr, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, error = p.communicate() p_status = p.wait() @@ -324,7 +325,7 @@ def stop_process(module, oracle_home): linelist = lline.split(' ') if len(lline) > 3: - listener_name = linelist[-2] + listener_name = linelist[linelist.index(tnslsnr) + 1] lsnrctl_bin = '%s/bin/lsnrctl' % (oracle_home) try: p = subprocess.check_call([lsnrctl_bin, 'stop', '%s' % listener_name]) From 031b16a920be130795033dad25072acf5af1c2c7 Mon Sep 17 00:00:00 2001 From: Dietmar Uhlig Date: Thu, 16 Jan 2020 17:01:04 +0100 Subject: [PATCH 04/13] First version of oracle_sqldba. Signed-off-by: Dietmar Uhlig --- oracle_sqldba | 375 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 oracle_sqldba diff --git a/oracle_sqldba b/oracle_sqldba new file mode 100644 index 0000000..6be5c96 --- /dev/null +++ b/oracle_sqldba @@ -0,0 +1,375 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +DOCUMENTATION = ''' +--- +module: oracle_sqldba +short_description: Execute sql (scripts) using sqlplus (BEQ) or catcon.pl +description: + - Needed for post-installation tasks not covered by other modules + - Uses sqlplus (BEQ connect, e.g. / as sysdba) or $OH//perl catcon.pl +options: + sql: + description: + - Single SQL statement + - Will be executed by sqlplus + - Used for DDL and DML + required: false + default: None + sqlscript: + description: + - Script name, optionally followed by parameters + - Will be executed by sqlplus + required: false + default: None + catcon_pl: + description: + - Script name, optionally followed by parameters + - Will be executed by $OH//perl catcon.pl + required: false + default: None + sqlselect: + description: + - Single SQL statement + - Will be executed by sqlplus using dbms_xmlgen.getxml + - Used for select only, returns dict in .state + - To access the column "value" of the first row write: "<>.state.ROW[0].VALUE" (use uppercase) + required: false + default: None + creates_sql: + description: + - This is the check query to ensure idempotence. + - Must be a single SQL select that results to no rows or a plain 0 if the catcon_pl/sqlscript/sql has to be executed. Any other result let the catcon_pl/sqlscript/sql execute. + - The catcon_pl/sqlscript/sql will be executed unconditionally if creates_sql is omitted. + - Creates_sql must be omitted when sqlselect is used. + - Creates_sql is executed with sqlplus / as sysdba in the root container. Write the sql query according to this fact. + required: false + default: None + username: + description: + - Database username, defaults to "/ as sysdba" + required: false + default: None + password: + description: + - Password of database user + required: false + default: None + scope: + description: + - Shall the SQL be applied to CDB, PDBs, or both? + values: + - default: if catcon_pl is filled then all_pdbs else cdb + - db: alias for cdb, allows for better readability for non-cdb + - cdb: apply to root container or whole db + - pdbs: apply to specified PDB's only (requires pdb_list) + - all_pdbs: apply to all PDB's except PDB$SEED + required: false + default: cdb + pdb_list: + description: + - Optional list of PDB names + - Space separated, as catcon.pl wants + - Gets used only if scope is "pdbs" + required: false + default: None + oracle_home: + description: + - content of $ORACLE_HOME + oracle_db_name: + description: + - SID or DB_NAME, needed for BEQ connect +author: Dietmar Uhlig, Robotron (www.robotron.de) +''' + +EXAMPLES = ''' +# Example 1, mixed post installation tasks +# from inventory: + +oracle_databases: + - oracle_db_name: eek17ec + home: 12.2.0.1-ee + state: present + init_parameters: "{{ init_parameters['12.2.0.1-EMS'] }}" + profiles: "{{ db_profiles['12.2.0.1-EMS'] }}" + postinstall: "{{ db_postinstall['12.2.0.1-EMS'] }}" + +oracle_pdbs: + - cdb: eek17ec + pdb_name: eckpdb + - cdb: eek17ec + pdb_name: sckpdb + +db_postinstall: + 12.2.0.1-EMS: + - catcon_pl: "$ORACLE_HOME/ctx/admin/catctx.sql context SYSAUX TEMP NOLOCK" + creates_sql: "select 1 from dba_registry where comp_id = 'CONTEXT'" + - sqlscript: "?/rdbms/admin/initsqlj.sql" + scope: pdbs + creates_sql: "select count(*) from dba_tab_privs where table_name = 'SQLJUTL' and grantee = 'PUBLIC'" + - sqlscript: "?/rdbms/admin/utlrp.sql" + - sql: "alter pluggable database {{ pdb.pdb_name | default(omit) }} save state" + scope: pdbs + +# see role oradb-postinstall, loops over {{ oracle_databases }} = loop_var oradb + +- name: Conditionally execute post installation tasks + oracle_sqldba: + sql: "{{ pitask.sql | default(omit) }}" + sqlscript: "{{ pitask.sqlscript | default(omit) }}" + catcon_pl: "{{ pitask.catcon_pl | default(omit) }}" + creates_sql: "{{ pitask.creates_sql | default(omit) }}" + username: "{{ pitask.username | default(omit) }}" + password: "{%if pitask.username is defined%}{{ dbpasswords[oradb.oracle_db_name][pitask.username] }}{%endif%}" + scope: "{{ pitask.scope | default(omit) }}" + pdb_list: "{{ oracle_pdbs | default([]) | json_query('[?cdb==`' + oradb.oracle_db_name + '`].pdb_name') | join(' ') }}" + oracle_home: "{{ db_homes_config[oradb.home].oracle_home }}" + oracle_db_name: "{{ oradb.oracle_db_name }}" + loop: "{{ oradb.postinstall }}" + loop_control: + loop_var: pitask + +# Example 2, read sql result + +- name: Read job_queue_processes + oracle_sqldba: + sqlselect: "select value from gv$parameter where name = 'job_queue_processes'" + oracle_home: "{{ oracle_db_home }}" + oracle_db_name: "{{ oracle_db_name }}" + register: jqpresult + +- name: Store job_queue_processes + set_fact: + job_queue_processes: "{{ jqpresult.state.ROW[0].VALUE }}" + # Use all uppercase for "ROW" and for column names! + +''' + +import errno +import os +import re +import shlex +import shutil +from subprocess import Popen, PIPE +import tempfile +from threading import Timer +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native, to_text +import xml.etree.ElementTree as ET +from copy import copy + +changed = False +result = "" +err_msg = None +oracle_home = "" + +# Maximum runtime for sqlplus and catcon.pl in seconds. 0 means no timeout. +timeout = 900 + +# dictify is based on https://stackoverflow.com/questions/2148119/how-to-convert-an-xml-string-to-a-dictionary/10077069#10077069 +def dictify(r,root=True): + if root: + #return {r.tag : dictify(r, False)} # no, but... + return dictify(r, False) # skip root node "ROWSET" + d=copy(r.attrib) + if (r.text).strip(): + d["_text"]=r.text + for x in r.findall("./*"): + if x.tag not in d: + d[x.tag]=[] + if (x.text).strip(): # assume scalar + d[x.tag] = x.text + else: + d[x.tag].append(dictify(x,False)) + return d + +def sqlplus(): + global oracle_home + + sql_bin = os.path.join(oracle_home, "bin", "sqlplus") + return [sql_bin, "-l", "-s", "/nolog"] + +def conn(username, password): + if username == None: + return "conn / as sysdba\n" + else: + return "conn " + "/".join([username, password]) + "\n" + +def sql_input(sql, username, password, scope, pdb_list): + sql_scr = "set heading off echo off feedback off termout on\n" + sql_scr += "set long 1000000 pagesize 0 linesize 1000 trimspool on\n" + sql_scr += conn(username, password) + + if scope == 'pdbs': + sql_scr += "whenever sqlerror exit\n" + for pdb in pdb_list.split(): + sql_scr += "alter session set container = " + pdb + ";\n" + sql_scr += sql + "\n" + else: + sql_scr += sql + "\n" + sql_scr += "exit;\n" + return sql_scr + +def run_sql(sql, username, password, scope, pdb_list): + global changed, err_msg + + t = None + try: + sql_cmd = sql_input(sql, username, password, scope, pdb_list) + p = Popen(sqlplus(), stdin = PIPE, stdout = PIPE, stderr = PIPE) + if timeout > 0: + t = Timer(timeout, p.kill) + t.start() + [sout, serr] = p.communicate(input = sql_cmd) + except Exception as e: + err_msg = 'Could not call sqlplus. %s. called: %s.' % (to_native(e), sql_cmd) + return "ERR" + finally: + if timeout > 0 and t is not None: + t.cancel() + if p.returncode != 0: + err_msg = "called: " + sql_cmd + "\nresult: " + sout + ". stderr = " + serr + "." + return "ERR" + sqlplus_err = re.compile("^(ORA|TNS|SP2)-", re.MULTILINE) + if sqlplus_err.search(sout): + err_msg = "sqlplus# " + sql_cmd + ": " + sout + "." + return "ERR" + + changed = True + return sout.strip() + + +def check_creates_sql(sql): + global result + + if not sql.endswith(";"): + sql += ";" + result = run_sql(sql, None, None, None, None) + return False if not result or result == "0" else True + + +def run_catcon_pl(catcon_pl, scope, pdb_list): + # after pre-processing in main() the parameter scope is not necessary any more + global oracle_home, changed, result, err_msg + + catcon_pl = re.sub("^(\$ORACLE_HOME|\?)", oracle_home, catcon_pl) + logdir = tempfile.mkdtemp() + catcon_cmd = [ os.path.join(oracle_home, "perl", "bin", "perl"), + os.path.join(oracle_home, "rdbms", "admin", "catcon.pl"), + "-l", logdir, "-b", "catcon" ] + if pdb_list is not None: + catcon_cmd += [ "-c", pdb_list ] + cc_script = shlex.split(catcon_pl) + if len(cc_script) > 1: + for i in range(1, len(cc_script)): + cc_script[i] = "1" + cc_script[i] + catcon_cmd += [ "-a", "1" ] + catcon_cmd += [ "--" ] + cc_script + try: + p = Popen(catcon_cmd, stdout = PIPE, stderr = PIPE) + if timeout > 0: + t = Timer(timeout, p.kill) + t.start() + [sout, serr] = p.communicate() + except Exception as e: + err_msg = 'Could not call perl. %s. called: %s.' % (to_native(e), " ".join(catcon_cmd)) + return + finally: + if timeout > 0: + t.cancel() + try: + shutil.rmtree(logdir) + except OSError as exc: + if exc.errno != errno.ENOENT: + raise + if p.returncode != 0: + err_msg = "called: " + " ".join(catcon_cmd) + "\nresult: " + sout + ".\nstderr = " + serr + "." + return + result = sout + changed = True + + + +def main(): + global oracle_home + + module = AnsibleModule( + argument_spec = dict( + sql = dict(required = False), + sqlscript = dict(required = False), + catcon_pl = dict(required = False), + sqlselect = dict(required = False), + creates_sql = dict(required = False), + username = dict(required = False), + password = dict(required = False, no_log = True), + scope = dict(required = False, choices = ["default", "db", "cdb", "pdbs", "all_pdbs"], default = 'default'), + pdb_list = dict(required = False), + oracle_home = dict(required = True), + oracle_db_name = dict(required = True) + ), + mutually_exclusive=[['sql', 'sqlscript', 'catcon_pl', 'sqlselect'], ['sqlselect', 'creates_sql']] + ) + + sql = module.params["sql"] + sqlscript = module.params["sqlscript"] + catcon_pl = module.params["catcon_pl"] + sqlselect = module.params["sqlselect"] + creates_sql = module.params["creates_sql"] + username = module.params["username"] + password = module.params["password"] + scope = module.params["scope"] + pdb_list = module.params["pdb_list"] + oracle_home = module.params["oracle_home"] + oracle_db_name = module.params["oracle_db_name"] + + os.environ["ORACLE_HOME"] = oracle_home + os.environ["ORACLE_SID"] = oracle_db_name + os.environ["PATH"] += os.pathsep + os.path.join(oracle_home, "bin") + + if scope == 'db': + scope = 'cdb' + if scope == 'default': + scope = "all_pdbs" if catcon_pl is not None else "cdb" + if scope == 'pdbs' and pdb_list.strip() == "": + module.exit_json(msg = "scope = pdbs, but pdb_list is empty", changed = False) + if scope == 'cdb' and catcon_pl is not None: + scope = pdbs + pdb_list = 'CDB$ROOT' + if scope == 'all_pdbs' and catcon_pl is None: + module.fail_json(msg = "scope = all_pdbs not implemented for sql, sqlscript, and sqlselect", changed = False) + + if creates_sql is not None: + already_done = check_creates_sql(creates_sql) + if err_msg: + module.fail_json(msg = err_msg, changed = False) + else: + if already_done: + module.exit_json(msg = result, changed = False) + + if sqlselect is not None: + if sqlselect.endswith(";"): + sqlselect.rstrip(";") + sqlselect = "select dbms_xmlgen.getxml('" + sqlselect.replace("'", "''") + "') from dual;" + run_sql(sqlselect, username, password, scope, pdb_list) + elif sql is not None: + if not sql.endswith(";"): + sql += ";" + run_sql(sql, username, password, scope, pdb_list) + elif sqlscript is not None: + if not sqlscript.startswith("@"): + sqlscript = "@" + sqlscript + run_sql(sqlscript, username, password, scope, pdb_list) + elif catcon_pl is not None: + run_catcon_pl(catcon_pl, scope, pdb_list) + + if not err_msg: + if sqlselect is not None: + module.exit_json(msg = result, changed = False, state = dictify(ET.fromstring(result))) + else: + module.exit_json(msg = result, changed = changed) + else: + module.fail_json(msg = err_msg, changed = False) + + +if __name__ == '__main__': + main() From b99ad50e73bde1e68a77dd06bbd1092be0fad00d Mon Sep 17 00:00:00 2001 From: Dietmar Uhlig Date: Fri, 17 Jan 2020 13:26:55 +0100 Subject: [PATCH 05/13] Fix broken sqlselect in oracle_sqldba Signed-off-by: Dietmar Uhlig --- oracle_sqldba | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/oracle_sqldba b/oracle_sqldba index 6be5c96..6e57bb9 100644 --- a/oracle_sqldba +++ b/oracle_sqldba @@ -222,7 +222,7 @@ def run_sql(sql, username, password, scope, pdb_list): t.start() [sout, serr] = p.communicate(input = sql_cmd) except Exception as e: - err_msg = 'Could not call sqlplus. %s. called: %s.' % (to_native(e), sql_cmd) + err_msg = 'Could not call sqlplus. %s. called: %s.' % (to_native(e), " ".join(sqlplus())) return "ERR" finally: if timeout > 0 and t is not None: @@ -350,21 +350,22 @@ def main(): if sqlselect.endswith(";"): sqlselect.rstrip(";") sqlselect = "select dbms_xmlgen.getxml('" + sqlselect.replace("'", "''") + "') from dual;" - run_sql(sqlselect, username, password, scope, pdb_list) + result = run_sql(sqlselect, username, password, scope, pdb_list) elif sql is not None: if not sql.endswith(";"): sql += ";" - run_sql(sql, username, password, scope, pdb_list) + result = run_sql(sql, username, password, scope, pdb_list) elif sqlscript is not None: if not sqlscript.startswith("@"): sqlscript = "@" + sqlscript - run_sql(sqlscript, username, password, scope, pdb_list) + result = run_sql(sqlscript, username, password, scope, pdb_list) elif catcon_pl is not None: run_catcon_pl(catcon_pl, scope, pdb_list) if not err_msg: if sqlselect is not None: - module.exit_json(msg = result, changed = False, state = dictify(ET.fromstring(result))) + res_dict = dictify(ET.fromstring(result)) if result else {"ROW": []} + module.exit_json(msg = result, changed = False, state = res_dict) else: module.exit_json(msg = result, changed = changed) else: From 49dea152209b44fa34413b70bd1b6aad13515f81 Mon Sep 17 00:00:00 2001 From: Dietmar Uhlig Date: Fri, 17 Jan 2020 16:55:18 +0100 Subject: [PATCH 06/13] Fix broken run_catcon_pl in oracle_sqldba. (I should have tested more thoroughly after the last refactoring.) --- oracle_sqldba | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/oracle_sqldba b/oracle_sqldba index 6e57bb9..0ed0983 100644 --- a/oracle_sqldba +++ b/oracle_sqldba @@ -151,7 +151,6 @@ import re import shlex import shutil from subprocess import Popen, PIPE -import tempfile from threading import Timer from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native, to_text @@ -248,7 +247,7 @@ def check_creates_sql(sql): return False if not result or result == "0" else True -def run_catcon_pl(catcon_pl, scope, pdb_list): +def run_catcon_pl(catcon_pl, pdb_list): # after pre-processing in main() the parameter scope is not necessary any more global oracle_home, changed, result, err_msg @@ -291,7 +290,7 @@ def run_catcon_pl(catcon_pl, scope, pdb_list): def main(): - global oracle_home + global oracle_home, changed, result, err_msg module = AnsibleModule( argument_spec = dict( @@ -360,7 +359,7 @@ def main(): sqlscript = "@" + sqlscript result = run_sql(sqlscript, username, password, scope, pdb_list) elif catcon_pl is not None: - run_catcon_pl(catcon_pl, scope, pdb_list) + run_catcon_pl(catcon_pl, pdb_list) if not err_msg: if sqlselect is not None: From fff6a7a0890b34e3eba4078dd2ff1a52b0acd10f Mon Sep 17 00:00:00 2001 From: duhlig <47413026+duhlig@users.noreply.github.com> Date: Fri, 28 Jun 2019 10:49:03 +0200 Subject: [PATCH 07/13] added stop_processes for remove_patch --- oracle_opatch | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/oracle_opatch b/oracle_opatch index 8f72a6c..fd359ea 100644 --- a/oracle_opatch +++ b/oracle_opatch @@ -341,7 +341,7 @@ def stop_process(module, oracle_home): if msg: module.fail_json(msg=msg, changed=False) -def remove_patch (module, msg, oracle_home, patch_base, patch_id, opatchauto, ocm_response_file,output): +def remove_patch (module, msg, oracle_home, patch_base, patch_id, opatchauto, ocm_response_file, stop_processes, output): ''' Removes the patch ''' @@ -354,6 +354,9 @@ def remove_patch (module, msg, oracle_home, patch_base, patch_id, opatchauto, oc command = '%s/OPatch/%s %s -oh %s ' % (oracle_home,opatch_cmd, patch_base, oracle_home) else: + if stop_processes: + stop_process(module, oracle_home) + opatch_cmd = 'opatch rollback' command = '%s/OPatch/%s -id %s -silent' % (oracle_home,opatch_cmd, patch_id) @@ -547,7 +550,7 @@ def main(): elif state == 'absent': if check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto): - if remove_patch(module, msg, oracle_home, patch_base, patch_id, opatchauto,ocm_response_file, output): + if remove_patch(module, msg, oracle_home, patch_base, patch_id, opatchauto,ocm_response_file, stop_processes, output): if patch_version is not None: msg = 'Patch %s (%s) successfully removed from %s' % (patch_id,patch_version, oracle_home) else: From 192aa5f6c7432142cad04095818b1952590a6512 Mon Sep 17 00:00:00 2001 From: Thorsten Bruhns Date: Sun, 21 Apr 2019 09:20:21 +0200 Subject: [PATCH 08/13] oracle_db: Bugfix for start_db in Oracle Restart --- oracle_db | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/oracle_db b/oracle_db index 4f323bb..b3d5469 100644 --- a/oracle_db +++ b/oracle_db @@ -703,7 +703,8 @@ def apply_restart_changes(module,msg,oracle_home,db_name,db_unique_name,sid,inst for sql in change_restart_sql: execute_sql(module,msg,cursor,sql) if stop_db(module,msg,oracle_home,db_name,db_unique_name,sid): - if start_db(module,msg,oracle_home,db_name,db_unique_name, sid): + start_db_state = start_db(module,msg,oracle_home,db_name,db_unique_name, sid) + if start_db_state in ('ok', 'changed'): if newdb: msg = 'Database %s successfully created (%s) ' % (db_name, archcomp) if output == 'verbose': @@ -762,13 +763,25 @@ def start_db (module,msg, oracle_home, db_name, db_unique_name, sid): if gimanaged: if db_unique_name is not None: db_name = db_unique_name - command = '%s/bin/srvctl start database -d %s' % (oracle_home,db_name) + command = '%s/bin/srvctl status database -d %s' % (oracle_home,db_name) (rc, stdout, stderr) = module.run_command(command) if rc != 0: msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) module.fail_json(msg=msg, changed=False) + elif 'Database is running' in stdout: + return 'ok' + elif 'Database is not running' in stdout: + + command = '%s/bin/srvctl start database -d %s' % (oracle_home,db_name) + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + return 'changed' else: - return True + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) else: if sid is not None: os.environ['ORACLE_SID'] = sid @@ -789,7 +802,7 @@ def start_db (module,msg, oracle_home, db_name, db_unique_name, sid): msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, startup_sql) module.fail_json(msg=msg, changed=False) else: - return True + return 'changed' def start_instance (module,msg, oracle_home, db_name, db_unique_name,sid, open_mode, instance_name, host_name, israc): @@ -1033,9 +1046,13 @@ def main(): msg = "Database not found. %s" % msg module.fail_json(msg=msg, changed=False) else: - if start_db (module, msg, oracle_home, db_name, db_unique_name, sid): + start_db_state = start_db (module, msg, oracle_home, db_name, db_unique_name, sid) + if start_db_state == 'changed': msg = "Database started." module.exit_json(msg=msg, changed=True) + if start_db_state == 'ok': + msg = "Database already running." + module.exit_json(msg=msg, changed=False) else: msg = "Startup failed. %s" % msg module.fail_json(msg=msg, changed=False) From c10fe265a393ee4e3d879e90c3ef91a03214d7ba Mon Sep 17 00:00:00 2001 From: Dietmar Uhlig Date: Mon, 25 Nov 2019 17:34:07 +0100 Subject: [PATCH 09/13] Some interim patches are PSU/RU dependent. The patch ID is the same but content, dependencies, and UPI (unique patch ID) differ. These patches have to be rolled back in advance of applying the PSU/RU and must afterwards be installed with the correct version (=UPI). They should however not be rolled back if they are already there with the desired UPI. This helps to write more idempotent playbooks. So there is a new parameter exclude_upi that is evaluated only if state=absent. To keep changes in working code at a minimum the check for UPI could be acquired only by another "opatch lspatches" that costs some seconds. --- oracle_opatch | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/oracle_opatch b/oracle_opatch index fd359ea..f1750c8 100644 --- a/oracle_opatch +++ b/oracle_opatch @@ -37,6 +37,15 @@ options: required: False default: None aliases: ['version_added'] + exclude_upi: + description: + - The unique patch identifier that should not be rolled back + e.g 22990084 + - This option will be honored only when state=absent. + - It helps to write idempotent playbooks when changing interim patches that are dependent on a specific PSU/RU. + required: False + default: None + aliases: ['exclude_unique_patch_id'] opatch_minversion: description: - The minimum version of opatch needed @@ -155,7 +164,7 @@ def get_file_owner(module, msg, oracle_home): msg = 'Could not determine owner of %s ' % (checkfile) module.fail_json(msg=msg) -def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto): +def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): ''' Gets all patches already applied and compares to the intended patch @@ -169,8 +178,8 @@ def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatc (rc, stdout, stderr) = module.run_command(command) #module.exit_json(msg=stdout, changed=False) if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) else: if opatchauto: chk = '%s' % (patch_version) @@ -180,7 +189,17 @@ def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatc chk = '%s' % (patch_id) if chk in stdout: - return True + if exclude_upi is None: + return True + else: + command += ' -id %s' % patch_id + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + chk = 'unique_patch_id:%s' % exclude_upi + return (chk not in stdout) else: return False @@ -450,6 +469,7 @@ def main(): patch_base = dict(default=None, aliases = ['path','source','patch_source','phBaseDir']), patch_id = dict(default=None, aliases = ['id']), patch_version = dict(required=None, aliases = ['version']), + exclude_upi = dict(required=None, aliases = ['exclude_unique_patch_id']), opatch_minversion = dict(default=None, aliases = ['opmv']), opatchauto = dict(default='False', type='bool',aliases = ['autopatch']), rolling = dict(default='True', type='bool',aliases = ['rolling']), @@ -461,7 +481,7 @@ def main(): output = dict(default="short", choices = ["short","verbose"]), state = dict(default="present", choices = ["present", "absent", "opatchversion"]), hostname = dict(required=False, default = 'localhost', aliases = ['host']), - port = dict(required=False, default = 1521), + port = dict(required=False, type='int', default = 1521), @@ -473,6 +493,7 @@ def main(): patch_base = module.params["patch_base"] patch_id = module.params["patch_id"] patch_version = module.params["patch_version"] + exclude_upi = module.params["exclude_upi"] opatch_minversion = module.params["opatch_minversion"] opatchauto = module.params["opatchauto"] rolling = module.params["rolling"] @@ -533,7 +554,7 @@ def main(): module.exit_json(msg=opatch_version,changed=False) if state == 'present': - if not check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto): + if not check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, None): if apply_patch(module, msg, oracle_home, patch_base, patch_id,patch_version, opatchauto,ocm_response_file,offline,stop_processes,rolling,output): if patch_version is not None: msg = 'Patch %s (%s) successfully applied to %s' % (patch_id,patch_version, oracle_home) @@ -549,7 +570,7 @@ def main(): module.exit_json(msg=msg, changed=False) elif state == 'absent': - if check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto): + if check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): if remove_patch(module, msg, oracle_home, patch_base, patch_id, opatchauto,ocm_response_file, stop_processes, output): if patch_version is not None: msg = 'Patch %s (%s) successfully removed from %s' % (patch_id,patch_version, oracle_home) @@ -560,8 +581,9 @@ def main(): module.fail_json(msg=msg, changed=False) else: if patch_version is not None: - msg = 'Patch %s (%s) is not applied to %s' % (patch_id,patch_version, oracle_home) - + msg = 'Patch %s (%s) is not applied to %s' % (patch_id, patch_version, oracle_home) + elif exclude_upi is not None: + msg = 'Patch %s (UPI %s) has already been applied and will not be removed from %s' % (patch_id, exclude_upi, oracle_home) else: msg = 'Patch %s is not applied to %s' % (patch_id, oracle_home) From 3ec6f89531e5d7c3d563cbe277908a44475049bf Mon Sep 17 00:00:00 2001 From: Dietmar Uhlig Date: Fri, 6 Dec 2019 16:31:10 +0100 Subject: [PATCH 10/13] Use Thorsten's fix over mine. --- oracle_opatch | 1200 ++++++++++++++++++++++++------------------------- 1 file changed, 600 insertions(+), 600 deletions(-) diff --git a/oracle_opatch b/oracle_opatch index f1750c8..dffc1b7 100644 --- a/oracle_opatch +++ b/oracle_opatch @@ -1,600 +1,600 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -DOCUMENTATION = ''' ---- -module: oracle_opatch -short_description: Manage patches in an Oracle environment - - Manages patches (applies/rolls back) - - Only manages the opatch part of patching (opatch/opatch auto/opatchauto) - - If opatchauto is true, the task has to be run as root -version_added: "2.4.0.0" -options: - oracle_home: - description: - - The home which will be patched - required: True - aliases: ['oh'] - patch_base: - description: - - Path to where the patch is located - e.g /nfs/patches/12.1.0.2/27468957 - required: False - default: None - aliases: ['path','source','patch_source','phBaseDir'] - patch_id: - description: - - The patch id - e.g 27468957 - required: False - default: None - aliases: ['id'] - patch_version: - description: - - The patch version - e.g 12.2.0.1.180417 - - This key is mandatory if you're applying a 'opatchauto' type of patch - required: False - default: None - aliases: ['version_added'] - exclude_upi: - description: - - The unique patch identifier that should not be rolled back - e.g 22990084 - - This option will be honored only when state=absent. - - It helps to write idempotent playbooks when changing interim patches that are dependent on a specific PSU/RU. - required: False - default: None - aliases: ['exclude_unique_patch_id'] - opatch_minversion: - description: - - The minimum version of opatch needed - - If this key is set, a comparison is made between existing version and opatch_minversion - If existing < opatch_minversion an error is raised - required: False - default: None - aliases: ['opmv'] - opatchauto: - description: - - Should the patch be applied using opatchato - - If set to true, the task has to run as 'root' - required: False - default: False - aliases: ['autopatch'] - conflict_check: - description: - - Should a conflict check be run before applying a patch. - - If the check errors the module exits with a failure - required: False - default: True - stop_processes: - description: - - Stop Instances and Listener before applying a patch in ORACLE_HOME - required: False - default: False - rolling: - description: - - Should a patch installed in rolling upgrade mode? - required: False - default: True - ocm_response_file: - description: - - The OCM responsefile needed for OPatch versions < '12.2.0.1.5' (basically for DB/GI versions < 12.1) - required: False - default: False - state: - description: - - Should a patch be applied or removed - - present = applied, absent = removed, opatchversion = returns the version of opatch - default: present - choices: ['present','absent','opatchversion'] - hostname: - description: - - The host of the database if using dbms_service - required: false - default: localhost - aliases: ['host'] - port: - description: - - The listener port to connect to the database if using dbms_service - required: false - default: 1521 - -notes: - - -requirements: [ "os","pwd","distutils.version" ] -author: Mikael Sandström, oravirt@gmail.com, @oravirt -''' - -EXAMPLES = ''' - -''' -import os, pwd -from distutils.version import LooseVersion -# -# try: -# import cx_Oracle -# except ImportError: -# cx_oracle_exists = False -# else: -# cx_oracle_exists = True - - -def get_version(module, msg, oracle_home): - ''' - Returns the DB server version - ''' - - command = '%s/bin/sqlplus -V' % (oracle_home) - (rc, stdout, stderr) = module.run_command(command) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - else: - return stdout.split(' ')[2][0:4] - -def get_opatch_version(module, msg, oracle_home): - ''' - Returns the Opatch version - ''' - - command = '%s/OPatch/opatch version' % (oracle_home) - (rc, stdout, stderr) = module.run_command(command) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - else: - return stdout.split('\n')[0].split(':')[1].strip() - -def get_file_owner(module, msg, oracle_home): - ''' - This will only be run if opatchauto is True. - The owner of ORACLE_HOME has to be established, and we do this be checking file - ownership on ORACLE_HOME/bin/oracle. - returns the owner - ''' - - checkfile = '%s/bin/oracle' % (oracle_home) - if os.path.exists(checkfile): - stat_info = os.stat(checkfile) - uid = stat_info.st_uid - user = pwd.getpwuid(uid)[0] - return user - else: - msg = 'Could not determine owner of %s ' % (checkfile) - module.fail_json(msg=msg) - -def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): - ''' - Gets all patches already applied and compares to the - intended patch - ''' - - command = '' - if opatchauto: - oh_owner = get_file_owner(module,msg,oracle_home) - command += 'sudo -u %s ' % (oh_owner) - command += '%s/OPatch/opatch lspatches ' % (oracle_home) - (rc, stdout, stderr) = module.run_command(command) - #module.exit_json(msg=stdout, changed=False) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - else: - if opatchauto: - chk = '%s' % (patch_version) - elif not opatchauto and patch_id is not None and patch_version is not None: - chk = '%s (%s)' % (patch_version,patch_id) - else: - chk = '%s' % (patch_id) - - if chk in stdout: - if exclude_upi is None: - return True - else: - command += ' -id %s' % patch_id - (rc, stdout, stderr) = module.run_command(command) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - else: - chk = 'unique_patch_id:%s' % exclude_upi - return (chk not in stdout) - else: - return False - -def analyze_patch (module, msg, oracle_home, patch_base, opatchauto): - - checks = [] - - if opatchauto: - if major_version < '12.1': - oh_owner = get_file_owner(module,msg,oracle_home) - command = '' - command += 'sudo -u %s ' % (oh_owner) - opatch_cmd = 'opatch ' - conflcommand = '%s %s/OPatch/opatch prereq CheckConflictAgainstOHWithDetail -ph %s -oh %s' % (command,oracle_home, patch_base, oracle_home) - spacecommand = '%s %s/OPatch/opatch prereq CheckSystemSpace -ph %s -oh %s' % (command,oracle_home, patch_base, oracle_home) - checks.append(conflcommand) - checks.append(spacecommand) - - else: - opatch_cmd = 'opatchauto' - command = '%s/OPatch/%s apply %s -oh %s -analyze' % (oracle_home, opatch_cmd, patch_base, oracle_home) - checks.append(command) - else: - conflcommand = '%s/OPatch/opatch prereq CheckConflictAgainstOHWithDetail -ph %s -oh %s' % (oracle_home, patch_base, oracle_home) - spacecommand = '%s/OPatch/opatch prereq CheckSystemSpace -ph %s -oh %s' % (oracle_home, patch_base, oracle_home) - checks.append(conflcommand) - checks.append(spacecommand) - - for cmd in checks: - (rc, stdout, stderr) = module.run_command(cmd) - # module.exit_json(msg=stdout, changed=False) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, cmd) - module.fail_json(msg=msg, changed=False) - elif rc == 0 and 'failed' in stdout: # <- Conflicts exist - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,cmd) - module.fail_json(msg=msg, changed=False) - else: - return True - -def apply_patch (module, msg, oracle_home, patch_base, patch_id, patch_version, opatchauto, ocm_response_file, offline, stop_processes, rolling, output): - ''' - Applies the patch - ''' - - if conflict_check: - if not analyze_patch(module, msg, oracle_home, patch_base, opatchauto): - module.fail_json(msg='Prereq checks failed') - - if opatchauto: - opoptions = '' - if major_version < '12.1': - opatch_cmd = 'opatch auto' - if offline: - oh = ' -och' - else: - oh = ' -oh' - else: - oh = ' -oh' - opatch_cmd = 'opatchauto apply' - - if not rolling: - opoptions += ' -nonrolling ' - - command = '%s/OPatch/%s %s %s %s %s' % (oracle_home,opatch_cmd, opoptions, patch_base,oh,oracle_home) - - else: - if stop_processes: - stop_process(module, oracle_home) - - opatch_cmd = 'opatch' - command = '%s/OPatch/%s apply %s -oh %s -silent' % (oracle_home,opatch_cmd, patch_base,oracle_home) - - if ocm_response_file is not None and (LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm)): - command += ' -ocmrf %s' % (ocm_response_file) - - (rc, stdout, stderr) = module.run_command(command) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - elif rc == 0 and 'Opatch version check failed' in stdout: # OPatch version check failed - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) - module.fail_json(msg=msg, changed=False) - else: - checks = ['successfully applied' in stdout, - 'patch applied successfully' in stdout, - 'apply successful' in stdout] - if any(checks): - if output == 'short': - return True - else: - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) - module.exit_json(msg=msg, changed=True) - else: - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) - module.exit_json(msg=msg, changed=False) - -def stop_process(module, oracle_home): - ''' - Stop processes in ORACLE_HOME for non GI/Restart Environments - ''' - - oratabfile = '/etc/oratab' - - if os.path.exists(oratabfile): - with open(oratabfile) as oratab: - - msg = '' - - for line in oratab: - if line.startswith('#') or line.startswith(' '): - continue - elif len(line.split(':')) >= 2 and line.split(':')[1] == oracle_home: - - # Find listener for ORACLE_HOME - p = subprocess.Popen('ps -o cmd -C tnslsnr | grep "^%s/bin/tnslsnr "' % (line) , shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, error = p.communicate() - p_status = p.wait() - - if output: - for lline in output.split('\n'): - - proclen = len("%s/bin/tnslsnr " % (oracle_home)) - - # remove executable from ps output, split by ' ' - # => 1st element is listener_name - # ps example: /.../bin/tnslsnr LISTENER -inherit - listener_name = lline[proclen:].split(' ')[0] - if len(listener_name) > 0: - lsnrctl_bin = '%s/bin/lsnrctl' % (oracle_home) - try: - p = subprocess.check_call([lsnrctl_bin, 'stop', '%s' % listener_name]) - except subprocess.CalledProcessError: - msg += 'Stop of Listener %s failed ' % listener_name - - # Stop instances in ORACLE_HOME - # The [0-9] is used to remove the grep itself from the result! - p = subprocess.Popen('ps -elf| grep "[0-9] ora_pmon_%s"' % (line.split(':')[0]) , shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, error = p.communicate() - p_status = p.wait() - - if output and p.returncode == 0: - os.environ['ORACLE_SID'] = line.split(':')[0] - shutdown_sql = '''connect / as sysdba - shutdown immediate; - exit''' - sqlplus_bin = '%s/bin/sqlplus' % (oracle_home) - p = subprocess.Popen([sqlplus_bin,'/nolog'],stdin=subprocess.PIPE, - stdout=subprocess.PIPE,stderr=subprocess.PIPE) - (stdout,stderr) = p.communicate(shutdown_sql.encode('utf-8')) - p_status = p.wait() - #module.fail_json(msg=stdout, changed=False) - - rc = p.returncode - if rc != 0: - msg += 'Stop of Instance %s failed' % line.split(':')[0] - - if msg: - module.fail_json(msg=msg, changed=False) - -def remove_patch (module, msg, oracle_home, patch_base, patch_id, opatchauto, ocm_response_file, stop_processes, output): - ''' - Removes the patch - ''' - - if opatchauto: - if major_version < '12.1': - opatch_cmd = 'opatch auto -rollback' - else: - opatch_cmd = 'opatchauto rollback' - - command = '%s/OPatch/%s %s -oh %s ' % (oracle_home,opatch_cmd, patch_base, oracle_home) - else: - if stop_processes: - stop_process(module, oracle_home) - - opatch_cmd = 'opatch rollback' - command = '%s/OPatch/%s -id %s -silent' % (oracle_home,opatch_cmd, patch_id) - - - if ocm_response_file is not None and (LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm)): - command += ' -ocmrf %s' % (ocm_response_file) - - #module.exit_json(msg=command, changed=False) - (rc, stdout, stderr) = module.run_command(command) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - else: - checks = ['RollbackSession removing interim patch' in stdout, - 'rolled back successfully' in stdout, - 'rollback successful' in stdout] - if any(checks): - if output == 'short': - return True - else: - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) - module.exit_json(msg=msg, changed=True) - else: - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) - module.exit_json(msg=msg, changed=False) - - -# -# def execute_sql_get(module, msg, cursor, sql): -# -# try: -# cursor.execute(sql) -# result = (cursor.fetchall()) -# except cx_Oracle.DatabaseError as exc: -# error, = exc.args -# msg = 'Something went wrong while executing sql_get - %s sql: %s' % (error.message, sql) -# module.fail_json(msg=msg, changed=False) -# return False -# return result -# -# def execute_sql(module, msg, cursor, sql): -# -# try: -# cursor.execute(sql) -# except cx_Oracle.DatabaseError as exc: -# error, = exc.args -# msg = 'Something went wrong while executing sql - %s sql: %s' % (error.message, sql) -# module.fail_json(msg=msg, changed=False) -# return False -# return True -# -# def getconn(module,msg): -# -# hostname = os.uname()[1] -# wallet_connect = '/@%s' % service_name -# try: -# if (not user and not password ): # If neither user or password is supplied, the use of an oracle wallet is assumed -# connect = wallet_connect -# conn = cx_Oracle.connect(wallet_connect, mode=cx_Oracle.SYSDBA) -# elif (user and password ): -# dsn = cx_Oracle.makedsn(host=hostname, port=port, service_name=service_name, ) -# connect = dsn -# conn = cx_Oracle.connect(user, password, dsn, mode=cx_Oracle.SYSDBA) -# elif (not(user) or not(password)): -# module.fail_json(msg='Missing username or password for cx_Oracle') -# -# except cx_Oracle.DatabaseError as exc: -# error, = exc.args -# msg = 'Could not connect to database - %s, connect descriptor: %s' % (error.message, connect) -# module.fail_json(msg=msg, changed=False) -# -# cursor = conn.cursor() -# return cursor - - - -def main(): - - msg = [''] - cursor = None - global major_version - global opatch_version - global opatch_version_noocm - global hostname - global port - global conflict_check - - module = AnsibleModule( - argument_spec = dict( - oracle_home = dict(required=True, aliases = ['oh']), - patch_base = dict(default=None, aliases = ['path','source','patch_source','phBaseDir']), - patch_id = dict(default=None, aliases = ['id']), - patch_version = dict(required=None, aliases = ['version']), - exclude_upi = dict(required=None, aliases = ['exclude_unique_patch_id']), - opatch_minversion = dict(default=None, aliases = ['opmv']), - opatchauto = dict(default='False', type='bool',aliases = ['autopatch']), - rolling = dict(default='True', type='bool',aliases = ['rolling']), - conflict_check = dict(default='True', type='bool'), - ocm_response_file = dict(required=None,aliases = ['ocmrf']), - offline = dict(default='False', type='bool'), -# stop_processes = dict(default='True', type='bool'), - stop_processes = dict(default='False', type='bool'), - output = dict(default="short", choices = ["short","verbose"]), - state = dict(default="present", choices = ["present", "absent", "opatchversion"]), - hostname = dict(required=False, default = 'localhost', aliases = ['host']), - port = dict(required=False, type='int', default = 1521), - - - - ), - - ) - - oracle_home = module.params["oracle_home"] - patch_base = module.params["patch_base"] - patch_id = module.params["patch_id"] - patch_version = module.params["patch_version"] - exclude_upi = module.params["exclude_upi"] - opatch_minversion = module.params["opatch_minversion"] - opatchauto = module.params["opatchauto"] - rolling = module.params["rolling"] - conflict_check = module.params["conflict_check"] - ocm_response_file = module.params["ocm_response_file"] - offline = module.params["offline"] - stop_processes = module.params["stop_processes"] - output = module.params["output"] - state = module.params["state"] - hostname = module.params["hostname"] - port = module.params["port"] - - - if not os.path.exists(oracle_home): - msg = 'oracle_home: %s doesn\'t exist' % (oracle_home) - module.fail_json(msg=msg, changed=False) - - if not os.path.exists('%s/OPatch/opatch' % (oracle_home)): - msg = 'OPatch doesn\'t seem to exist in %s/OPatch/' % (oracle_home) - module.fail_json(msg=msg, changed=False) - - if (patch_base or patch_id) is None and state in ('present','absent'): - msg = 'patch_base & patch_id needs to be set' - module.fail_json(msg=msg, changed=False) - - if opatchauto: - if patch_version is None: - msg = 'patch_version (e.g 12.1.0.2.1801417) needs to be set if opatchauto is True' - module.fail_json(msg=msg, changed=False) - - if oracle_home is not None: - os.environ['ORACLE_HOME'] = oracle_home - #os.environ['LD_LIBRARY_PATH'] = ld_library_path - elif 'ORACLE_HOME' in os.environ: - oracle_home = os.environ['ORACLE_HOME'] - #ld_library_path = os.environ['LD_LIBRARY_PATH'] - else: - msg = 'ORACLE_HOME variable not set. Please set it and re-run the command' - module.fail_json(msg=msg, changed=False) - - - # Get the Oracle % Opatch version - major_version = get_version(module,msg,oracle_home) - opatch_version = get_opatch_version(module,msg,oracle_home) - opatch_version_noocm = '12.2.0.1.5' - - if opatch_minversion is not None: - opatch_minversion_ = opatch_minversion.replace('.','') - if LooseVersion(opatch_version) < LooseVersion(opatch_minversion): - msg = 'Current OPatch version: %s, minimum version needed is: %s' % (opatch_version,opatch_minversion) - module.fail_json(msg=msg, changed=False) - - if state == 'present' and ocm_response_file is None and LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm): - msg='An OCM response file is needed when the opatch version is < %s. Current opatch version: %s' % (opatch_version_noocm,opatch_version) - module.fail_json(msg=msg,changed=False) - - if state == 'opatchversion': - module.exit_json(msg=opatch_version,changed=False) - - if state == 'present': - if not check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, None): - if apply_patch(module, msg, oracle_home, patch_base, patch_id,patch_version, opatchauto,ocm_response_file,offline,stop_processes,rolling,output): - if patch_version is not None: - msg = 'Patch %s (%s) successfully applied to %s' % (patch_id,patch_version, oracle_home) - else: - msg = 'Patch %s successfully applied to %s' % (patch_id, oracle_home) - module.exit_json(msg=msg, changed=True) - - else: - if patch_version is not None: - msg = 'Patch %s (%s) is already applied to %s' % (patch_id,patch_version, oracle_home) - else: - msg = 'Patch %s is already applied to %s' % (patch_id, oracle_home) - module.exit_json(msg=msg, changed=False) - - elif state == 'absent': - if check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): - if remove_patch(module, msg, oracle_home, patch_base, patch_id, opatchauto,ocm_response_file, stop_processes, output): - if patch_version is not None: - msg = 'Patch %s (%s) successfully removed from %s' % (patch_id,patch_version, oracle_home) - else: - msg = 'Patch %s successfully removed from %s' % (patch_id, oracle_home) - module.exit_json(msg=msg, changed=True) - else: - module.fail_json(msg=msg, changed=False) - else: - if patch_version is not None: - msg = 'Patch %s (%s) is not applied to %s' % (patch_id, patch_version, oracle_home) - elif exclude_upi is not None: - msg = 'Patch %s (UPI %s) has already been applied and will not be removed from %s' % (patch_id, exclude_upi, oracle_home) - else: - msg = 'Patch %s is not applied to %s' % (patch_id, oracle_home) - - module.exit_json(msg=msg, changed=False) - - - module.exit_json(msg="Unhandled exit", changed=False) - - - - -from ansible.module_utils.basic import * -if __name__ == '__main__': - main() +#!/usr/bin/python +# -*- coding: utf-8 -*- + +DOCUMENTATION = ''' +--- +module: oracle_opatch +short_description: Manage patches in an Oracle environment + - Manages patches (applies/rolls back) + - Only manages the opatch part of patching (opatch/opatch auto/opatchauto) + - If opatchauto is true, the task has to be run as root +version_added: "2.4.0.0" +options: + oracle_home: + description: + - The home which will be patched + required: True + aliases: ['oh'] + patch_base: + description: + - Path to where the patch is located + e.g /nfs/patches/12.1.0.2/27468957 + required: False + default: None + aliases: ['path','source','patch_source','phBaseDir'] + patch_id: + description: + - The patch id + e.g 27468957 + required: False + default: None + aliases: ['id'] + patch_version: + description: + - The patch version + e.g 12.2.0.1.180417 + - This key is mandatory if you're applying a 'opatchauto' type of patch + required: False + default: None + aliases: ['version_added'] + exclude_upi: + description: + - The unique patch identifier that should not be rolled back + e.g 22990084 + - This option will be honored only when state=absent. + - It helps to write idempotent playbooks when changing interim patches that are dependent on a specific PSU/RU. + required: False + default: None + aliases: ['exclude_unique_patch_id'] + opatch_minversion: + description: + - The minimum version of opatch needed + - If this key is set, a comparison is made between existing version and opatch_minversion + If existing < opatch_minversion an error is raised + required: False + default: None + aliases: ['opmv'] + opatchauto: + description: + - Should the patch be applied using opatchato + - If set to true, the task has to run as 'root' + required: False + default: False + aliases: ['autopatch'] + conflict_check: + description: + - Should a conflict check be run before applying a patch. + - If the check errors the module exits with a failure + required: False + default: True + stop_processes: + description: + - Stop Instances and Listener before applying a patch in ORACLE_HOME + required: False + default: False + rolling: + description: + - Should a patch installed in rolling upgrade mode? + required: False + default: True + ocm_response_file: + description: + - The OCM responsefile needed for OPatch versions < '12.2.0.1.5' (basically for DB/GI versions < 12.1) + required: False + default: False + state: + description: + - Should a patch be applied or removed + - present = applied, absent = removed, opatchversion = returns the version of opatch + default: present + choices: ['present','absent','opatchversion'] + hostname: + description: + - The host of the database if using dbms_service + required: false + default: localhost + aliases: ['host'] + port: + description: + - The listener port to connect to the database if using dbms_service + required: false + default: 1521 + +notes: + - +requirements: [ "os","pwd","distutils.version" ] +author: Mikael Sandström, oravirt@gmail.com, @oravirt +''' + +EXAMPLES = ''' + +''' +import os, pwd +from distutils.version import LooseVersion +# +# try: +# import cx_Oracle +# except ImportError: +# cx_oracle_exists = False +# else: +# cx_oracle_exists = True + + +def get_version(module, msg, oracle_home): + ''' + Returns the DB server version + ''' + + command = '%s/bin/sqlplus -V' % (oracle_home) + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + return stdout.split(' ')[2][0:4] + +def get_opatch_version(module, msg, oracle_home): + ''' + Returns the Opatch version + ''' + + command = '%s/OPatch/opatch version' % (oracle_home) + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + return stdout.split('\n')[0].split(':')[1].strip() + +def get_file_owner(module, msg, oracle_home): + ''' + This will only be run if opatchauto is True. + The owner of ORACLE_HOME has to be established, and we do this be checking file + ownership on ORACLE_HOME/bin/oracle. + returns the owner + ''' + + checkfile = '%s/bin/oracle' % (oracle_home) + if os.path.exists(checkfile): + stat_info = os.stat(checkfile) + uid = stat_info.st_uid + user = pwd.getpwuid(uid)[0] + return user + else: + msg = 'Could not determine owner of %s ' % (checkfile) + module.fail_json(msg=msg) + +def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): + ''' + Gets all patches already applied and compares to the + intended patch + ''' + + command = '' + if opatchauto: + oh_owner = get_file_owner(module,msg,oracle_home) + command += 'sudo -u %s ' % (oh_owner) + command += '%s/OPatch/opatch lspatches ' % (oracle_home) + (rc, stdout, stderr) = module.run_command(command) + #module.exit_json(msg=stdout, changed=False) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + if opatchauto: + chk = '%s' % (patch_version) + elif not opatchauto and patch_id is not None and patch_version is not None: + chk = '%s (%s)' % (patch_version,patch_id) + else: + chk = '%s' % (patch_id) + + if chk in stdout: + if exclude_upi is None: + return True + else: + command += ' -id %s' % patch_id + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + chk = 'unique_patch_id:%s' % exclude_upi + return (chk not in stdout) + else: + return False + +def analyze_patch (module, msg, oracle_home, patch_base, opatchauto): + + checks = [] + + if opatchauto: + if major_version < '12.1': + oh_owner = get_file_owner(module,msg,oracle_home) + command = '' + command += 'sudo -u %s ' % (oh_owner) + opatch_cmd = 'opatch ' + conflcommand = '%s %s/OPatch/opatch prereq CheckConflictAgainstOHWithDetail -ph %s -oh %s' % (command,oracle_home, patch_base, oracle_home) + spacecommand = '%s %s/OPatch/opatch prereq CheckSystemSpace -ph %s -oh %s' % (command,oracle_home, patch_base, oracle_home) + checks.append(conflcommand) + checks.append(spacecommand) + + else: + opatch_cmd = 'opatchauto' + command = '%s/OPatch/%s apply %s -oh %s -analyze' % (oracle_home, opatch_cmd, patch_base, oracle_home) + checks.append(command) + else: + conflcommand = '%s/OPatch/opatch prereq CheckConflictAgainstOHWithDetail -ph %s -oh %s' % (oracle_home, patch_base, oracle_home) + spacecommand = '%s/OPatch/opatch prereq CheckSystemSpace -ph %s -oh %s' % (oracle_home, patch_base, oracle_home) + checks.append(conflcommand) + checks.append(spacecommand) + + for cmd in checks: + (rc, stdout, stderr) = module.run_command(cmd) + # module.exit_json(msg=stdout, changed=False) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, cmd) + module.fail_json(msg=msg, changed=False) + elif rc == 0 and 'failed' in stdout: # <- Conflicts exist + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,cmd) + module.fail_json(msg=msg, changed=False) + else: + return True + +def apply_patch (module, msg, oracle_home, patch_base, patch_id, patch_version, opatchauto, ocm_response_file, offline, stop_processes, rolling, output): + ''' + Applies the patch + ''' + + if conflict_check: + if not analyze_patch(module, msg, oracle_home, patch_base, opatchauto): + module.fail_json(msg='Prereq checks failed') + + if opatchauto: + opoptions = '' + if major_version < '12.1': + opatch_cmd = 'opatch auto' + if offline: + oh = ' -och' + else: + oh = ' -oh' + else: + oh = ' -oh' + opatch_cmd = 'opatchauto apply' + + if not rolling: + opoptions += ' -nonrolling ' + + command = '%s/OPatch/%s %s %s %s %s' % (oracle_home,opatch_cmd, opoptions, patch_base,oh,oracle_home) + + else: + if stop_processes: + stop_process(module, oracle_home) + + opatch_cmd = 'opatch' + command = '%s/OPatch/%s apply %s -oh %s -silent' % (oracle_home,opatch_cmd, patch_base,oracle_home) + + if ocm_response_file is not None and (LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm)): + command += ' -ocmrf %s' % (ocm_response_file) + + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + elif rc == 0 and 'Opatch version check failed' in stdout: # OPatch version check failed + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) + module.fail_json(msg=msg, changed=False) + else: + checks = ['successfully applied' in stdout, + 'patch applied successfully' in stdout, + 'apply successful' in stdout] + if any(checks): + if output == 'short': + return True + else: + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) + module.exit_json(msg=msg, changed=True) + else: + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) + module.exit_json(msg=msg, changed=False) + +def stop_process(module, oracle_home): + ''' + Stop processes in ORACLE_HOME for non GI/Restart Environments + ''' + + oratabfile = '/etc/oratab' + + if os.path.exists(oratabfile): + with open(oratabfile) as oratab: + + msg = '' + + for line in oratab: + if line.startswith('#') or line.startswith(' '): + continue + elif len(line.split(':')) >= 2 and line.split(':')[1] == oracle_home: + + # Find listener for ORACLE_HOME + p = subprocess.Popen('ps -o cmd -C tnslsnr | grep "^%s/bin/tnslsnr "' % (line) , shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, error = p.communicate() + p_status = p.wait() + + if output: + for lline in output.split('\n'): + + proclen = len("%s/bin/tnslsnr " % (oracle_home)) + + # remove executable from ps output, split by ' ' + # => 1st element is listener_name + # ps example: /.../bin/tnslsnr LISTENER -inherit + listener_name = lline[proclen:].split(' ')[0] + if len(listener_name) > 0: + lsnrctl_bin = '%s/bin/lsnrctl' % (oracle_home) + try: + p = subprocess.check_call([lsnrctl_bin, 'stop', '%s' % listener_name]) + except subprocess.CalledProcessError: + msg += 'Stop of Listener %s failed ' % listener_name + + # Stop instances in ORACLE_HOME + # The [0-9] is used to remove the grep itself from the result! + p = subprocess.Popen('ps -elf| grep "[0-9] ora_pmon_%s"' % (line.split(':')[0]) , shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, error = p.communicate() + p_status = p.wait() + + if output and p.returncode == 0: + os.environ['ORACLE_SID'] = line.split(':')[0] + shutdown_sql = '''connect / as sysdba + shutdown immediate; + exit''' + sqlplus_bin = '%s/bin/sqlplus' % (oracle_home) + p = subprocess.Popen([sqlplus_bin,'/nolog'],stdin=subprocess.PIPE, + stdout=subprocess.PIPE,stderr=subprocess.PIPE) + (stdout,stderr) = p.communicate(shutdown_sql.encode('utf-8')) + p_status = p.wait() + #module.fail_json(msg=stdout, changed=False) + + rc = p.returncode + if rc != 0: + msg += 'Stop of Instance %s failed' % line.split(':')[0] + + if msg: + module.fail_json(msg=msg, changed=False) + +def remove_patch (module, msg, oracle_home, patch_base, patch_id, opatchauto, ocm_response_file, stop_processes, output): + ''' + Removes the patch + ''' + + if opatchauto: + if major_version < '12.1': + opatch_cmd = 'opatch auto -rollback' + else: + opatch_cmd = 'opatchauto rollback' + + command = '%s/OPatch/%s %s -oh %s ' % (oracle_home,opatch_cmd, patch_base, oracle_home) + else: + if stop_processes: + stop_process(module, oracle_home) + + opatch_cmd = 'opatch rollback' + command = '%s/OPatch/%s -id %s -silent' % (oracle_home,opatch_cmd, patch_id) + + + if ocm_response_file is not None and (LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm)): + command += ' -ocmrf %s' % (ocm_response_file) + + #module.exit_json(msg=command, changed=False) + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + checks = ['RollbackSession removing interim patch' in stdout, + 'rolled back successfully' in stdout, + 'rollback successful' in stdout] + if any(checks): + if output == 'short': + return True + else: + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) + module.exit_json(msg=msg, changed=True) + else: + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) + module.exit_json(msg=msg, changed=False) + + +# +# def execute_sql_get(module, msg, cursor, sql): +# +# try: +# cursor.execute(sql) +# result = (cursor.fetchall()) +# except cx_Oracle.DatabaseError as exc: +# error, = exc.args +# msg = 'Something went wrong while executing sql_get - %s sql: %s' % (error.message, sql) +# module.fail_json(msg=msg, changed=False) +# return False +# return result +# +# def execute_sql(module, msg, cursor, sql): +# +# try: +# cursor.execute(sql) +# except cx_Oracle.DatabaseError as exc: +# error, = exc.args +# msg = 'Something went wrong while executing sql - %s sql: %s' % (error.message, sql) +# module.fail_json(msg=msg, changed=False) +# return False +# return True +# +# def getconn(module,msg): +# +# hostname = os.uname()[1] +# wallet_connect = '/@%s' % service_name +# try: +# if (not user and not password ): # If neither user or password is supplied, the use of an oracle wallet is assumed +# connect = wallet_connect +# conn = cx_Oracle.connect(wallet_connect, mode=cx_Oracle.SYSDBA) +# elif (user and password ): +# dsn = cx_Oracle.makedsn(host=hostname, port=port, service_name=service_name, ) +# connect = dsn +# conn = cx_Oracle.connect(user, password, dsn, mode=cx_Oracle.SYSDBA) +# elif (not(user) or not(password)): +# module.fail_json(msg='Missing username or password for cx_Oracle') +# +# except cx_Oracle.DatabaseError as exc: +# error, = exc.args +# msg = 'Could not connect to database - %s, connect descriptor: %s' % (error.message, connect) +# module.fail_json(msg=msg, changed=False) +# +# cursor = conn.cursor() +# return cursor + + + +def main(): + + msg = [''] + cursor = None + global major_version + global opatch_version + global opatch_version_noocm + global hostname + global port + global conflict_check + + module = AnsibleModule( + argument_spec = dict( + oracle_home = dict(required=True, aliases = ['oh']), + patch_base = dict(default=None, aliases = ['path','source','patch_source','phBaseDir']), + patch_id = dict(default=None, aliases = ['id']), + patch_version = dict(required=None, aliases = ['version']), + exclude_upi = dict(required=None, aliases = ['exclude_unique_patch_id']), + opatch_minversion = dict(default=None, aliases = ['opmv']), + opatchauto = dict(default='False', type='bool',aliases = ['autopatch']), + rolling = dict(default='True', type='bool',aliases = ['rolling']), + conflict_check = dict(default='True', type='bool'), + ocm_response_file = dict(required=None,aliases = ['ocmrf']), + offline = dict(default='False', type='bool'), +# stop_processes = dict(default='True', type='bool'), + stop_processes = dict(default='False', type='bool'), + output = dict(default="short", choices = ["short","verbose"]), + state = dict(default="present", choices = ["present", "absent", "opatchversion"]), + hostname = dict(required=False, default = 'localhost', aliases = ['host']), + port = dict(required=False, type='int', default = 1521), + + + + ), + + ) + + oracle_home = module.params["oracle_home"] + patch_base = module.params["patch_base"] + patch_id = module.params["patch_id"] + patch_version = module.params["patch_version"] + exclude_upi = module.params["exclude_upi"] + opatch_minversion = module.params["opatch_minversion"] + opatchauto = module.params["opatchauto"] + rolling = module.params["rolling"] + conflict_check = module.params["conflict_check"] + ocm_response_file = module.params["ocm_response_file"] + offline = module.params["offline"] + stop_processes = module.params["stop_processes"] + output = module.params["output"] + state = module.params["state"] + hostname = module.params["hostname"] + port = module.params["port"] + + + if not os.path.exists(oracle_home): + msg = 'oracle_home: %s doesn\'t exist' % (oracle_home) + module.fail_json(msg=msg, changed=False) + + if not os.path.exists('%s/OPatch/opatch' % (oracle_home)): + msg = 'OPatch doesn\'t seem to exist in %s/OPatch/' % (oracle_home) + module.fail_json(msg=msg, changed=False) + + if (patch_base or patch_id) is None and state in ('present','absent'): + msg = 'patch_base & patch_id needs to be set' + module.fail_json(msg=msg, changed=False) + + if opatchauto: + if patch_version is None: + msg = 'patch_version (e.g 12.1.0.2.1801417) needs to be set if opatchauto is True' + module.fail_json(msg=msg, changed=False) + + if oracle_home is not None: + os.environ['ORACLE_HOME'] = oracle_home + #os.environ['LD_LIBRARY_PATH'] = ld_library_path + elif 'ORACLE_HOME' in os.environ: + oracle_home = os.environ['ORACLE_HOME'] + #ld_library_path = os.environ['LD_LIBRARY_PATH'] + else: + msg = 'ORACLE_HOME variable not set. Please set it and re-run the command' + module.fail_json(msg=msg, changed=False) + + + # Get the Oracle % Opatch version + major_version = get_version(module,msg,oracle_home) + opatch_version = get_opatch_version(module,msg,oracle_home) + opatch_version_noocm = '12.2.0.1.5' + + if opatch_minversion is not None: + opatch_minversion_ = opatch_minversion.replace('.','') + if LooseVersion(opatch_version) < LooseVersion(opatch_minversion): + msg = 'Current OPatch version: %s, minimum version needed is: %s' % (opatch_version,opatch_minversion) + module.fail_json(msg=msg, changed=False) + + if state == 'present' and ocm_response_file is None and LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm): + msg='An OCM response file is needed when the opatch version is < %s. Current opatch version: %s' % (opatch_version_noocm,opatch_version) + module.fail_json(msg=msg,changed=False) + + if state == 'opatchversion': + module.exit_json(msg=opatch_version,changed=False) + + if state == 'present': + if not check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, None): + if apply_patch(module, msg, oracle_home, patch_base, patch_id,patch_version, opatchauto,ocm_response_file,offline,stop_processes,rolling,output): + if patch_version is not None: + msg = 'Patch %s (%s) successfully applied to %s' % (patch_id,patch_version, oracle_home) + else: + msg = 'Patch %s successfully applied to %s' % (patch_id, oracle_home) + module.exit_json(msg=msg, changed=True) + + else: + if patch_version is not None: + msg = 'Patch %s (%s) is already applied to %s' % (patch_id,patch_version, oracle_home) + else: + msg = 'Patch %s is already applied to %s' % (patch_id, oracle_home) + module.exit_json(msg=msg, changed=False) + + elif state == 'absent': + if check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): + if remove_patch(module, msg, oracle_home, patch_base, patch_id, opatchauto,ocm_response_file, stop_processes, output): + if patch_version is not None: + msg = 'Patch %s (%s) successfully removed from %s' % (patch_id,patch_version, oracle_home) + else: + msg = 'Patch %s successfully removed from %s' % (patch_id, oracle_home) + module.exit_json(msg=msg, changed=True) + else: + module.fail_json(msg=msg, changed=False) + else: + if patch_version is not None: + msg = 'Patch %s (%s) is not applied to %s' % (patch_id, patch_version, oracle_home) + elif exclude_upi is not None: + msg = 'Patch %s (UPI %s) has already been applied and will not be removed from %s' % (patch_id, exclude_upi, oracle_home) + else: + msg = 'Patch %s is not applied to %s' % (patch_id, oracle_home) + + module.exit_json(msg=msg, changed=False) + + + module.exit_json(msg="Unhandled exit", changed=False) + + + + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 43fda6b8f393f2e123bfe6353fc570eff83abbb3 Mon Sep 17 00:00:00 2001 From: Thorsten Bruhns Date: Fri, 29 May 2020 15:52:55 +0000 Subject: [PATCH 11/13] oracle_db: fixed ORA-00904: invalid identifier sql: select SUPPLEMENTAL_LOG_DATA_MIN --- oracle_db | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/oracle_db b/oracle_db index b3d5469..4843b3c 100644 --- a/oracle_db +++ b/oracle_db @@ -594,11 +594,15 @@ def ensure_db_state (module,msg,oracle_home,db_name,db_unique_name,sid, archivel change_restart_sql = [] change_db_sql = [] - supp_log_check_sql = 'select SUPPLEMENTAL_LOG_DATA_MIN,SUPPLEMENTAL_LOG_DATA_PL,SUPPLEMENTAL_LOG_DATA_SR,SUPPLEMENTAL_LOG_DATA_PK,SUPPLEMENTAL_LOG_DATA_UI from v$database' log_check_sql = 'select log_mode,force_logging, flashback_on from v$database' - supp_log_check_ = execute_sql_get(module,msg,cursor,supp_log_check_sql) log_check_ = execute_sql_get(module,msg,cursor,log_check_sql) + if major_version >= '19.0': + supp_log_check_sql = 'select SUPPLEMENTAL_LOG_DATA_MIN,SUPPLEMENTAL_LOG_DATA_PL,SUPPLEMENTAL_LOG_DATA_SR,SUPPLEMENTAL_LOG_DATA_PK,SUPPLEMENTAL_LOG_DATA_UI from v$database' + supp_log_check_ = execute_sql_get(module,msg,cursor,supp_log_check_sql) + if supp_log_check_[0][0] != slcomp: + change_db_sql.append(slsql) + if israc_[0][0] == 'NO': israc = False else: @@ -657,9 +661,6 @@ def ensure_db_state (module,msg,oracle_home,db_name,db_unique_name,sid, archivel if log_check_[0][2] != fbcomp: change_db_sql.append(fbsql) - if supp_log_check_[0][0] != slcomp: - change_db_sql.append(slsql) - if len(change_db_sql) > 0 or len(change_restart_sql) > 0: if log_check_[0][0] == 'ARCHIVELOG' and log_check_[0][2] == 'YES' and not archivelog and not flashback: # Flashback database needs to be turned off before archivelog is turned off From 64bd72a4ed0a6446d6f5e56a97c2793a4d993b8e Mon Sep 17 00:00:00 2001 From: Thorsten Bruhns Date: Sat, 6 Jun 2020 06:58:04 +0000 Subject: [PATCH 12/13] oracle_db: Bugfix for empty slsql --- oracle_db | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/oracle_db b/oracle_db index 4843b3c..f2652ba 100644 --- a/oracle_db +++ b/oracle_db @@ -601,6 +601,14 @@ def ensure_db_state (module,msg,oracle_home,db_name,db_unique_name,sid, archivel supp_log_check_sql = 'select SUPPLEMENTAL_LOG_DATA_MIN,SUPPLEMENTAL_LOG_DATA_PL,SUPPLEMENTAL_LOG_DATA_SR,SUPPLEMENTAL_LOG_DATA_PK,SUPPLEMENTAL_LOG_DATA_UI from v$database' supp_log_check_ = execute_sql_get(module,msg,cursor,supp_log_check_sql) if supp_log_check_[0][0] != slcomp: + + if supplemental_logging == True: + slcomp = 'YES' + slsql = alterdb_sql + ' add supplemental log data' + else: + slcomp = 'NO' + slsql = alterdb_sql + ' drop supplemental log data' + change_db_sql.append(slsql) if israc_[0][0] == 'NO': @@ -629,13 +637,6 @@ def ensure_db_state (module,msg,oracle_home,db_name,db_unique_name,sid, archivel fbcomp = 'NO' fbsql = alterdb_sql + ' flashback off' - if supplemental_logging == True: - slcomp = 'YES' - slsql = alterdb_sql + ' add supplemental log data' - else: - slcomp = 'NO' - slsql = alterdb_sql + ' drop supplemental log data' - if def_tbs_type[0] != default_tablespace_type: deftbstypesql = 'alter database set default %s tablespace ' % (default_tablespace_type) change_db_sql.append(deftbstypesql) From 716c31fbcf3b49940be7c7ec4f1e54660bb7770c Mon Sep 17 00:00:00 2001 From: Dietmar Uhlig Date: Fri, 17 Dec 2021 10:37:21 +0100 Subject: [PATCH 13/13] Minor fixes, oracle_parameter: changed default of scope to spfile --- oracle_datapatch | 3 + oracle_db | 33 +- oracle_opatch | 1201 +++++++++++++++++++++++----------------------- oracle_parameter | 41 +- oracle_pdb | 14 +- oracle_profile | 2 +- oracle_sql | 2 +- oracle_sqldba | 101 ++-- 8 files changed, 733 insertions(+), 664 deletions(-) diff --git a/oracle_datapatch b/oracle_datapatch index 7fea4b7..e831f32 100644 --- a/oracle_datapatch +++ b/oracle_datapatch @@ -120,6 +120,9 @@ def check_db_exists(module, msg, oracle_home, db_name, sid, db_unique_name ): return False elif 'Database name: %s' % (db_name) in stdout: #<-- Database already exist return True + else: + msg = '%s' % (stdout) # this assignment does nothing as msg is not global --> should be fixed later + return True else: existingdbs = [] oratabfile = '/etc/oratab' diff --git a/oracle_db b/oracle_db index f2652ba..e7af903 100644 --- a/oracle_db +++ b/oracle_db @@ -282,6 +282,8 @@ oracle_db: ''' import os, re, time +msg = [''] + try: import cx_Oracle except ImportError: @@ -301,7 +303,8 @@ def get_version(module, msg, oracle_home): # Check if the database exists -def check_db_exists(module, msg, oracle_home, db_name, sid, db_unique_name ): +def check_db_exists(module, oracle_home, db_name, sid, db_unique_name ): + global msg if sid is None: sid = '' @@ -377,7 +380,7 @@ def create_db (module, msg, oracle_home, sys_password, system_password, dbsnmp_p initparam += 'db_name=%s,db_unique_name=%s,' % (db_name,db_unique_name) if domain is not None: - initparam += 'db_domain=%s' % domain + initparam += 'db_domain=%s,' % domain if initparams is not None: paramslist = ",".join(initparams) @@ -391,8 +394,14 @@ def create_db (module, msg, oracle_home, sys_password, system_password, dbsnmp_p if system_password is not None: command += ' -systemPassword \"%s\"' % (system_password) else: - system_password = sys_password - command += ' -systemPassword \"%s\"' % (system_password) + pw_found = False + with open(responsefile) as rspfile: + for line in rspfile: + if re.match('systemPassword=.+', line): + pw_found = True + break + if not pw_found: + command += ' -systemPassword \"%s\"' % (sys_password) if dbsnmp_password is not None: command += ' -dbsnmpPassword \"%s\"' % (dbsnmp_password) else: @@ -897,8 +906,8 @@ def getconn(module,msg): def main(): - msg = [''] cursor = None + global msg global gimanaged global major_version global user @@ -1044,17 +1053,17 @@ def main(): if state == 'started': msg = "oracle_home: %s db_name: %s sid: %s db_unique_name: %s" % (oracle_home, db_name, sid, db_unique_name) - if not check_db_exists(module, msg, oracle_home,db_name, sid, db_unique_name): + if not check_db_exists(module, oracle_home,db_name, sid, db_unique_name): msg = "Database not found. %s" % msg module.fail_json(msg=msg, changed=False) else: start_db_state = start_db (module, msg, oracle_home, db_name, db_unique_name, sid) - if start_db_state == 'changed': + if start_db_state == 'changed': msg = "Database started." module.exit_json(msg=msg, changed=True) - if start_db_state == 'ok': - msg = "Database already running." - module.exit_json(msg=msg, changed=False) + elif start_db_state == 'ok': + msg = "Database already running." + module.exit_json(msg=msg, changed=False) else: msg = "Startup failed. %s" % msg module.fail_json(msg=msg, changed=False) @@ -1062,7 +1071,7 @@ def main(): elif state == 'present': - if not check_db_exists(module, msg, oracle_home,db_name, sid, db_unique_name): + if not check_db_exists(module, oracle_home,db_name, sid, db_unique_name): if create_db(module, msg, oracle_home, sys_password, system_password, dbsnmp_password, db_name, sid, db_unique_name, responsefile, template, cdb, local_undo, datafile_dest, recoveryfile_dest, storage_type, dbconfig_type, racone_service, characterset, memory_percentage, memory_totalmb, nodelist, db_type, amm, initparams, customscripts,datapatch, domain): newdb = True @@ -1075,7 +1084,7 @@ def main(): # module.exit_json(msg=msg, changed=False) elif state == 'absent': - if check_db_exists(module, msg, oracle_home, db_name, sid, db_unique_name): + if check_db_exists(module, oracle_home, db_name, sid, db_unique_name): if remove_db(module, msg, oracle_home, db_name, sid, db_unique_name, sys_password): msg = 'Successfully removed database %s' % (db_name) module.exit_json(msg=msg, changed=True) diff --git a/oracle_opatch b/oracle_opatch index 98f98d7..21713c7 100644 --- a/oracle_opatch +++ b/oracle_opatch @@ -1,600 +1,601 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -DOCUMENTATION = ''' ---- -module: oracle_opatch -short_description: Manage patches in an Oracle environment - - Manages patches (applies/rolls back) - - Only manages the opatch part of patching (opatch/opatch auto/opatchauto) - - If opatchauto is true, the task has to be run as root -version_added: "2.4.0.0" -options: - oracle_home: - description: - - The home which will be patched - required: True - aliases: ['oh'] - patch_base: - description: - - Path to where the patch is located - e.g /nfs/patches/12.1.0.2/27468957 - required: False - default: None - aliases: ['path','source','patch_source','phBaseDir'] - patch_id: - description: - - The patch id - e.g 27468957 - required: False - default: None - aliases: ['id'] - patch_version: - description: - - The patch version - e.g 12.2.0.1.180417 - - This key is mandatory if you're applying a 'opatchauto' type of patch - required: False - default: None - aliases: ['version_added'] - exclude_upi: - description: - - The unique patch identifier that should not be rolled back - e.g 22990084 - - This option will be honored only when state=absent. - - It helps to write idempotent playbooks when changing interim patches that are dependent on a specific PSU/RU. - required: False - default: None - aliases: ['exclude_unique_patch_id'] - opatch_minversion: - description: - - The minimum version of opatch needed - - If this key is set, a comparison is made between existing version and opatch_minversion - If existing < opatch_minversion an error is raised - required: False - default: None - aliases: ['opmv'] - opatchauto: - description: - - Should the patch be applied using opatchato - - If set to true, the task has to run as 'root' - required: False - default: False - aliases: ['autopatch'] - conflict_check: - description: - - Should a conflict check be run before applying a patch. - - If the check errors the module exits with a failure - required: False - default: True - stop_processes: - description: - - Stop Instances and Listener before applying a patch in ORACLE_HOME - required: False - default: False - rolling: - description: - - Should a patch installed in rolling upgrade mode? - required: False - default: True - ocm_response_file: - description: - - The OCM responsefile needed for OPatch versions < '12.2.0.1.5' (basically for DB/GI versions < 12.1) - required: False - default: False - state: - description: - - Should a patch be applied or removed - - present = applied, absent = removed, opatchversion = returns the version of opatch - default: present - choices: ['present','absent','opatchversion'] - hostname: - description: - - The host of the database if using dbms_service - required: false - default: localhost - aliases: ['host'] - port: - description: - - The listener port to connect to the database if using dbms_service - required: false - default: 1521 - -notes: - - -requirements: [ "os","pwd","distutils.version" ] -author: Mikael Sandström, oravirt@gmail.com, @oravirt -''' - -EXAMPLES = ''' - -''' -import os, pwd -from distutils.version import LooseVersion -# -# try: -# import cx_Oracle -# except ImportError: -# cx_oracle_exists = False -# else: -# cx_oracle_exists = True - - -def get_version(module, msg, oracle_home): - ''' - Returns the DB server version - ''' - - command = '%s/bin/sqlplus -V' % (oracle_home) - (rc, stdout, stderr) = module.run_command(command) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - else: - return stdout.split(' ')[2][0:4] - -def get_opatch_version(module, msg, oracle_home): - ''' - Returns the Opatch version - ''' - - command = '%s/OPatch/opatch version' % (oracle_home) - (rc, stdout, stderr) = module.run_command(command) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - else: - return stdout.split('\n')[0].split(':')[1].strip() - -def get_file_owner(module, msg, oracle_home): - ''' - This will only be run if opatchauto is True. - The owner of ORACLE_HOME has to be established, and we do this be checking file - ownership on ORACLE_HOME/bin/oracle. - returns the owner - ''' - - checkfile = '%s/bin/oracle' % (oracle_home) - if os.path.exists(checkfile): - stat_info = os.stat(checkfile) - uid = stat_info.st_uid - user = pwd.getpwuid(uid)[0] - return user - else: - msg = 'Could not determine owner of %s ' % (checkfile) - module.fail_json(msg=msg) - -def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): - ''' - Gets all patches already applied and compares to the - intended patch - ''' - - command = '' - if opatchauto: - oh_owner = get_file_owner(module,msg,oracle_home) - command += 'sudo -u %s ' % (oh_owner) - command += '%s/OPatch/opatch lspatches ' % (oracle_home) - (rc, stdout, stderr) = module.run_command(command) - #module.exit_json(msg=stdout, changed=False) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - else: - if opatchauto: - chk = '%s' % (patch_version) - elif not opatchauto and patch_id is not None and patch_version is not None: - chk = '%s (%s)' % (patch_version,patch_id) - else: - chk = '%s' % (patch_id) - - if chk in stdout: - if exclude_upi is None: - return True - else: - command += ' -id %s' % patch_id - (rc, stdout, stderr) = module.run_command(command) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - else: - chk = 'unique_patch_id:%s' % exclude_upi - return (chk not in stdout) - else: - return False - -def analyze_patch (module, msg, oracle_home, patch_base, opatchauto): - - checks = [] - - if opatchauto: - if major_version < '12.1': - oh_owner = get_file_owner(module,msg,oracle_home) - command = '' - command += 'sudo -u %s ' % (oh_owner) - opatch_cmd = 'opatch ' - conflcommand = '%s %s/OPatch/opatch prereq CheckConflictAgainstOHWithDetail -ph %s -oh %s' % (command,oracle_home, patch_base, oracle_home) - spacecommand = '%s %s/OPatch/opatch prereq CheckSystemSpace -ph %s -oh %s' % (command,oracle_home, patch_base, oracle_home) - checks.append(conflcommand) - checks.append(spacecommand) - - else: - opatch_cmd = 'opatchauto' - command = '%s/OPatch/%s apply %s -oh %s -analyze' % (oracle_home, opatch_cmd, patch_base, oracle_home) - checks.append(command) - else: - conflcommand = '%s/OPatch/opatch prereq CheckConflictAgainstOHWithDetail -ph %s -oh %s' % (oracle_home, patch_base, oracle_home) - spacecommand = '%s/OPatch/opatch prereq CheckSystemSpace -ph %s -oh %s' % (oracle_home, patch_base, oracle_home) - checks.append(conflcommand) - checks.append(spacecommand) - - for cmd in checks: - (rc, stdout, stderr) = module.run_command(cmd) - # module.exit_json(msg=stdout, changed=False) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, cmd) - module.fail_json(msg=msg, changed=False) - elif rc == 0 and 'failed' in stdout: # <- Conflicts exist - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,cmd) - module.fail_json(msg=msg, changed=False) - else: - return True - -def apply_patch (module, msg, oracle_home, patch_base, patch_id, patch_version, opatchauto, ocm_response_file, offline, stop_processes, rolling, output): - ''' - Applies the patch - ''' - - if conflict_check: - if not analyze_patch(module, msg, oracle_home, patch_base, opatchauto): - module.fail_json(msg='Prereq checks failed') - - if opatchauto: - opoptions = '' - if major_version < '12.1': - opatch_cmd = 'opatch auto' - if offline: - oh = ' -och' - else: - oh = ' -oh' - else: - oh = ' -oh' - opatch_cmd = 'opatchauto apply' - - if not rolling: - opoptions += ' -nonrolling ' - - command = '%s/OPatch/%s %s %s %s %s' % (oracle_home,opatch_cmd, opoptions, patch_base,oh,oracle_home) - - else: - if stop_processes: - stop_process(module, oracle_home) - - opatch_cmd = 'opatch' - command = '%s/OPatch/%s apply %s -oh %s -silent' % (oracle_home,opatch_cmd, patch_base,oracle_home) - - if ocm_response_file is not None and (LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm)): - command += ' -ocmrf %s' % (ocm_response_file) - - (rc, stdout, stderr) = module.run_command(command) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - elif rc == 0 and 'Opatch version check failed' in stdout: # OPatch version check failed - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) - module.fail_json(msg=msg, changed=False) - else: - checks = ['successfully applied' in stdout, - 'patch applied successfully' in stdout, - 'apply successful' in stdout] - if any(checks): - if output == 'short': - return True - else: - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) - module.exit_json(msg=msg, changed=True) - else: - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) - module.exit_json(msg=msg, changed=False) - -def stop_process(module, oracle_home): - ''' - Stop processes in ORACLE_HOME for non GI/Restart Environments - ''' - - oratabfile = '/etc/oratab' - - if os.path.exists(oratabfile): - with open(oratabfile) as oratab: - - msg = '' - - for line in oratab: - if line.startswith('#') or line.startswith(' '): - continue - elif len(line.split(':')) >= 2 and line.split(':')[1] == oracle_home: - - # Find listener for ORACLE_HOME - p = subprocess.Popen('ps -o cmd -C tnslsnr | grep "^%s/bin/tnslsnr "' % (line) , shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, error = p.communicate() - p_status = p.wait() - - if output: - for lline in output.split('\n'): - - proclen = len("%s/bin/tnslsnr " % (oracle_home)) - - # remove executable from ps output, split by ' ' - # => 1st element is listener_name - # ps example: /.../bin/tnslsnr LISTENER -inherit - listener_name = lline[proclen:].split(' ')[0] - if len(listener_name) > 0: - lsnrctl_bin = '%s/bin/lsnrctl' % (oracle_home) - try: - p = subprocess.check_call([lsnrctl_bin, 'stop', '%s' % listener_name]) - except subprocess.CalledProcessError: - msg += 'Stop of Listener %s failed ' % listener_name - - # Stop instances in ORACLE_HOME - # The [0-9] is used to remove the grep itself from the result! - p = subprocess.Popen('ps -elf| grep "[0-9] ora_pmon_%s"' % (line.split(':')[0]) , shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, error = p.communicate() - p_status = p.wait() - - if output and p.returncode == 0: - os.environ['ORACLE_SID'] = line.split(':')[0] - shutdown_sql = '''connect / as sysdba - shutdown immediate; - exit''' - sqlplus_bin = '%s/bin/sqlplus' % (oracle_home) - p = subprocess.Popen([sqlplus_bin,'/nolog'],stdin=subprocess.PIPE, - stdout=subprocess.PIPE,stderr=subprocess.PIPE) - (stdout,stderr) = p.communicate(shutdown_sql.encode('utf-8')) - p_status = p.wait() - #module.fail_json(msg=stdout, changed=False) - - rc = p.returncode - if rc != 0: - msg += 'Stop of Instance %s failed' % line.split(':')[0] - - if msg: - module.fail_json(msg=msg, changed=False) - -def remove_patch (module, msg, oracle_home, patch_base, patch_id, opatchauto, ocm_response_file, stop_processes, output): - ''' - Removes the patch - ''' - - if opatchauto: - if major_version < '12.1': - opatch_cmd = 'opatch auto -rollback' - else: - opatch_cmd = 'opatchauto rollback' - - command = '%s/OPatch/%s %s -oh %s ' % (oracle_home,opatch_cmd, patch_base, oracle_home) - else: - if stop_processes: - stop_process(module, oracle_home) - - opatch_cmd = 'opatch rollback' - command = '%s/OPatch/%s -id %s -silent' % (oracle_home,opatch_cmd, patch_id) - - - if ocm_response_file is not None and (LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm)): - command += ' -ocmrf %s' % (ocm_response_file) - - #module.exit_json(msg=command, changed=False) - (rc, stdout, stderr) = module.run_command(command) - if rc != 0: - msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) - module.fail_json(msg=msg, changed=False) - else: - checks = ['RollbackSession removing interim patch' in stdout, - 'rolled back successfully' in stdout, - 'rollback successful' in stdout] - if any(checks): - if output == 'short': - return True - else: - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) - module.exit_json(msg=msg, changed=True) - else: - msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) - module.exit_json(msg=msg, changed=False) - - -# -# def execute_sql_get(module, msg, cursor, sql): -# -# try: -# cursor.execute(sql) -# result = (cursor.fetchall()) -# except cx_Oracle.DatabaseError as exc: -# error, = exc.args -# msg = 'Something went wrong while executing sql_get - %s sql: %s' % (error.message, sql) -# module.fail_json(msg=msg, changed=False) -# return False -# return result -# -# def execute_sql(module, msg, cursor, sql): -# -# try: -# cursor.execute(sql) -# except cx_Oracle.DatabaseError as exc: -# error, = exc.args -# msg = 'Something went wrong while executing sql - %s sql: %s' % (error.message, sql) -# module.fail_json(msg=msg, changed=False) -# return False -# return True -# -# def getconn(module,msg): -# -# hostname = os.uname()[1] -# wallet_connect = '/@%s' % service_name -# try: -# if (not user and not password ): # If neither user or password is supplied, the use of an oracle wallet is assumed -# connect = wallet_connect -# conn = cx_Oracle.connect(wallet_connect, mode=cx_Oracle.SYSDBA) -# elif (user and password ): -# dsn = cx_Oracle.makedsn(host=hostname, port=port, service_name=service_name, ) -# connect = dsn -# conn = cx_Oracle.connect(user, password, dsn, mode=cx_Oracle.SYSDBA) -# elif (not(user) or not(password)): -# module.fail_json(msg='Missing username or password for cx_Oracle') -# -# except cx_Oracle.DatabaseError as exc: -# error, = exc.args -# msg = 'Could not connect to database - %s, connect descriptor: %s' % (error.message, connect) -# module.fail_json(msg=msg, changed=False) -# -# cursor = conn.cursor() -# return cursor - - - -def main(): - - msg = [''] - cursor = None - global major_version - global opatch_version - global opatch_version_noocm - global hostname - global port - global conflict_check - - module = AnsibleModule( - argument_spec = dict( - oracle_home = dict(required=True, aliases = ['oh']), - patch_base = dict(default=None, aliases = ['path','source','patch_source','phBaseDir']), - patch_id = dict(default=None, aliases = ['id']), - patch_version = dict(required=None, aliases = ['version']), - exclude_upi = dict(required=None, aliases = ['exclude_unique_patch_id']), - opatch_minversion = dict(default=None, aliases = ['opmv']), - opatchauto = dict(default='False', type='bool',aliases = ['autopatch']), - rolling = dict(default='True', type='bool',aliases = ['rolling']), - conflict_check = dict(default='True', type='bool'), - ocm_response_file = dict(required=None,aliases = ['ocmrf']), - offline = dict(default='False', type='bool'), -# stop_processes = dict(default='True', type='bool'), - stop_processes = dict(default='False', type='bool'), - output = dict(default="short", choices = ["short","verbose"]), - state = dict(default="present", choices = ["present", "absent", "opatchversion"]), - hostname = dict(required=False, default = 'localhost', aliases = ['host']), - port = dict(required=False, type='int', default = 1521), - - - - ), - - ) - - oracle_home = module.params["oracle_home"] - patch_base = module.params["patch_base"] - patch_id = module.params["patch_id"] - patch_version = module.params["patch_version"] - exclude_upi = module.params["exclude_upi"] - opatch_minversion = module.params["opatch_minversion"] - opatchauto = module.params["opatchauto"] - rolling = module.params["rolling"] - conflict_check = module.params["conflict_check"] - ocm_response_file = module.params["ocm_response_file"] - offline = module.params["offline"] - stop_processes = module.params["stop_processes"] - output = module.params["output"] - state = module.params["state"] - hostname = module.params["hostname"] - port = module.params["port"] - - - if not os.path.exists(oracle_home): - msg = 'oracle_home: %s doesn\'t exist' % (oracle_home) - module.fail_json(msg=msg, changed=False) - - if not os.path.exists('%s/OPatch/opatch' % (oracle_home)): - msg = 'OPatch doesn\'t seem to exist in %s/OPatch/' % (oracle_home) - module.fail_json(msg=msg, changed=False) - - if (patch_base or patch_id) is None and state in ('present','absent'): - msg = 'patch_base & patch_id needs to be set' - module.fail_json(msg=msg, changed=False) - - if opatchauto: - if patch_version is None: - msg = 'patch_version (e.g 12.1.0.2.1801417) needs to be set if opatchauto is True' - module.fail_json(msg=msg, changed=False) - - if oracle_home is not None: - os.environ['ORACLE_HOME'] = oracle_home - #os.environ['LD_LIBRARY_PATH'] = ld_library_path - elif 'ORACLE_HOME' in os.environ: - oracle_home = os.environ['ORACLE_HOME'] - #ld_library_path = os.environ['LD_LIBRARY_PATH'] - else: - msg = 'ORACLE_HOME variable not set. Please set it and re-run the command' - module.fail_json(msg=msg, changed=False) - - - # Get the Oracle % Opatch version - major_version = get_version(module,msg,oracle_home) - opatch_version = get_opatch_version(module,msg,oracle_home) - opatch_version_noocm = '12.2.0.1.5' - - if opatch_minversion is not None: - opatch_minversion_ = opatch_minversion.replace('.','') - if LooseVersion(opatch_version) < LooseVersion(opatch_minversion): - msg = 'Current OPatch version: %s, minimum version needed is: %s' % (opatch_version,opatch_minversion) - module.fail_json(msg=msg, changed=False) - - if state == 'present' and ocm_response_file is None and LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm): - msg='An OCM response file is needed when the opatch version is < %s. Current opatch version: %s' % (opatch_version_noocm,opatch_version) - module.fail_json(msg=msg,changed=False) - - if state == 'opatchversion': - module.exit_json(msg=opatch_version,changed=False) - - if state == 'present': - if not check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, None): - if apply_patch(module, msg, oracle_home, patch_base, patch_id,patch_version, opatchauto,ocm_response_file,offline,stop_processes,rolling,output): - if patch_version is not None: - msg = 'Patch %s (%s) successfully applied to %s' % (patch_id,patch_version, oracle_home) - else: - msg = 'Patch %s successfully applied to %s' % (patch_id, oracle_home) - module.exit_json(msg=msg, changed=True) - - else: - if patch_version is not None: - msg = 'Patch %s (%s) is already applied to %s' % (patch_id,patch_version, oracle_home) - else: - msg = 'Patch %s is already applied to %s' % (patch_id, oracle_home) - module.exit_json(msg=msg, changed=False) - - elif state == 'absent': - if check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): - if remove_patch(module, msg, oracle_home, patch_base, patch_id, opatchauto,ocm_response_file, stop_processes, output): - if patch_version is not None: - msg = 'Patch %s (%s) successfully removed from %s' % (patch_id,patch_version, oracle_home) - else: - msg = 'Patch %s successfully removed from %s' % (patch_id, oracle_home) - module.exit_json(msg=msg, changed=True) - else: - module.fail_json(msg=msg, changed=False) - else: - if patch_version is not None: - msg = 'Patch %s (%s) is not applied to %s' % (patch_id, patch_version, oracle_home) - elif exclude_upi is not None: - msg = 'Patch %s (UPI %s) has already been applied and will not be removed from %s' % (patch_id, exclude_upi, oracle_home) - else: - msg = 'Patch %s is not applied to %s' % (patch_id, oracle_home) - - module.exit_json(msg=msg, changed=False) - - - module.exit_json(msg="Unhandled exit", changed=False) - - - - -from ansible.module_utils.basic import * -if __name__ == '__main__': - main() +#!/usr/bin/python +# -*- coding: utf-8 -*- + +DOCUMENTATION = ''' +--- +module: oracle_opatch +short_description: Manage patches in an Oracle environment + - Manages patches (applies/rolls back) + - Only manages the opatch part of patching (opatch/opatch auto/opatchauto) + - If opatchauto is true, the task has to be run as root +version_added: "2.4.0.0" +options: + oracle_home: + description: + - The home which will be patched + required: True + aliases: ['oh'] + patch_base: + description: + - Path to where the patch is located + e.g /nfs/patches/12.1.0.2/27468957 + required: False + default: None + aliases: ['path','source','patch_source','phBaseDir'] + patch_id: + description: + - The patch id + e.g 27468957 + required: False + default: None + aliases: ['id'] + patch_version: + description: + - The patch version + e.g 12.2.0.1.180417 + - This key is mandatory if you're applying a 'opatchauto' type of patch + required: False + default: None + aliases: ['version_added'] + exclude_upi: + description: + - The unique patch identifier that should not be rolled back + e.g 22990084 + - This option will be honored only when state=absent. + - It helps to write idempotent playbooks when changing interim patches that are dependent on a specific PSU/RU. + required: False + default: None + aliases: ['exclude_unique_patch_id'] + opatch_minversion: + description: + - The minimum version of opatch needed + - If this key is set, a comparison is made between existing version and opatch_minversion + If existing < opatch_minversion an error is raised + required: False + default: None + aliases: ['opmv'] + opatchauto: + description: + - Should the patch be applied using opatchato + - If set to true, the task has to run as 'root' + required: False + default: False + aliases: ['autopatch'] + conflict_check: + description: + - Should a conflict check be run before applying a patch. + - If the check errors the module exits with a failure + required: False + default: True + stop_processes: + description: + - Stop Instances and Listener before applying a patch in ORACLE_HOME + required: False + default: False + rolling: + description: + - Should a patch installed in rolling upgrade mode? + required: False + default: True + ocm_response_file: + description: + - The OCM responsefile needed for OPatch versions < '12.2.0.1.5' (basically for DB/GI versions < 12.1) + required: False + default: False + state: + description: + - Should a patch be applied or removed + - present = applied, absent = removed, opatchversion = returns the version of opatch + default: present + choices: ['present','absent','opatchversion'] + hostname: + description: + - The host of the database if using dbms_service + required: false + default: localhost + aliases: ['host'] + port: + description: + - The listener port to connect to the database if using dbms_service + required: false + default: 1521 + +notes: + - +requirements: [ "os","pwd","re","distutils.version" ] +author: Mikael Sandström, oravirt@gmail.com, @oravirt +''' + +EXAMPLES = ''' + +''' +import os, pwd, re +from distutils.version import LooseVersion +# +# try: +# import cx_Oracle +# except ImportError: +# cx_oracle_exists = False +# else: +# cx_oracle_exists = True + + +def get_version(module, msg, oracle_home): + ''' + Returns the DB server version + ''' + + command = '%s/bin/sqlplus -V' % (oracle_home) + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + return stdout.split(' ')[2][0:4] + +def get_opatch_version(module, msg, oracle_home): + ''' + Returns the Opatch version + ''' + + command = '%s/OPatch/opatch version' % (oracle_home) + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + return stdout.split('\n')[0].split(':')[1].strip() + +def get_file_owner(module, msg, oracle_home): + ''' + This will only be run if opatchauto is True. + The owner of ORACLE_HOME has to be established, and we do this be checking file + ownership on ORACLE_HOME/bin/oracle. + returns the owner + ''' + + checkfile = '%s/bin/oracle' % (oracle_home) + if os.path.exists(checkfile): + stat_info = os.stat(checkfile) + uid = stat_info.st_uid + user = pwd.getpwuid(uid)[0] + return user + else: + msg = 'Could not determine owner of %s ' % (checkfile) + module.fail_json(msg=msg) + +def check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): + ''' + Gets all patches already applied and compares to the + intended patch + ''' + + command = '' + if opatchauto: + oh_owner = get_file_owner(module,msg,oracle_home) + command += 'sudo -u %s ' % (oh_owner) + command += '%s/OPatch/opatch lspatches ' % (oracle_home) + (rc, stdout, stderr) = module.run_command(command) + #module.exit_json(msg=stdout, changed=False) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + if opatchauto: + chk = '%s' % (patch_version) + elif not opatchauto and patch_id is not None and patch_version is not None: + chk = r'%s \(%s\)' % (patch_version,patch_id) + else: + chk = '^%s;' % (patch_id) + + if re.search(chk, stdout, re.MULTILINE): + if exclude_upi is None: + return True + else: + command += ' -id %s' % patch_id + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + chk = 'unique_patch_id:%s' % exclude_upi + return (chk not in stdout) + else: + return False + +def analyze_patch (module, msg, oracle_home, patch_base, opatchauto): + + checks = [] + + if opatchauto: + if major_version < '12.1': + oh_owner = get_file_owner(module,msg,oracle_home) + command = '' + command += 'sudo -u %s ' % (oh_owner) + opatch_cmd = 'opatch ' + conflcommand = '%s %s/OPatch/opatch prereq CheckConflictAgainstOHWithDetail -ph %s -oh %s' % (command,oracle_home, patch_base, oracle_home) + spacecommand = '%s %s/OPatch/opatch prereq CheckSystemSpace -ph %s -oh %s' % (command,oracle_home, patch_base, oracle_home) + checks.append(conflcommand) + checks.append(spacecommand) + + else: + opatch_cmd = 'opatchauto' + command = '%s/OPatch/%s apply %s -oh %s -analyze' % (oracle_home, opatch_cmd, patch_base, oracle_home) + checks.append(command) + else: + conflcommand = '%s/OPatch/opatch prereq CheckConflictAgainstOHWithDetail -ph %s -oh %s' % (oracle_home, patch_base, oracle_home) + spacecommand = '%s/OPatch/opatch prereq CheckSystemSpace -ph %s -oh %s' % (oracle_home, patch_base, oracle_home) + checks.append(conflcommand) + checks.append(spacecommand) + + for cmd in checks: + (rc, stdout, stderr) = module.run_command(cmd) + # module.exit_json(msg=stdout, changed=False) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, cmd) + module.fail_json(msg=msg, changed=False) + elif rc == 0 and 'failed' in stdout: # <- Conflicts exist + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,cmd) + module.fail_json(msg=msg, changed=False) + else: + return True + +def apply_patch (module, msg, oracle_home, patch_base, patch_id, patch_version, opatchauto, ocm_response_file, offline, stop_processes, rolling, output): + ''' + Applies the patch + ''' + + if conflict_check: + if not analyze_patch(module, msg, oracle_home, patch_base, opatchauto): + module.fail_json(msg='Prereq checks failed') + + if opatchauto: + opoptions = '' + if major_version < '12.1': + opatch_cmd = 'opatch auto' + if offline: + oh = ' -och' + else: + oh = ' -oh' + else: + oh = ' -oh' + opatch_cmd = 'opatchauto apply' + + if not rolling: + opoptions += ' -nonrolling ' + + command = '%s/OPatch/%s %s %s %s %s' % (oracle_home,opatch_cmd, opoptions, patch_base,oh,oracle_home) + + else: + if stop_processes: + stop_process(module, oracle_home) + + opatch_cmd = 'opatch' + command = '%s/OPatch/%s apply %s -oh %s -silent' % (oracle_home,opatch_cmd, patch_base,oracle_home) + + if ocm_response_file is not None and (LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm)): + command += ' -ocmrf %s' % (ocm_response_file) + + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + elif rc == 0 and 'Opatch version check failed' in stdout: # OPatch version check failed + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) + module.fail_json(msg=msg, changed=False) + else: + checks = ['successfully applied' in stdout, + 'patch applied successfully' in stdout, + 'apply successful' in stdout] + if any(checks): + if output == 'short': + return True + else: + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) + module.exit_json(msg=msg, changed=True) + else: + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) + module.exit_json(msg=msg, changed=False) + +def stop_process(module, oracle_home): + ''' + Stop processes in ORACLE_HOME for non GI/Restart Environments + ''' + + oratabfile = '/etc/oratab' + + if os.path.exists(oratabfile): + with open(oratabfile) as oratab: + + msg = '' + + for line in oratab: + if line.startswith('#') or line.startswith(' '): + continue + elif len(line.split(':')) >= 2 and line.split(':')[1] == oracle_home: + + # Find listener for ORACLE_HOME + p = subprocess.Popen('ps -o cmd -C tnslsnr | grep "^%s/bin/tnslsnr "' % (line.split(':')[1]) , shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, error = p.communicate() + p_status = p.wait() + + if output: + for lline in output.decode('utf-8').split('\n'): + + proclen = len("%s/bin/tnslsnr " % (oracle_home)) + + # remove executable from ps output, split by ' ' + # => 1st element is listener_name + # ps example: /.../bin/tnslsnr LISTENER -inherit + listener_name = lline[proclen:].split(' ')[0] + if len(listener_name) > 0: + lsnrctl_bin = '%s/bin/lsnrctl' % (oracle_home) + try: + p = subprocess.check_call([lsnrctl_bin, 'stop', '%s' % listener_name]) + except subprocess.CalledProcessError: + msg += 'Stop of Listener %s failed ' % listener_name + + # Stop instances in ORACLE_HOME + # The [0-9] is used to remove the grep itself from the result! + p = subprocess.Popen('ps -elf| grep "[0-9] ora_pmon_%s"' % (line.split(':')[0]) , shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, error = p.communicate() + p_status = p.wait() + + if output and p.returncode == 0: + os.environ['ORACLE_SID'] = line.split(':')[0] + shutdown_sql = '''connect / as sysdba + shutdown immediate; + exit''' + sqlplus_bin = '%s/bin/sqlplus' % (oracle_home) + p = subprocess.Popen([sqlplus_bin,'/nolog'],stdin=subprocess.PIPE, + stdout=subprocess.PIPE,stderr=subprocess.PIPE) + (stdout,stderr) = p.communicate(shutdown_sql.encode('utf-8')) + p_status = p.wait() + #module.fail_json(msg=stdout, changed=False) + + rc = p.returncode + if rc != 0: + msg += 'Stop of Instance %s failed' % line.split(':')[0] + + if msg: + module.fail_json(msg=msg, changed=False) + +def remove_patch (module, msg, oracle_home, patch_base, patch_id, opatchauto, ocm_response_file, stop_processes, output): + ''' + Removes the patch + ''' + + if opatchauto: + if major_version < '12.1': + opatch_cmd = 'opatch auto -rollback' + else: + opatch_cmd = 'opatchauto rollback' + + command = '%s/OPatch/%s %s -oh %s ' % (oracle_home,opatch_cmd, patch_base, oracle_home) + else: + if stop_processes: + stop_process(module, oracle_home) + + opatch_cmd = 'opatch rollback' + command = '%s/OPatch/%s -id %s -silent' % (oracle_home,opatch_cmd, patch_id) + + + if ocm_response_file is not None and (LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm)): + command += ' -ocmrf %s' % (ocm_response_file) + + #module.exit_json(msg=command, changed=False) + (rc, stdout, stderr) = module.run_command(command) + if rc != 0: + msg = 'Error - STDOUT: %s, STDERR: %s, COMMAND: %s' % (stdout, stderr, command) + module.fail_json(msg=msg, changed=False) + else: + checks = ['RollbackSession removing interim patch' in stdout, + 'rolled back successfully' in stdout, + 'rollback successful' in stdout] + if any(checks): + if output == 'short': + return True + else: + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) + module.exit_json(msg=msg, changed=True) + else: + msg = 'STDOUT: %s, COMMAND: %s' % (stdout,command) + module.exit_json(msg=msg, changed=False) + + +# +# def execute_sql_get(module, msg, cursor, sql): +# +# try: +# cursor.execute(sql) +# result = (cursor.fetchall()) +# except cx_Oracle.DatabaseError as exc: +# error, = exc.args +# msg = 'Something went wrong while executing sql_get - %s sql: %s' % (error.message, sql) +# module.fail_json(msg=msg, changed=False) +# return False +# return result +# +# def execute_sql(module, msg, cursor, sql): +# +# try: +# cursor.execute(sql) +# except cx_Oracle.DatabaseError as exc: +# error, = exc.args +# msg = 'Something went wrong while executing sql - %s sql: %s' % (error.message, sql) +# module.fail_json(msg=msg, changed=False) +# return False +# return True +# +# def getconn(module,msg): +# +# hostname = os.uname()[1] +# wallet_connect = '/@%s' % service_name +# try: +# if (not user and not password ): # If neither user or password is supplied, the use of an oracle wallet is assumed +# connect = wallet_connect +# conn = cx_Oracle.connect(wallet_connect, mode=cx_Oracle.SYSDBA) +# elif (user and password ): +# dsn = cx_Oracle.makedsn(host=hostname, port=port, service_name=service_name, ) +# connect = dsn +# conn = cx_Oracle.connect(user, password, dsn, mode=cx_Oracle.SYSDBA) +# elif (not(user) or not(password)): +# module.fail_json(msg='Missing username or password for cx_Oracle') +# +# except cx_Oracle.DatabaseError as exc: +# error, = exc.args +# msg = 'Could not connect to database - %s, connect descriptor: %s' % (error.message, connect) +# module.fail_json(msg=msg, changed=False) +# +# cursor = conn.cursor() +# return cursor + + + +def main(): + + msg = [''] + cursor = None + global major_version + global opatch_version + global opatch_version_noocm + global hostname + global port + global conflict_check + + module = AnsibleModule( + argument_spec = dict( + oracle_home = dict(required=True, aliases = ['oh']), + patch_base = dict(default=None, aliases = ['path','source','patch_source','phBaseDir']), + patch_id = dict(default=None, aliases = ['id']), + patch_version = dict(required=None, aliases = ['version']), + exclude_upi = dict(required=None, aliases = ['exclude_unique_patch_id']), + opatch_minversion = dict(default=None, aliases = ['opmv']), + opatchauto = dict(default='False', type='bool',aliases = ['autopatch']), + rolling = dict(default='True', type='bool',aliases = ['rolling']), + conflict_check = dict(default='True', type='bool'), + ocm_response_file = dict(required=None,aliases = ['ocmrf']), + offline = dict(default='False', type='bool'), +# stop_processes = dict(default='True', type='bool'), + stop_processes = dict(default='False', type='bool'), + output = dict(default="short", choices = ["short","verbose"]), + state = dict(default="present", choices = ["present", "absent", "opatchversion"]), + hostname = dict(required=False, default = 'localhost', aliases = ['host']), + port = dict(required=False, type='int', default = 1521), + + + + ), + + ) + + oracle_home = module.params["oracle_home"] + patch_base = module.params["patch_base"] + patch_id = module.params["patch_id"] + patch_version = module.params["patch_version"] + exclude_upi = module.params["exclude_upi"] + opatch_minversion = module.params["opatch_minversion"] + opatchauto = module.params["opatchauto"] + rolling = module.params["rolling"] + conflict_check = module.params["conflict_check"] + ocm_response_file = module.params["ocm_response_file"] + offline = module.params["offline"] + stop_processes = module.params["stop_processes"] + output = module.params["output"] + state = module.params["state"] + hostname = module.params["hostname"] + port = module.params["port"] + + + if not os.path.exists(oracle_home): + msg = 'oracle_home: %s doesn\'t exist' % (oracle_home) + module.fail_json(msg=msg, changed=False) + + if not os.path.exists('%s/OPatch/opatch' % (oracle_home)): + msg = 'OPatch doesn\'t seem to exist in %s/OPatch/' % (oracle_home) + module.fail_json(msg=msg, changed=False) + + if (patch_base or patch_id) is None and state in ('present','absent'): + msg = 'patch_base & patch_id needs to be set' + module.fail_json(msg=msg, changed=False) + + if opatchauto: + if patch_version is None: + msg = 'patch_version (e.g 12.1.0.2.1801417) needs to be set if opatchauto is True' + module.fail_json(msg=msg, changed=False) + + if oracle_home is not None: + os.environ['ORACLE_HOME'] = oracle_home + #os.environ['LD_LIBRARY_PATH'] = ld_library_path + elif 'ORACLE_HOME' in os.environ: + oracle_home = os.environ['ORACLE_HOME'] + #ld_library_path = os.environ['LD_LIBRARY_PATH'] + else: + msg = 'ORACLE_HOME variable not set. Please set it and re-run the command' + module.fail_json(msg=msg, changed=False) + + + # Get the Oracle % Opatch version + opatch_version = get_opatch_version(module,msg,oracle_home) + + if state == 'opatchversion': + module.exit_json(msg=opatch_version,changed=False) + + major_version = get_version(module,msg,oracle_home) + opatch_version_noocm = '12.2.0.1.5' + + if opatch_minversion is not None: + opatch_minversion_ = opatch_minversion.replace('.','') + if LooseVersion(opatch_version) < LooseVersion(opatch_minversion): + msg = 'Current OPatch version: %s, minimum version needed is: %s' % (opatch_version,opatch_minversion) + module.fail_json(msg=msg, changed=False) + + if state == 'present' and ocm_response_file is None and LooseVersion(opatch_version) < LooseVersion(opatch_version_noocm): + msg='An OCM response file is needed when the opatch version is < %s. Current opatch version: %s' % (opatch_version_noocm,opatch_version) + module.fail_json(msg=msg,changed=False) + + if state == 'present': + if not check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, None): + if apply_patch(module, msg, oracle_home, patch_base, patch_id,patch_version, opatchauto,ocm_response_file,offline,stop_processes,rolling,output): + if patch_version is not None: + msg = 'Patch %s (%s) successfully applied to %s' % (patch_id,patch_version, oracle_home) + else: + msg = 'Patch %s successfully applied to %s' % (patch_id, oracle_home) + module.exit_json(msg=msg, changed=True) + + else: + if patch_version is not None: + msg = 'Patch %s (%s) is already applied to %s' % (patch_id,patch_version, oracle_home) + else: + msg = 'Patch %s is already applied to %s' % (patch_id, oracle_home) + module.exit_json(msg=msg, changed=False) + + elif state == 'absent': + if check_patch_applied(module, msg, oracle_home, patch_id, patch_version, opatchauto, exclude_upi): + if remove_patch(module, msg, oracle_home, patch_base, patch_id, opatchauto,ocm_response_file, stop_processes, output): + if patch_version is not None: + msg = 'Patch %s (%s) successfully removed from %s' % (patch_id,patch_version, oracle_home) + else: + msg = 'Patch %s successfully removed from %s' % (patch_id, oracle_home) + module.exit_json(msg=msg, changed=True) + else: + module.fail_json(msg=msg, changed=False) + else: + if patch_version is not None: + msg = 'Patch %s (%s) is not applied to %s' % (patch_id, patch_version, oracle_home) + elif exclude_upi is not None: + msg = 'Patch %s (UPI %s) has already been applied and will not be removed from %s' % (patch_id, exclude_upi, oracle_home) + else: + msg = 'Patch %s is not applied to %s' % (patch_id, oracle_home) + + module.exit_json(msg=msg, changed=False) + + + module.exit_json(msg="Unhandled exit", changed=False) + + + + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() diff --git a/oracle_parameter b/oracle_parameter index b1380e9..1316578 100644 --- a/oracle_parameter +++ b/oracle_parameter @@ -80,9 +80,11 @@ else: cx_oracle_exists = True -# Check if the parameter exists -def check_parameter_exists(module, mode, msg, cursor, name): +msg = [''] +# Check if the parameter exists +def check_parameter_exists(module, mode, cursor, name): + global msg if not(name): module.fail_json(msg='Error: Missing parameter name', changed=False) @@ -106,7 +108,7 @@ def check_parameter_exists(module, mode, msg, cursor, name): msg = error.message+ 'sql: ' + sql return False - if result > 0: + if result: return True else: msg = 'The parameter (%s) doesn\'t exist' % name @@ -115,7 +117,8 @@ def check_parameter_exists(module, mode, msg, cursor, name): -def modify_parameter(module, mode, msg, cursor, name, value, comment, scope, sid): +def modify_parameter(module, mode, cursor, name, value, comment, scope, sid): + global msg contains = re.compile(r'[?*$%#()!\s,._/=+-]') starters = ('+','_','"','"_','/') @@ -126,7 +129,7 @@ def modify_parameter(module, mode, msg, cursor, name, value, comment, scope, sid - currval = get_curr_value(module, mode, msg, cursor, name, scope) + currval = get_curr_value(module, mode, cursor, name, scope) if currval == value.lower() or not currval and value == "''": module.exit_json(msg='The parameter (%s) is already set to %s' % (name, value), changed=False) @@ -179,7 +182,8 @@ def clean_string(item): return item -def reset_parameter(module, mode, msg, cursor, name, value, comment, scope, sid): +def reset_parameter(module, mode, cursor, name, value, comment, scope, sid): + global msg starters = ('+','_','"','"_') if not(name): @@ -212,20 +216,21 @@ def reset_parameter(module, mode, msg, cursor, name, value, comment, scope, sid) return True -def get_curr_value(module, mode, msg, cursor, name, scope): +def get_curr_value(module, mode, cursor, name, scope): + global msg name = clean_string(name) - if scope == 'spfile': - parameter_source = 'v$spparameter' - else: + if scope == 'memory': parameter_source = 'v$parameter' + else: + parameter_source = 'v$spparameter' if mode == 'sysdba': - if scope == 'spfile': - sql = 'select lower(KSPSPFFTCTXSPDVALUE) from x$kspspfile where lower(KSPSPFFTCTXSPNAME) = lower(\'%s\')' % (name) - else: + if scope == 'memory': sql = 'select lower(y.ksppstdvl) from sys.x$ksppi x, sys.x$ksppcv y where x.indx = y.indx and x.ksppinm = lower(\'%s\')' % (name) + else: + sql = 'select lower(KSPSPFFTCTXSPDVALUE) from x$kspspfile where lower(KSPSPFFTCTXSPNAME) = lower(\'%s\')' % (name) else: sql = 'select lower(display_value) from %s where name = lower(\'%s\')' % (parameter_source, name) @@ -242,8 +247,8 @@ def get_curr_value(module, mode, msg, cursor, name, scope): def main(): + global msg - msg = [''] module = AnsibleModule( argument_spec = dict( hostname = dict(default='localhost'), @@ -312,8 +317,8 @@ def main(): cursor = conn.cursor() if state == 'present': - if check_parameter_exists(module, mode, msg, cursor, name): - if modify_parameter(module, mode, msg, cursor, name, value, comment, scope, sid): + if check_parameter_exists(module, mode, cursor, name): + if modify_parameter(module, mode, cursor, name, value, comment, scope, sid): module.exit_json(msg=msg, changed=True) else: module.fail_json(msg=msg, changed=False) @@ -321,8 +326,8 @@ def main(): module.fail_json(msg=msg, changed=False) elif state == 'reset' or state == 'absent': - if check_parameter_exists(module, mode, msg, cursor, name): - if reset_parameter(module, mode, msg, cursor, name, value, comment, scope, sid): + if check_parameter_exists(module, mode, cursor, name): + if reset_parameter(module, mode, cursor, name, value, comment, scope, sid): module.exit_json(msg=msg, changed=True) else: module.fail_json(msg=msg, changed=False) diff --git a/oracle_pdb b/oracle_pdb index a0af2ef..d2ac775 100755 --- a/oracle_pdb +++ b/oracle_pdb @@ -54,6 +54,11 @@ options: required: false default: None aliases: ['plug_dest','upd','pd'] + as_clone: + description: + - Create PDB as clone? + required: false + default: false username: description: - The database username to connect to the database @@ -158,13 +163,16 @@ def quote_string(item): return value def create_pdb(cursor, module, msg, oracle_home, name, sourcedb, pdb_admin_username, - pdb_admin_password, datafile_dest, save_state, unplug_dest, file_name_convert): + pdb_admin_password, datafile_dest, save_state, unplug_dest, file_name_convert, as_clone): run_sql = [] opensql = 'alter pluggable database %s open instances=all' % (name) createsql = 'create pluggable database %s ' % (name) + if as_clone: + createsql += ' as clone ' + if unplug_dest is not None: createsql += ' using \'%s\' ' % (unplug_dest) createsql += ' tempfile reuse' @@ -349,6 +357,7 @@ def main(): pdb_admin_password = dict(required=False, no_log=True, default = 'pdb_admin', aliases = ['pdbadmpw']), datafile_dest = dict(required=False, aliases = ['dfd','create_file_dest']), unplug_dest = dict(required=False, aliases = ['plug_dest','upd','pd']), + as_clone = dict(required=False, default=False, type = 'bool'), file_name_convert = dict(required=False, aliases = ['fnc']), service_name_convert = dict(required=False, aliases = ['snc']), default_tablespace_type = dict(default='smallfile',choices = ['smallfile','bigfile']), @@ -376,6 +385,7 @@ def main(): pdb_admin_password = module.params["pdb_admin_password"] datafile_dest = module.params["datafile_dest"] unplug_dest = module.params["unplug_dest"] + as_clone = module.params["as_clone"] file_name_convert = module.params["file_name_convert"] service_name_convert = module.params["service_name_convert"] default_tablespace_type = module.params["default_tablespace_type"] @@ -440,7 +450,7 @@ def main(): if state in ('present','closed', 'open', 'restricted','read_only','read_write'): if not check_pdb_exists(cursor, module, msg, name): - if create_pdb(cursor, module, msg, oracle_home, name, sourcedb, pdb_admin_username, pdb_admin_password, datafile_dest, save_state, unplug_dest, file_name_convert): + if create_pdb(cursor, module, msg, oracle_home, name, sourcedb, pdb_admin_username, pdb_admin_password, datafile_dest, save_state, unplug_dest, file_name_convert, as_clone): ensure_pdb_state(cursor, module, msg, name, state, newpdb,default_tablespace,default_tablespace_type,default_temp_tablespace,timezone) module.exit_json(msg=msg, changed=True) else: diff --git a/oracle_profile b/oracle_profile index cd7180a..fa52ca0 100644 --- a/oracle_profile +++ b/oracle_profile @@ -181,7 +181,7 @@ def ensure_profile_state(cursor, module, msg, name, state, attribute_name, attri current_attributes = get_current_attributes (cursor, module, msg, name, attribute_names_) # Convert to dict and compare current with wanted - if cmp(dict(current_attributes),dict(wanted_attributes)) is not 0: + if dict(current_attributes) != dict(wanted_attributes): for i in wanted_attributes: total_sql.append("alter profile %s limit %s %s " % (name, i[0], i[1])) diff --git a/oracle_sql b/oracle_sql index 7b1a7d7..bf875fa 100755 --- a/oracle_sql +++ b/oracle_sql @@ -144,7 +144,7 @@ def main(): mode=dict(default="normal", choices=["sysasm", "sysdba", "normal"]), service_name=dict(required=False, aliases=['sn']), hostname=dict(required=False, default='localhost', aliases=['host']), - port=dict(required=False, default=1521), + port=dict(required=False, default='1521'), sql=dict(required=False), script=dict(required=False), diff --git a/oracle_sqldba b/oracle_sqldba index 70effd3..0571e33 100755 --- a/oracle_sqldba +++ b/oracle_sqldba @@ -60,11 +60,12 @@ options: description: - Shall the SQL be applied to CDB, PDBs, or both? values: - - default: if catcon_pl is filled then all_pdbs else cdb + - default: if catcon_pl is filled then all_pdbs_and_root else cdb - db: alias for cdb, allows for better readability for non-cdb - cdb: apply to root container or whole db - pdbs: apply to specified PDB's only (requires pdb_list) - - all_pdbs: apply to all PDB's except PDB$SEED + - all_pdbs: apply to all PDB's except PDB$SEED and CDB$ROOT + - all_pdbs_and_root: apply to all PDB's except PDB$SEED required: false default: cdb pdb_list: @@ -169,14 +170,22 @@ from copy import copy changed = False result = "" -err_msg = None +err_msg = "" oracle_home = "" pdb_list = "" sql_process = None +debug_trace = "" # Maximum runtime for sqlplus and catcon.pl in seconds. 0 means no timeout. timeout = 0 +def trc(msg): + global debug_trace + debug_trace += msg + if msg[-1] != "\n": + debug_trace += "\n" + return msg + # dictify is based on https://stackoverflow.com/questions/2148119/how-to-convert-an-xml-string-to-a-dictionary/10077069#10077069 def dictify(r,root=True): if root: @@ -202,19 +211,22 @@ def sqlplus(): def conn(username, password): if username == None: - return "conn / as sysdba\n" + return trc("conn / as sysdba\n") else: + trc("conn " + username + "/***") return "conn " + "/".join([username, password]) + "\n" def sql_input(sql, username, password, pdb): - sql_scr = "set heading off echo off feedback off termout on\n" - sql_scr += "set long 1000000 pagesize 0 linesize 1000 trimspool on\n" + trc("-- sql_input start --") + sql_scr = trc("set heading off echo off feedback off termout on\n") + sql_scr += trc("set long 1000000 pagesize 0 linesize 1000 trimspool on\n") sql_scr += conn(username, password) if pdb is not None: - sql_scr += "alter session set container = " + pdb + ";\n" - sql_scr += sql + "\n" - sql_scr += "exit;\n" + sql_scr += trc("alter session set container = " + pdb + ";\n") + sql_scr += trc(sql + "\n") + sql_scr += trc("exit;\n") + trc("-- sql_input end --") return sql_scr def kill_process(): @@ -226,18 +238,24 @@ def kill_process(): def run_sql_p(sql, username, password, scope, pdb_list): global changed, err_msg, sql_process + trc("-- run_sql_p start --") err_msg = "" result = "" if scope == 'pdbs': + trc("-- run_sql_p -- scope == 'pdbs' --") for pdb in pdb_list.split(): + trc("-- run_sql_p -- call run_sql --") result += run_sql(sql, username, password, pdb) else: + trc("-- run_sql_p -- scope != 'pdbs' --") result = run_sql(sql, username, password, None) + trc("-- run_sql_p end --") return result def run_sql(sql, username, password, pdb): global changed, err_msg, sql_process + trc("-- run_sql start --") t = None try: sql_cmd = sql_input(sql, username, password, pdb) @@ -245,54 +263,72 @@ def run_sql(sql, username, password, pdb): if timeout > 0: t = Timer(timeout, kill_process) t.start() - [sout, serr] = sql_process.communicate(input = sql_cmd) + [sout, serr] = sql_process.communicate(input = sql_cmd.encode('latin-1')) except Exception as e: + trc("-- run_sql -- catched exception --") err_msg += 'Could not call sqlplus. %s. called: %s.' % (to_native(e), " ".join(sqlplus())) return "[ERR]" finally: + trc("-- run_sql -- finally --") if timeout > 0 and t is not None: t.cancel() if sql_process.returncode != 0: - err_msg += "called: %s\nreturncode: %d\nresult: %s. stderr = %s." % (sql_cmd, sql_process.returncode, sout, serr) + trc("-- run_sql -- sql_process.returncode != 0 --") + err_msg += "called: %s\nreturncode: %d\nresult: %s. stderr = %s." % (sql_cmd, sql_process.returncode, sout.decode(), serr.decode()) return "[ERR]" sqlerr_pat = re.compile("^(ORA|TNS|SP2)-[0-9]+", re.MULTILINE) - sqlplus_err = sqlerr_pat.search(sout) + sqlplus_err = sqlerr_pat.search(sout.decode()) if sqlplus_err: + trc("-- run_sql -- sqlplus_err is not None --") err_msg += "[ERR] sqlplus: %s\nERR Code: %s.\n" % (sql_cmd, sqlplus_err.group()) - return "[ERR]\n%s\n" % sout.strip() + return "[ERR]\n%s\n" % sout.strip().decode() changed = True - return sout.strip() + trc("-- run_sql end --") + return sout.strip().decode() def check_creates_sql(sql, scope): global pdb_list + trc("-- check_creates_sql start --") if not sql.endswith(";"): sql += ";" if scope == 'cdb': + trc("-- check_creates_sql -- call run_sql --") res = run_sql(sql, None, None, None) # error handling see call of check_creates_sql + trc("-- check_creates_sql -- result: " + res + " --") + trc("-- check_creates_sql end --") return False if not res or res == "0" else True else: checked_pdb_list = "" for pdb in pdb_list.split(): + trc("-- check_creates_sql -- call run_sql, pdb = " + pdb + " --") res = run_sql(sql, None, None, pdb) # error handling see call of check_creates_sql + trc("-- check_creates_sql -- result: " + res + " --") if not res or res == "0": checked_pdb_list += " " + pdb pdb_list = checked_pdb_list.lstrip() + trc("-- check_creates_sql -- pdb_list: " + pdb_list + " --") + trc("-- check_creates_sql end --") return True if pdb_list == "" else False def is_container(): return run_sql("select cdb from gv$database;", None, None, None) == 'YES' -def get_all_pdbs(): +def get_all_pdbs(scope): global result, pdb_list + trc("-- get_all_pdbs start --") sql = "select listagg(pdb_name, ' ') within group (order by pdb_name) from dba_pdbs where status = 'NORMAL' and pdb_name <> 'PDB$SEED';" - pdb_list = 'CDB$ROOT ' + run_sql(sql, None, None, None) + pdb_list = run_sql(sql, None, None, None) + if scope == 'all_pdbs_and_root': + pdb_list = 'CDB$ROOT ' + pdb_list + trc("-- get_all_pdbs -- pdb_list: " + pdb_list + " --") + trc("-- get_all_pdbs end --") def run_catcon_pl(catcon_pl): @@ -326,14 +362,16 @@ def run_catcon_pl(catcon_pl): if timeout > 0: t.cancel() try: + with open(logdir + '/catcon0.log') as f: + result += f.read() shutil.rmtree(logdir) except OSError as exc: + result += sout.decode() if exc.errno != errno.ENOENT: raise if sql_process.returncode != 0: - err_msg += "called: %s\nreturncode: %d\nresult: %s\nstderr = %s." % (" ".join(catcon_cmd), sql_process.returncode, sout, serr) + err_msg += "called: %s\nreturncode: %d\nresult: %s\nstderr = %s." % (" ".join(catcon_cmd), sql_process.returncode, sout.decode(), serr.decode()) return - result += sout changed = True @@ -350,7 +388,7 @@ def main(): creates_sql = dict(required = False), username = dict(required = False), password = dict(required = False, no_log = True), - scope = dict(required = False, choices = ["default", "db", "cdb", "pdbs", "all_pdbs"], default = 'default'), + scope = dict(required = False, choices = ["default", "db", "cdb", "pdbs", "all_pdbs", "all_pdbs_and_root"], default = 'default'), pdb_list = dict(required = False), oracle_home = dict(required = True), oracle_db_name = dict(required = True), @@ -383,32 +421,35 @@ def main(): if scope == 'db': scope = 'cdb' if scope == 'default': - scope = "all_pdbs" if catcon_pl is not None else "cdb" + scope = "all_pdbs_and_root" if catcon_pl is not None else "cdb" if scope == 'pdbs' and (pdb_list is None or pdb_list.strip() == ""): - module.exit_json(msg = "scope = pdbs, but pdb_list is empty", changed = False) + module.exit_json(msg = "scope = pdbs, but pdb_list is empty", changed = False, trace = debug_trace) if scope == 'cdb' and catcon_pl is not None: scope = 'pdbs' pdb_list = 'CDB$ROOT' - if scope == 'all_pdbs' and (catcon_pl is None or creates_sql is not None): + if scope in ['all_pdbs', 'all_pdbs_and_root'] and (catcon_pl is None or creates_sql is not None): if is_container(): + get_all_pdbs(scope) scope = 'pdbs' - get_all_pdbs() else: scope = 'cdb' - + trc("scope: " + scope) + if workdir is not None: try: os.chdir(workdir) except Exception as e: - module.fail_json(msg = 'Could not chdir to %s: %s.' % (workdir, to_native(e)), changed = False) + module.fail_json(msg = 'Could not chdir to %s: %s.' % (workdir, to_native(e)), changed = False, trace = debug_trace) if creates_sql is not None: + trc("call check_creates_sql") already_done = check_creates_sql(creates_sql, scope) if err_msg: - module.fail_json(msg = "%s\n%s" % (result, err_msg), changed = False) + module.fail_json(msg = "%s\n%s" % (result, err_msg), changed = False, trace = debug_trace) else: + trc("check_creates_sql said: already_done = {}".format(already_done)) if already_done: - module.exit_json(msg = result, changed = False) + module.exit_json(msg = result, changed = False, trace = debug_trace) if pdb_list is not None: result = "Run on these PDBs: %s\n" % pdb_list @@ -433,11 +474,11 @@ def main(): if not err_msg: if sqlselect is not None: res_dict = dictify(ET.fromstring(result)) if result else {"ROW": []} - module.exit_json(msg = result, changed = False, state = res_dict) + module.exit_json(msg = result, changed = False, state = res_dict, trace = debug_trace) else: - module.exit_json(msg = result, changed = changed) + module.exit_json(msg = result, changed = changed, trace = debug_trace) else: - module.fail_json(msg = "%s\n%s" % (result, err_msg), changed = changed) + module.fail_json(msg = "%s\n%s\nENV:\n ORACLE_HOME = %s\n ORACLE_SID = %s\n" % (result, err_msg, oracle_home, oracle_db_name), changed = changed, trace = debug_trace) if __name__ == '__main__':