BootstrapMysql.py 4.8 KB

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