7676 type: list
7777 elements: str
7878notes:
79- - Does not support C(check_mode). Always reports that the state has changed even if no changes have been made.
79+ - Does not support C(check_mode).
80+ - If filepath is used, changed is true. If query is used, changed is true if it is not a SELECT.
81+ - Avoid using login shell flags (like 'become_flags' with value of '-i') when become_user is a SAP admin.
82+ - If a login shell is required, manually reset the environment in the task using the environment keyword.
8083author:
8184 - Rainer Leber (@rainerleber)
8285'''
156159from ansible .module_utils .common .text .converters import to_native
157160
158161
159- def csv_to_list (rawcsv ):
160- reader_raw = csv .DictReader (StringIO (rawcsv ))
161- reader = [dict ((k , v .strip ()) for k , v in row .items ()) for row in reader_raw ]
162+ def csv_to_list (raw_csv ):
163+ if not raw_csv .strip ():
164+ return []
165+
166+ reader_raw = csv .DictReader (StringIO (raw_csv ))
167+ # Using to_native and strip to ensure clean dictionary keys/values
168+ reader = [dict ((to_native (k ), to_native (v ).strip ()) for k , v in row .items ()) for row in reader_raw ]
162169 return list (reader )
163170
164171
172+ def run_hdb_command (module , full_cmd ):
173+ rc , out_raw , err = module .run_command (full_cmd )
174+
175+ if rc != 0 :
176+ err_msg = to_native (err ).lower ()
177+
178+ if "authentication failed" in err_msg or "invalid username or password" in err_msg :
179+ module .fail_json (msg = "SAP HANA Authentication Failed. Please check credentials/userstore." , rc = rc , stderr = err )
180+
181+ elif "insufficient privilege" in err_msg or "258:" in err_msg :
182+ module .fail_json (msg = "SAP HANA Authorization Error: The user has insufficient privileges to perform this action." , rc = rc , stderr = err )
183+
184+ elif "connect failed" in err_msg :
185+ module .fail_json (msg = "SAP HANA Connection Failed. Check host, instance, and port." , rc = rc , stderr = err )
186+
187+ else :
188+ module .fail_json (msg = "SQL Execution Error" , rc = rc , stderr = err , cmd = full_cmd )
189+
190+ return out_raw
191+
192+
165193def main ():
166194 module = AnsibleModule (
167195 argument_spec = dict (
@@ -178,68 +206,88 @@ def main():
178206 filepath = dict (type = 'list' , elements = 'path' , required = False ),
179207 autocommit = dict (type = 'bool' , default = True ),
180208 ),
181- required_one_of = [('query' , 'filepath' ), ( 'sid' , 'instance' ) ],
209+ required_one_of = [('query' , 'filepath' )],
182210 required_if = [('userstore' , False , ['password' ])],
183211 supports_check_mode = False ,
184212 )
185- rc , out , err , out_raw = [0 , [], "" , "" ]
186213
187214 params = module .params
215+ output = []
216+ has_changed = False
188217
189- sid = params ['sid' ]
190- bin_path = params ['bin_path' ]
191- instance = params ['instance' ]
192- user = params ['user' ]
193- userstore = params ['userstore' ]
194- password = params ['password' ]
195- autocommit = params ['autocommit' ]
196- host = params ['host' ]
197- database = params ['database' ]
198- encrypted = params ['encrypted' ]
199-
200- filepath = params ['filepath' ]
201- query = params ['query' ]
218+ # Determine if module will show as changed.
219+ # If filepaths are provided, we assume changes will be made, as files typically contain DDL or DML statements.
220+ # If only queries are provided, we check if any of them are not SELECT statements. If at least one is not a SELECT, we assume changes will be made.
221+ if params ['filepath' ]:
222+ has_changed = True
202223
224+ elif params ['query' ]:
225+ for q in params ['query' ]:
226+ # Strip whitespace and check if it starts with SELECT
227+ if not q .strip ().upper ().startswith ("SELECT" ):
228+ has_changed = True
229+ break
230+
231+ # Construct Binary Path
232+ bin_path = params ['bin_path' ]
203233 if bin_path is None :
204- bin_path = "/usr/sap/{sid}/HDB{instance}/exe/hdbsql" .format (sid = sid .upper (), instance = instance )
234+ if not params ['sid' ]:
235+ module .fail_json (msg = "Parameter 'sid' is required to determine default bin_path if 'bin_path' is not provided." )
236+
237+ bin_path = "/usr/sap/{sid}/HDB{instance}/exe/hdbsql" .format (
238+ sid = params ['sid' ].upper (),
239+ instance = params ['instance' ]
240+ )
205241
206242 try :
207243 command = [module .get_bin_path (bin_path , required = True )]
208244 except Exception as e :
209- module .fail_json (msg = 'Failed to find hdbsql at the expected path " {0}".Please check SID and instance number: " {1}" ' .format (bin_path , to_native (e )))
245+ module .fail_json (msg = 'Executable binary hdbsql not found at {0}: {1}' .format (bin_path , to_native (e )))
210246
211- if encrypted is True :
212- command .extend (['-attemptencrypt' ])
213- if autocommit is False :
247+ # Build Base Command
248+ if params ['encrypted' ]:
249+ # -e: Enforce encryption (don't fall back)
250+ # -ssltrustcert: Trust the server certificate
251+ # -sslcreatecert: Create a local certificate if necessary
252+ command .extend (['-e' , '-ssltrustcert' , '-sslcreatecert' ])
253+
254+ if not params ['autocommit' ]:
214255 command .extend (['-z' ])
215- if host is not None :
216- command .extend (['-n' , host ])
217- if database is not None :
218- command .extend (['-d' , database ])
219- # -x Suppresses additional output, such as the number of selected rows in a result set.
220- if userstore :
221- command .extend (['-x' , '-U' , user ])
256+
257+ if params ['host' ]:
258+ command .extend (['-n' , params ['host' ]])
259+
260+ if params ['database' ]:
261+ command .extend (['-d' , params ['database' ]])
262+
263+ if params ['userstore' ]:
264+ command .extend (['-x' , '-U' , params ['user' ]])
265+
222266 else :
223- command .extend (['-x' , '-i' , instance , '-u' , user , '-p' , password ])
224-
225- if filepath is not None :
226- command .extend (['-E 3' , '-I' ])
227- for p in filepath :
228- # makes a command like hdbsql -i 01 -u SYSTEM -p secret123# -I /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt,
229- # iterates through files and append the output to var out.
230- query_command = command + [p ]
231- (rc , out_raw , err ) = module .run_command (query_command )
232- out .append (csv_to_list (out_raw ))
233- if query is not None :
234- for q in query :
235- # makes a command like hdbsql -i 01 -u SYSTEM -p secret123# "select user_name from users",
236- # iterates through multiple commands and append the output to var out.
267+ command .extend (['-x' , '-i' , params ['instance' ], '-u' , params ['user' ], '-p' , params ['password' ]])
268+
269+ # Process Queries
270+ if params ['query' ]:
271+ for q in params ['query' ]:
237272 query_command = command + [q ]
238- (rc , out_raw , err ) = module .run_command (query_command )
239- out .append (csv_to_list (out_raw ))
240- changed = True
273+ out_raw = run_hdb_command (module , query_command )
274+ try :
275+ output .append (csv_to_list (out_raw ))
276+ except Exception as e :
277+ module .fail_json (msg = "Failed to parse CSV output: {0}" .format (to_native (e )))
278+
279+ # Process Files
280+ # Note: File processing adds extra arguments so it has to execute after query processing.
281+ if params ['filepath' ]:
282+ for p in params ['filepath' ]:
283+ file_query_command = command + ['-E' , '3' , '-I' , p ]
284+ out_raw = run_hdb_command (module , file_query_command )
285+ try :
286+ output .append (csv_to_list (out_raw ))
287+ except Exception as e :
288+ module .fail_json (msg = "Failed to parse output from file {0}: {1}" .format (p , to_native (e )))
241289
242- module .exit_json (changed = changed , rc = rc , query_result = out , stderr = err )
290+ module .exit_json (changed = has_changed , query_result = output )
243291
244292
245293if __name__ == '__main__' :
0 commit comments