BootstrapPhpfpm.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. from jinja2 import Environment, FileSystemLoader
  2. from modules.BootstrapBase import BootstrapBase
  3. from pathlib import Path
  4. import os
  5. import ipaddress
  6. import sys
  7. import time
  8. import platform
  9. import subprocess
  10. class Bootstrap(BootstrapBase):
  11. def bootstrap(self):
  12. self.connect_mysql()
  13. self.connect_redis()
  14. # Setup Jinja2 Environment and load vars
  15. self.env = Environment(
  16. loader=FileSystemLoader('/php-conf/config_templates'),
  17. keep_trailing_newline=True,
  18. lstrip_blocks=True,
  19. trim_blocks=True
  20. )
  21. extra_vars = {
  22. }
  23. self.env_vars = self.prepare_template_vars('/overwrites.json', extra_vars)
  24. print("Set Timezone")
  25. self.set_timezone()
  26. # Prepare Redis and MySQL Database
  27. # TODO: move to dockerapi
  28. if self.isYes(os.getenv("MASTER", "")):
  29. print("We are master, preparing...")
  30. self.prepare_redis()
  31. self.setup_apikeys(
  32. os.getenv("API_ALLOW_FROM", "").strip(),
  33. os.getenv("API_KEY", "").strip(),
  34. os.getenv("API_KEY_READ_ONLY", "").strip()
  35. )
  36. self.setup_mysql_events()
  37. print("Render config")
  38. self.render_config("opcache-recommended.ini.j2", "/usr/local/etc/php/conf.d/opcache-recommended.ini")
  39. self.render_config("pools.conf.j2", "/usr/local/etc/php-fpm.d/z-pools.conf")
  40. self.render_config("other.ini.j2", "/usr/local/etc/php/conf.d/zzz-other.ini")
  41. self.render_config("upload.ini.j2", "/usr/local/etc/php/conf.d/upload.ini")
  42. self.render_config("session_store.ini.j2", "/usr/local/etc/php/conf.d/session_store.ini")
  43. self.render_config("0081-custom-mailcow.css.j2", "/web/css/build/0081-custom-mailcow.css")
  44. self.copy_file("/usr/local/etc/php/conf.d/opcache-recommended.ini", "/php-conf/opcache-recommended.ini")
  45. self.copy_file("/usr/local/etc/php-fpm.d/z-pools.conf", "/php-conf/pools.conf")
  46. self.copy_file("/usr/local/etc/php/conf.d/zzz-other.ini", "/php-conf/other.ini")
  47. self.copy_file("/usr/local/etc/php/conf.d/upload.ini", "/php-conf/upload.ini")
  48. self.copy_file("/usr/local/etc/php/conf.d/session_store.ini", "/php-conf/session_store.ini")
  49. self.set_owner("/global_sieve", 82, 82, recursive=True)
  50. self.set_owner("/web/templates/cache", 82, 82, recursive=True)
  51. self.remove("/web/templates/cache", wipe_contents=True, exclude=[".gitkeep"])
  52. print("Running DB init...")
  53. self.run_command(["php", "-c", "/usr/local/etc/php", "-f", "/web/inc/init_db.inc.php"], check=False)
  54. def prepare_redis(self):
  55. print("Setting default Redis keys if missing...")
  56. # Q_RELEASE_FORMAT
  57. if self.redis_conn.get("Q_RELEASE_FORMAT") is None:
  58. self.redis_conn.set("Q_RELEASE_FORMAT", "raw")
  59. # Q_MAX_AGE
  60. if self.redis_conn.get("Q_MAX_AGE") is None:
  61. self.redis_conn.set("Q_MAX_AGE", 365)
  62. # PASSWD_POLICY hash defaults
  63. if self.redis_conn.hget("PASSWD_POLICY", "length") is None:
  64. self.redis_conn.hset("PASSWD_POLICY", mapping={
  65. "length": 6,
  66. "chars": 0,
  67. "special_chars": 0,
  68. "lowerupper": 0,
  69. "numbers": 0
  70. })
  71. # DOMAIN_MAP
  72. print("Rebuilding DOMAIN_MAP from MySQL...")
  73. self.redis_conn.delete("DOMAIN_MAP")
  74. domains = set()
  75. try:
  76. cursor = self.mysql_conn.cursor()
  77. cursor.execute("SELECT domain FROM domain")
  78. domains.update(row[0] for row in cursor.fetchall())
  79. cursor.execute("SELECT alias_domain FROM alias_domain")
  80. domains.update(row[0] for row in cursor.fetchall())
  81. cursor.close()
  82. if domains:
  83. for domain in domains:
  84. self.redis_conn.hset("DOMAIN_MAP", domain, 1)
  85. print(f"{len(domains)} domains added to DOMAIN_MAP.")
  86. else:
  87. print("No domains found to insert into DOMAIN_MAP.")
  88. except Exception as e:
  89. print(f"Failed to rebuild DOMAIN_MAP: {e}")
  90. def setup_apikeys(self, api_allow_from, api_key_rw, api_key_ro):
  91. if not api_allow_from or api_allow_from == "invalid":
  92. return
  93. print("Validating API_ALLOW_FROM IPs...")
  94. ip_list = [ip.strip() for ip in api_allow_from.split(",")]
  95. validated_ips = []
  96. for ip in ip_list:
  97. try:
  98. ipaddress.ip_network(ip, strict=False)
  99. validated_ips.append(ip)
  100. except ValueError:
  101. continue
  102. if not validated_ips:
  103. print("No valid IPs found in API_ALLOW_FROM")
  104. return
  105. allow_from_str = ",".join(validated_ips)
  106. cursor = self.mysql_conn.cursor()
  107. try:
  108. if api_key_rw and api_key_rw != "invalid":
  109. print("Setting RW API key...")
  110. cursor.execute("DELETE FROM api WHERE access = 'rw'")
  111. cursor.execute(
  112. "INSERT INTO api (api_key, active, allow_from, access) VALUES (%s, %s, %s, %s)",
  113. (api_key_rw, 1, allow_from_str, "rw")
  114. )
  115. if api_key_ro and api_key_ro != "invalid":
  116. print("Setting RO API key...")
  117. cursor.execute("DELETE FROM api WHERE access = 'ro'")
  118. cursor.execute(
  119. "INSERT INTO api (api_key, active, allow_from, access) VALUES (%s, %s, %s, %s)",
  120. (api_key_ro, 1, allow_from_str, "ro")
  121. )
  122. self.mysql_conn.commit()
  123. print("API key(s) set successfully.")
  124. except Exception as e:
  125. print(f"Failed to configure API keys: {e}")
  126. self.mysql_conn.rollback()
  127. finally:
  128. cursor.close()
  129. def setup_mysql_events(self):
  130. print("Creating scheduled MySQL EVENTS...")
  131. queries = [
  132. "DROP EVENT IF EXISTS clean_spamalias;",
  133. """
  134. CREATE EVENT clean_spamalias
  135. ON SCHEDULE EVERY 1 DAY
  136. DO
  137. DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
  138. """,
  139. "DROP EVENT IF EXISTS clean_oauth2;",
  140. """
  141. CREATE EVENT clean_oauth2
  142. ON SCHEDULE EVERY 1 DAY
  143. DO
  144. BEGIN
  145. DELETE FROM oauth_refresh_tokens WHERE expires < NOW();
  146. DELETE FROM oauth_access_tokens WHERE expires < NOW();
  147. DELETE FROM oauth_authorization_codes WHERE expires < NOW();
  148. END;
  149. """,
  150. "DROP EVENT IF EXISTS clean_sasl_log;",
  151. """
  152. CREATE EVENT clean_sasl_log
  153. ON SCHEDULE EVERY 1 DAY
  154. DO
  155. BEGIN
  156. DELETE sasl_log.* FROM sasl_log
  157. LEFT JOIN (
  158. SELECT username, service, MAX(datetime) AS lastdate
  159. FROM sasl_log
  160. GROUP BY username, service
  161. ) AS last
  162. ON sasl_log.username = last.username AND sasl_log.service = last.service
  163. WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY)
  164. AND datetime < lastdate;
  165. DELETE FROM sasl_log
  166. WHERE username NOT IN (SELECT username FROM mailbox)
  167. AND datetime < DATE_SUB(NOW(), INTERVAL 31 DAY);
  168. END;
  169. """
  170. ]
  171. try:
  172. cursor = self.mysql_conn.cursor()
  173. for query in queries:
  174. cursor.execute(query)
  175. self.mysql_conn.commit()
  176. cursor.close()
  177. print("MySQL EVENTS created successfully.")
  178. except Exception as e:
  179. print(f"Failed to create MySQL EVENTS: {e}")
  180. self.mysql_conn.rollback()