BootstrapMysql.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. from jinja2 import Environment, FileSystemLoader
  2. from modules.BootstrapBase import BootstrapBase
  3. import os
  4. import time
  5. import subprocess
  6. class BootstrapMysql(BootstrapBase):
  7. def bootstrap(self):
  8. dbuser = "root"
  9. dbpass = os.getenv("MYSQL_ROOT_PASSWORD", "")
  10. socket = "/tmp/mysql-temp.sock"
  11. # Check if mysql has been initialized
  12. if os.path.exists("/var/lib/mysql/mysql/db.frm"):
  13. print("Starting temporary mysqld for upgrade...")
  14. self.start_temporary(socket)
  15. self.connect_mysql(socket)
  16. print("Running mysql_upgrade...")
  17. self.upgrade_mysql(dbuser, dbpass, socket)
  18. print("Checking timezone support with CONVERT_TZ...")
  19. self.check_and_import_timezone_support(dbuser, dbpass, socket)
  20. print("Shutting down temporary mysqld...")
  21. self.close_mysql()
  22. self.stop_temporary(dbuser, dbpass, socket)
  23. # Setup Jinja2 Environment and load vars
  24. self.env = Environment(
  25. loader=FileSystemLoader([
  26. '/service_config/custom_templates',
  27. '/service_config/config_templates'
  28. ]),
  29. keep_trailing_newline=True,
  30. lstrip_blocks=True,
  31. trim_blocks=True
  32. )
  33. extra_vars = {
  34. }
  35. self.env_vars = self.prepare_template_vars('/service_config/overwrites.json', extra_vars)
  36. print("Set Timezone")
  37. self.set_timezone()
  38. print("Render config")
  39. self.render_config("/service_config")
  40. def start_temporary(self, socket):
  41. """
  42. Starts a temporary mysqld process in the background using the given UNIX socket.
  43. The server is started with networking disabled (--skip-networking).
  44. Args:
  45. socket (str): Path to the UNIX socket file for MySQL to listen on.
  46. Returns:
  47. subprocess.Popen: The running mysqld process object.
  48. """
  49. return subprocess.Popen([
  50. "mysqld",
  51. "--user=mysql",
  52. "--skip-networking",
  53. f"--socket={socket}"
  54. ])
  55. def stop_temporary(self, dbuser, dbpass, socket):
  56. """
  57. Shuts down the temporary mysqld instance gracefully.
  58. Uses mariadb-admin to issue a shutdown command to the running server.
  59. Args:
  60. dbuser (str): The MySQL username with shutdown privileges (typically 'root').
  61. dbpass (str): The password for the MySQL user.
  62. socket (str): Path to the UNIX socket the server is listening on.
  63. """
  64. self.run_command([
  65. "mariadb-admin",
  66. "shutdown",
  67. f"--socket={socket}",
  68. "-u", dbuser,
  69. f"-p{dbpass}"
  70. ])
  71. def upgrade_mysql(self, dbuser, dbpass, socket, max_retries=5, wait_interval=3):
  72. """
  73. Executes mysql_upgrade to check and fix any schema or table incompatibilities.
  74. Retries the upgrade command if it fails, up to a maximum number of attempts.
  75. Args:
  76. dbuser (str): MySQL username with privilege to perform the upgrade.
  77. dbpass (str): Password for the MySQL user.
  78. socket (str): Path to the MySQL UNIX socket for local communication.
  79. max_retries (int): Maximum number of attempts before giving up. Default is 5.
  80. wait_interval (int): Number of seconds to wait between retries. Default is 3.
  81. Returns:
  82. bool: True if upgrade succeeded, False if all attempts failed.
  83. """
  84. retries = 0
  85. while retries < max_retries:
  86. result = self.run_command([
  87. "mysql_upgrade",
  88. "-u", dbuser,
  89. f"-p{dbpass}",
  90. f"--socket={socket}"
  91. ], check=False)
  92. if result.returncode == 0:
  93. print("mysql_upgrade completed successfully.")
  94. break
  95. else:
  96. print(f"mysql_upgrade failed (try {retries+1}/{max_retries})")
  97. retries += 1
  98. time.sleep(wait_interval)
  99. else:
  100. print("mysql_upgrade failed after all retries.")
  101. return False
  102. def check_and_import_timezone_support(self, dbuser, dbpass, socket):
  103. """
  104. Checks if MySQL supports timezone conversion (CONVERT_TZ).
  105. If not, it imports timezone info using mysql_tzinfo_to_sql piped into mariadb.
  106. """
  107. try:
  108. cursor = self.mysql_conn.cursor()
  109. cursor.execute("SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC')")
  110. result = cursor.fetchone()
  111. cursor.close()
  112. if not result or result[0] is None:
  113. print("Timezone conversion failed or returned NULL. Importing timezone info...")
  114. # Use mysql_tzinfo_to_sql piped into mariadb
  115. tz_dump = subprocess.Popen(
  116. ["mysql_tzinfo_to_sql", "/usr/share/zoneinfo"],
  117. stdout=subprocess.PIPE
  118. )
  119. self.run_command([
  120. "mariadb",
  121. "--socket", socket,
  122. "-u", dbuser,
  123. f"-p{dbpass}",
  124. "mysql"
  125. ], input_stream=tz_dump.stdout)
  126. tz_dump.stdout.close()
  127. tz_dump.wait()
  128. print("Timezone info successfully imported.")
  129. else:
  130. print(f"Timezone support is working. Sample result: {result[0]}")
  131. except Exception as e:
  132. print(f"Failed to verify or import timezone info: {e}")