3737import logging
3838import subprocess
3939import time
40- from typing import Optional , Literal
40+ from typing import Optional , Literal , Union
4141from enum import Enum
4242
4343from container_ci_suite .engines .podman_wrapper import PodmanCLIWrapper
@@ -95,8 +95,9 @@ def __init__(
9595 self .image_name = image_name
9696 self .db_type = db_type .lower ()
9797 logger .debug (
98- f"DatabaseWrapper initialized with image: { image_name } , "
99- f"type: { self .db_type } "
98+ "DatabaseWrapper initialized with image: %s, type: %s" ,
99+ image_name ,
100+ self .db_type ,
100101 )
101102
102103 def assert_login_success (
@@ -180,8 +181,11 @@ def assert_login_access(
180181 >>> assert db.assert_login_access("172.17.0.2", "user", "wrong", False)
181182 """
182183 logger .info (
183- f"Testing { self .db_type } login as { username } :{ password } ; "
184- f"expected_success={ expected_success } "
184+ "Testing %s login as %s:%s; expected_success=%s" ,
185+ self .db_type ,
186+ username ,
187+ password ,
188+ expected_success ,
185189 )
186190
187191 try :
@@ -195,22 +199,22 @@ def assert_login_access(
195199 )
196200
197201 if success and expected_success :
198- logger .info (f " { username } ( { password } ) access granted as expected" )
202+ logger .info (" %s(%s ) access granted as expected" , username , password )
199203 return True
200204 elif not success and not expected_success :
201- logger .info (f " { username } ( { password } ) access denied as expected" )
205+ logger .info (" %s(%s ) access denied as expected" , username , password )
202206 return True
203207 else :
204- logger .error (f " { username } ( { password } ) login assertion failed" )
208+ logger .error (" %s(%s ) login assertion failed" , username , password )
205209 return False
206210
207211 except Exception as e :
208- logger .error (f "Error during login test: { e } " )
212+ logger .error ("Error during login test: %s" , e )
209213 if not expected_success :
210- logger .info (f " { username } ( { password } ) access denied as expected" )
214+ logger .info (" %s(%s ) access denied as expected" , username , password )
211215 return True
212216 else :
213- logger .error (f " { username } ( { password } ) login assertion failed" )
217+ logger .error (" %s(%s ) login assertion failed" , username , password )
214218 return False
215219
216220 def _test_mysql_login (
@@ -287,6 +291,8 @@ def mysql_cmd(
287291 port : int = 3306 ,
288292 extra_args : str = "" ,
289293 sql_command : Optional [str ] = None ,
294+ container_id : Optional [str ] = None ,
295+ podman_run_command : Optional [str ] = "run --rm" ,
290296 ) -> str :
291297 """
292298 Execute a MySQL command against a container.
@@ -310,7 +316,8 @@ def mysql_cmd(
310316 port: Port number (default: 3306)
311317 extra_args: Additional arguments to pass to mysql command
312318 sql_command: SQL command to execute (e.g., "-e 'SELECT 1;'")
313-
319+ podman_run_command: Podman run command to use (default: "run --rm")
320+ ignore_error: Ignore error and return output (default: False)
314321 Returns:
315322 Command output as string
316323
@@ -322,9 +329,13 @@ def mysql_cmd(
322329 >>> output = db.mysql_cmd("172.17.0.2", "user", "pass",
323330 ... sql_command="-e 'SELECT 1;'")
324331 """
332+ if not container_id :
333+ container_id = self .image_name
334+ if not sql_command :
335+ sql_command = "-e 'SELECT 1;'"
325336 cmd_parts = [
326- "run --rm" ,
327- self . image_name ,
337+ podman_run_command ,
338+ container_id ,
328339 "mysql" ,
329340 f"--host { container_ip } " ,
330341 f"--port { port } " ,
@@ -341,19 +352,23 @@ def mysql_cmd(
341352 cmd_parts .append (database )
342353
343354 cmd = " " .join (cmd_parts )
344- logging .debug (f "Executing command: { cmd } " )
355+ logging .debug ("Executing command: %s" , cmd )
345356
346- return PodmanCLIWrapper .call_podman_command (cmd = cmd , return_output = True )
357+ return PodmanCLIWrapper .call_podman_command (
358+ cmd = cmd , return_output = True , ignore_error = False
359+ )
347360
348361 def postgresql_cmd (
349362 self ,
350363 container_ip : str ,
351364 username : str ,
352365 password : str ,
366+ container_id : Optional [str ] = None ,
353367 database : str = "db" ,
354368 port : int = 5432 ,
355369 extra_args : str = "" ,
356370 sql_command : Optional [str ] = None ,
371+ podman_run_command : Optional [str ] = "run --rm" ,
357372 ) -> str :
358373 """
359374 Execute a PostgreSQL command against a container.
@@ -387,9 +402,10 @@ def postgresql_cmd(
387402 ... sql_command="-c 'SELECT 1;'")
388403 """
389404 connection_string = f"postgresql://{ username } @{ container_ip } :{ port } /{ database } "
390-
405+ if not container_id :
406+ container_id = self .image_name
391407 cmd_parts = [
392- "run --rm" ,
408+ podman_run_command ,
393409 f"-e PGPASSWORD={ password } " ,
394410 self .image_name ,
395411 "psql" ,
@@ -432,7 +448,7 @@ def test_connection(
432448 port: Port number (default: 3306 for MySQL, 5432 for PostgreSQL)
433449 max_attempts: Maximum number of connection attempts (default: 60)
434450 sleep_time: Seconds to wait between attempts (default: 3)
435-
451+ sql_cmd: SQL command to execute (e.g., "SELECT 1;")
436452 Returns:
437453 True if connection successful, False otherwise
438454
@@ -441,7 +457,7 @@ def test_connection(
441457 >>> if db.test_connection("172.17.0.2", "user", "pass"):
442458 ... print("Database is ready!")
443459 """
444- logger .info (f "Testing { self . db_type } connection to { container_ip } ..." )
460+ logger .info ("Testing %s connection to %s ..." , self . db_type , container_ip )
445461 logger .info ("Trying to connect..." )
446462 for attempt in range (1 , max_attempts + 1 ):
447463 try :
@@ -463,16 +479,16 @@ def test_connection(
463479 database = database ,
464480 sql_command = sql_cmd ,
465481 )
466- logging .debug (f "Output: { return_output } " )
467- logger .info (f "Connection successful on attempt { attempt } " )
482+ logging .debug ("Output: %s" , return_output )
483+ logger .info ("Connection successful on attempt %s" , attempt )
468484 return True
469485
470486 except subprocess .CalledProcessError :
471487 if attempt < max_attempts :
472- logger .debug (f "Attempt { attempt } failed, retrying..." )
488+ logger .debug ("Attempt %s failed, retrying..." , attempt )
473489 time .sleep (sleep_time )
474490 else :
475- logger .error (f "Failed to connect after { max_attempts } attempts" )
491+ logger .error ("Failed to connect after %s attempts" , max_attempts )
476492 return False
477493
478494 return False
@@ -526,27 +542,118 @@ def assert_local_access(self, container_id: str, username: str = None) -> bool:
526542 logger .error (" Local access assertion failed" )
527543 return False
528544
529- # def run_db_command(
530- # self, container_id: str, username: str, password: str, db_command: str
531- # ) -> str:
532- # """
533- # Run a database command inside the container.
534-
535- # Args:
536- # container_id: Container ID or name
537- # command: Command to run like mysql or psql
538-
539- # Returns:
540- # Command output as string
541- # """
542- # if self.db_type in ["postgresql", "postgres"]:
543- # db_cmd = (
544- # f"psql -h {container_ip} -u{username} -p{password} -e '{db_command};'"
545- # )
546- # else:
547- # db_cmd = (
548- # f"mysql -h {container_ip} -u{username} -p{password} -e '{db_command};'"
549- # )
550- # return PodmanCLIWrapper.call_podman_command(
551- # cmd=f"run --rm {container_id} {db_cmd}", return_output=True
552- # )
545+ def run_sql_command (
546+ self ,
547+ username : Optional [str ] = None ,
548+ password : Optional [str ] = None ,
549+ container_ip : str = None ,
550+ port : int = 3306 ,
551+ sql_cmd : Optional [Union [list [str ], str ]] = None ,
552+ database : str = "db" ,
553+ max_attempts : int = 60 ,
554+ sleep_time : int = 3 ,
555+ container_id : Optional [str ] = None ,
556+ podman_run_command : Optional [str ] = "run --rm" ,
557+ ignore_error : bool = False ,
558+ ) -> str | bool :
559+ """
560+ Run a database command inside the container.
561+
562+ Bash equivalent:
563+ ```bash
564+ docker exec -i $(get_cid "$id") bash -c psql <<< "SELECT 1;"
565+ ```
566+
567+ Args:
568+ username: Username to test with (default: "root" for MySQL, None for PostgreSQL)
569+ password: Password to test with
570+ container_ip: IP address of the container
571+ sql_cmd: SQL command to execute (e.g., "SELECT 1;")
572+ database: Database name (default: "db")
573+ port: Port number (default: 3306 for MySQL, 5432 for PostgreSQL)
574+ max_attempts: Maximum number of attempts (default: 60)
575+ sleep_time: Time to sleep between attempts (default: 3)
576+ container_id: Container ID or name
577+ podman_run_command: Podman run command to use (default: "run --rm")
578+
579+ Returns:
580+ Command output as string or False if command failed
581+ """
582+ if not container_id :
583+ container_id = self .image_name
584+ if not sql_cmd :
585+ sql_cmd = "SELECT 1;"
586+ if isinstance (sql_cmd , str ):
587+ sql_cmd = [sql_cmd ]
588+ logger .debug (
589+ "Podman run command: %s with image: %s" , podman_run_command , container_id
590+ )
591+ logger .debug ("Database type: %s" , self .db_type )
592+ logger .debug ("SQL command: %s" , sql_cmd )
593+ logger .debug ("Database: %s" , database )
594+ logger .debug ("Username: %s" , username )
595+ logger .debug ("Password: %s" , password )
596+ logger .debug ("Container IP: %s" , container_ip )
597+ logger .debug ("Port: %s" , port )
598+ logger .debug ("Max attempts: %s" , max_attempts )
599+ logger .debug ("Sleep time: %s" , sleep_time )
600+ return_output = None
601+ for cmd in sql_cmd :
602+ for attempt in range (1 , max_attempts + 1 ):
603+ if self .db_type in ["postgresql" , "postgres" ]:
604+ return_output = self .postgresql_cmd (
605+ container_ip = container_ip ,
606+ username = username ,
607+ password = password ,
608+ database = database ,
609+ sql_command = f"-e '{ cmd } '" ,
610+ container_id = container_id ,
611+ podman_run_command = podman_run_command ,
612+ )
613+ else :
614+ try :
615+ return_output = self .mysql_cmd (
616+ container_ip = container_ip ,
617+ username = username ,
618+ password = password ,
619+ database = database ,
620+ sql_command = f"-e '{ cmd } '" ,
621+ container_id = container_id ,
622+ podman_run_command = podman_run_command ,
623+ )
624+ except subprocess .CalledProcessError as cpe :
625+ # In case of ignore_error, we return the output
626+ # This is useful for commands that are expected to fail, like wrong login
627+ if ignore_error :
628+ return_output = cpe .output
629+ else :
630+ logger .error (
631+ "Failed to execute command, output: %s, error: %s" ,
632+ cpe .output ,
633+ cpe .stderr ,
634+ )
635+ return False
636+ if return_output or return_output == "" :
637+ logger .info ("Command executed successfully on attempt %s" , attempt )
638+ # Let's break out of the loop and return the output
639+ break
640+ else :
641+ if attempt < max_attempts :
642+ logger .debug (
643+ "Attempt %s failed, output: '%s', retrying..." ,
644+ attempt ,
645+ return_output ,
646+ )
647+ time .sleep (sleep_time )
648+ else :
649+ logger .error (
650+ "Failed to execute command after %s attempts, output: %s" ,
651+ max_attempts ,
652+ return_output ,
653+ )
654+ return False
655+ if return_output :
656+ logger .info ("All commands executed successfully" )
657+ logger .debug ("Output:\n %s" , return_output )
658+ return return_output
659+ return False
0 commit comments