From 921b2ae9440be868b62a0351efab165bb88c1e9d Mon Sep 17 00:00:00 2001 From: Dietmar Uhlig Date: Thu, 16 Jan 2020 17:01:04 +0100 Subject: [PATCH 1/4] 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 2c2551a875dcd0c3c841e58efe320aa9178875bd Mon Sep 17 00:00:00 2001 From: Dietmar Uhlig Date: Fri, 17 Jan 2020 13:26:55 +0100 Subject: [PATCH 2/4] 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 46dee8de855d06df392b3b78b5a09d090cf6030b Mon Sep 17 00:00:00 2001 From: Dietmar Uhlig Date: Fri, 17 Jan 2020 16:55:18 +0100 Subject: [PATCH 3/4] 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 9048614dfb5cd4fde7e04e7e295f45911b2fe505 Mon Sep 17 00:00:00 2001 From: duhlig Date: Thu, 6 Aug 2020 13:16:37 +0200 Subject: [PATCH 4/4] oracle_sqldba: fixed several bugs, better handling of PDBs --- oracle_sqldba | 171 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 120 insertions(+), 51 deletions(-) mode change 100644 => 100755 oracle_sqldba diff --git a/oracle_sqldba b/oracle_sqldba old mode 100644 new mode 100755 index 0ed0983..70effd3 --- a/oracle_sqldba +++ b/oracle_sqldba @@ -39,10 +39,11 @@ options: 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. + - 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 prevent the execution of catcon_pl/sqlscript/sql. - 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. + - If pdb_list is given (implicitely whith all_pdbs) creates_sql is executed in every PDB incl. CDB$ROOT. The pdb_list will be shortened according to the results of creates_sql in the PDBs. required: false default: None username: @@ -71,6 +72,7 @@ options: - Optional list of PDB names - Space separated, as catcon.pl wants - Gets used only if scope is "pdbs" + - Will be automatically filled and used when scope = all_pdbs and action like sql% required: false default: None oracle_home: @@ -79,6 +81,13 @@ options: oracle_db_name: description: - SID or DB_NAME, needed for BEQ connect + nls_lang: + description: + - set NLS_LANG to the given value + chdir: + description: + - Working directory for SQL/script execution + author: Dietmar Uhlig, Robotron (www.robotron.de) ''' @@ -150,6 +159,7 @@ import os import re import shlex import shutil +import tempfile from subprocess import Popen, PIPE from threading import Timer from ansible.module_utils.basic import AnsibleModule @@ -161,9 +171,11 @@ changed = False result = "" err_msg = None oracle_home = "" +pdb_list = "" +sql_process = None # Maximum runtime for sqlplus and catcon.pl in seconds. 0 means no timeout. -timeout = 900 +timeout = 0 # 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): @@ -194,63 +206,100 @@ def conn(username, password): else: return "conn " + "/".join([username, password]) + "\n" -def sql_input(sql, username, password, scope, pdb_list): +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" 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" + return sql_scr + +def kill_process(): + global err_msg, sql_process + + sql_process.kill() + err_msg = "Timeout occured after %d seconds. " % timeout + +def run_sql_p(sql, username, password, scope, pdb_list): + global changed, err_msg, sql_process + + err_msg = "" + result = "" 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" + result += run_sql(sql, username, password, pdb) else: - sql_scr += sql + "\n" - sql_scr += "exit;\n" - return sql_scr + result = run_sql(sql, username, password, None) + return result -def run_sql(sql, username, password, scope, pdb_list): - global changed, err_msg +def run_sql(sql, username, password, pdb): + global changed, err_msg, sql_process t = None try: - sql_cmd = sql_input(sql, username, password, scope, pdb_list) - p = Popen(sqlplus(), stdin = PIPE, stdout = PIPE, stderr = PIPE) + sql_cmd = sql_input(sql, username, password, pdb) + sql_process = Popen(sqlplus(), stdin = PIPE, stdout = PIPE, stderr = PIPE) if timeout > 0: - t = Timer(timeout, p.kill) + t = Timer(timeout, kill_process) t.start() - [sout, serr] = p.communicate(input = sql_cmd) + [sout, serr] = sql_process.communicate(input = sql_cmd) except Exception as e: - err_msg = 'Could not call sqlplus. %s. called: %s.' % (to_native(e), " ".join(sqlplus())) - return "ERR" + 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: 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" + if sql_process.returncode != 0: + err_msg += "called: %s\nreturncode: %d\nresult: %s. stderr = %s." % (sql_cmd, sql_process.returncode, sout, serr) + return "[ERR]" + sqlerr_pat = re.compile("^(ORA|TNS|SP2)-[0-9]+", re.MULTILINE) + sqlplus_err = sqlerr_pat.search(sout) + if sqlplus_err: + err_msg += "[ERR] sqlplus: %s\nERR Code: %s.\n" % (sql_cmd, sqlplus_err.group()) + return "[ERR]\n%s\n" % sout.strip() changed = True return sout.strip() -def check_creates_sql(sql): - global result +def check_creates_sql(sql, scope): + global pdb_list if not sql.endswith(";"): sql += ";" - result = run_sql(sql, None, None, None, None) - return False if not result or result == "0" else True + if scope == 'cdb': + res = run_sql(sql, None, None, None) + # error handling see call of check_creates_sql + return False if not res or res == "0" else True + else: + checked_pdb_list = "" + for pdb in pdb_list.split(): + res = run_sql(sql, None, None, pdb) + # error handling see call of check_creates_sql + if not res or res == "0": + checked_pdb_list += " " + pdb + pdb_list = checked_pdb_list.lstrip() + 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(): + global result, pdb_list + 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) -def run_catcon_pl(catcon_pl, pdb_list): + +def run_catcon_pl(catcon_pl): # after pre-processing in main() the parameter scope is not necessary any more - global oracle_home, changed, result, err_msg + global oracle_home, changed, result, err_msg, pdb_list, sql_process + 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"), @@ -265,13 +314,13 @@ def run_catcon_pl(catcon_pl, pdb_list): catcon_cmd += [ "-a", "1" ] catcon_cmd += [ "--" ] + cc_script try: - p = Popen(catcon_cmd, stdout = PIPE, stderr = PIPE) + sql_process = Popen(catcon_cmd, stdout = PIPE, stderr = PIPE) if timeout > 0: - t = Timer(timeout, p.kill) + t = Timer(timeout, kill_process) t.start() - [sout, serr] = p.communicate() + [sout, serr] = sql_process.communicate() except Exception as e: - err_msg = 'Could not call perl. %s. called: %s.' % (to_native(e), " ".join(catcon_cmd)) + err_msg += 'Could not call perl. %s. called: %s.' % (to_native(e), " ".join(catcon_cmd)) return finally: if timeout > 0: @@ -281,16 +330,16 @@ def run_catcon_pl(catcon_pl, pdb_list): except OSError as exc: if exc.errno != errno.ENOENT: raise - if p.returncode != 0: - err_msg = "called: " + " ".join(catcon_cmd) + "\nresult: " + sout + ".\nstderr = " + serr + "." + if sql_process.returncode != 0: + err_msg += "called: %s\nreturncode: %d\nresult: %s\nstderr = %s." % (" ".join(catcon_cmd), sql_process.returncode, sout, serr) return - result = sout + result += sout changed = True def main(): - global oracle_home, changed, result, err_msg + global oracle_home, changed, result, err_msg, pdb_list module = AnsibleModule( argument_spec = dict( @@ -304,7 +353,9 @@ def main(): 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) + oracle_db_name = dict(required = True), + nls_lang = dict(required = False), + chdir = dict(required = False) ), mutually_exclusive=[['sql', 'sqlscript', 'catcon_pl', 'sqlselect'], ['sqlselect', 'creates_sql']] ) @@ -320,46 +371,64 @@ def main(): pdb_list = module.params["pdb_list"] oracle_home = module.params["oracle_home"] oracle_db_name = module.params["oracle_db_name"] + nls_lang = module.params["nls_lang"] + workdir = module.params["chdir"] 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 nls_lang is not None: + os.environ["NLS_LANG"] = nls_lang 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() == "": + 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) if scope == 'cdb' and catcon_pl is not None: - scope = pdbs + 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 scope == 'all_pdbs' and (catcon_pl is None or creates_sql is not None): + if is_container(): + scope = 'pdbs' + get_all_pdbs() + else: + scope = 'cdb' + + 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) if creates_sql is not None: - already_done = check_creates_sql(creates_sql) + already_done = check_creates_sql(creates_sql, scope) if err_msg: - module.fail_json(msg = err_msg, changed = False) + module.fail_json(msg = "%s\n%s" % (result, err_msg), changed = False) else: if already_done: module.exit_json(msg = result, changed = False) + if pdb_list is not None: + result = "Run on these PDBs: %s\n" % pdb_list + if sqlselect is not None: if sqlselect.endswith(";"): sqlselect.rstrip(";") sqlselect = "select dbms_xmlgen.getxml('" + sqlselect.replace("'", "''") + "') from dual;" - result = run_sql(sqlselect, username, password, scope, pdb_list) + result = run_sql_p(sqlselect, username, password, scope, pdb_list) elif sql is not None: - if not sql.endswith(";"): + sql = os.linesep.join([s for s in sql.splitlines() if s.strip()]) + if not sql.endswith(";") and not sql.endswith("/"): sql += ";" - result = run_sql(sql, username, password, scope, pdb_list) + result += run_sql_p(sql, username, password, scope, pdb_list) elif sqlscript is not None: if not sqlscript.startswith("@"): sqlscript = "@" + sqlscript - result = run_sql(sqlscript, username, password, scope, pdb_list) + result += run_sql_p(sqlscript, username, password, scope, pdb_list) elif catcon_pl is not None: - run_catcon_pl(catcon_pl, pdb_list) + run_catcon_pl(catcon_pl) if not err_msg: if sqlselect is not None: @@ -368,7 +437,7 @@ def main(): else: module.exit_json(msg = result, changed = changed) else: - module.fail_json(msg = err_msg, changed = False) + module.fail_json(msg = "%s\n%s" % (result, err_msg), changed = changed) if __name__ == '__main__':