start.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. #!/usr/local/bin/python
  2. import codecs
  3. import glob
  4. import os
  5. import platform
  6. import subprocess
  7. import sys
  8. from typing import Any, Dict, List, Mapping, MutableMapping, NoReturn, Optional
  9. import jinja2
  10. # Utility functions
  11. def log(txt: str) -> None:
  12. print(txt)
  13. def error(txt: str) -> NoReturn:
  14. print(txt, file=sys.stderr)
  15. sys.exit(2)
  16. def flush_buffers() -> None:
  17. sys.stdout.flush()
  18. sys.stderr.flush()
  19. def convert(src: str, dst: str, environ: Mapping[str, object]) -> None:
  20. """Generate a file from a template
  21. Args:
  22. src: path to input file
  23. dst: path to file to write
  24. environ: environment dictionary, for replacement mappings.
  25. """
  26. with open(src) as infile:
  27. template = infile.read()
  28. rendered = jinja2.Template(template).render(**environ)
  29. with open(dst, "w") as outfile:
  30. outfile.write(rendered)
  31. def generate_config_from_template(
  32. config_dir: str,
  33. config_path: str,
  34. os_environ: Mapping[str, str],
  35. ownership: Optional[str],
  36. ) -> None:
  37. """Generate a homeserver.yaml from environment variables
  38. Args:
  39. config_dir: where to put generated config files
  40. config_path: where to put the main config file
  41. os_environ: environment mapping
  42. ownership: "<user>:<group>" string which will be used to set
  43. ownership of the generated configs. If None, ownership will not change.
  44. """
  45. for v in ("SYNAPSE_SERVER_NAME", "SYNAPSE_REPORT_STATS"):
  46. if v not in os_environ:
  47. error(
  48. "Environment variable '%s' is mandatory when generating a config file."
  49. % (v,)
  50. )
  51. # populate some params from data files (if they exist, else create new ones)
  52. environ: Dict[str, Any] = dict(os_environ)
  53. secrets = {
  54. "registration": "SYNAPSE_REGISTRATION_SHARED_SECRET",
  55. "macaroon": "SYNAPSE_MACAROON_SECRET_KEY",
  56. }
  57. for name, secret in secrets.items():
  58. if secret not in environ:
  59. filename = "/data/%s.%s.key" % (environ["SYNAPSE_SERVER_NAME"], name)
  60. # if the file already exists, load in the existing value; otherwise,
  61. # generate a new secret and write it to a file
  62. if os.path.exists(filename):
  63. log("Reading %s from %s" % (secret, filename))
  64. with open(filename) as handle:
  65. value = handle.read()
  66. else:
  67. log(f"Generating a random secret for {secret}")
  68. value = codecs.encode(os.urandom(32), "hex").decode()
  69. with open(filename, "w") as handle:
  70. handle.write(value)
  71. environ[secret] = value
  72. environ["SYNAPSE_APPSERVICES"] = glob.glob("/data/appservices/*.yaml")
  73. if not os.path.exists(config_dir):
  74. os.mkdir(config_dir)
  75. # Convert SYNAPSE_NO_TLS to boolean if exists
  76. if "SYNAPSE_NO_TLS" in environ:
  77. tlsanswerstring = str.lower(environ["SYNAPSE_NO_TLS"])
  78. if tlsanswerstring in ("true", "on", "1", "yes"):
  79. environ["SYNAPSE_NO_TLS"] = True
  80. else:
  81. if tlsanswerstring in ("false", "off", "0", "no"):
  82. environ["SYNAPSE_NO_TLS"] = False
  83. else:
  84. error(
  85. 'Environment variable "SYNAPSE_NO_TLS" found but value "'
  86. + tlsanswerstring
  87. + '" unrecognized; exiting.'
  88. )
  89. if "SYNAPSE_LOG_CONFIG" not in environ:
  90. environ["SYNAPSE_LOG_CONFIG"] = config_dir + "/log.config"
  91. log("Generating synapse config file " + config_path)
  92. convert("/conf/homeserver.yaml", config_path, environ)
  93. log_config_file = environ["SYNAPSE_LOG_CONFIG"]
  94. log("Generating log config file " + log_config_file)
  95. convert(
  96. "/conf/log.config",
  97. log_config_file,
  98. {**environ, "include_worker_name_in_log_line": False},
  99. )
  100. # Hopefully we already have a signing key, but generate one if not.
  101. args = [
  102. sys.executable,
  103. "-m",
  104. "synapse.app.homeserver",
  105. "--config-path",
  106. config_path,
  107. # tell synapse to put generated keys in /data rather than /compiled
  108. "--keys-directory",
  109. config_dir,
  110. "--generate-keys",
  111. ]
  112. if ownership is not None:
  113. log(f"Setting ownership on /data to {ownership}")
  114. subprocess.run(["chown", "-R", ownership, "/data"], check=True)
  115. args = ["gosu", ownership] + args
  116. subprocess.run(args, check=True)
  117. def run_generate_config(environ: Mapping[str, str], ownership: Optional[str]) -> None:
  118. """Run synapse with a --generate-config param to generate a template config file
  119. Args:
  120. environ: env vars from `os.enrivon`.
  121. ownership: "userid:groupid" arg for chmod. If None, ownership will not change.
  122. Never returns.
  123. """
  124. for v in ("SYNAPSE_SERVER_NAME", "SYNAPSE_REPORT_STATS"):
  125. if v not in environ:
  126. error("Environment variable '%s' is mandatory in `generate` mode." % (v,))
  127. server_name = environ["SYNAPSE_SERVER_NAME"]
  128. config_dir = environ.get("SYNAPSE_CONFIG_DIR", "/data")
  129. config_path = environ.get("SYNAPSE_CONFIG_PATH", config_dir + "/homeserver.yaml")
  130. data_dir = environ.get("SYNAPSE_DATA_DIR", "/data")
  131. if ownership is not None:
  132. # make sure that synapse has perms to write to the data dir.
  133. log(f"Setting ownership on {data_dir} to {ownership}")
  134. subprocess.run(["chown", ownership, data_dir], check=True)
  135. # create a suitable log config from our template
  136. log_config_file = "%s/%s.log.config" % (config_dir, server_name)
  137. if not os.path.exists(log_config_file):
  138. log("Creating log config %s" % (log_config_file,))
  139. convert("/conf/log.config", log_config_file, environ)
  140. # generate the main config file, and a signing key.
  141. args = [
  142. sys.executable,
  143. "-m",
  144. "synapse.app.homeserver",
  145. "--server-name",
  146. server_name,
  147. "--report-stats",
  148. environ["SYNAPSE_REPORT_STATS"],
  149. "--config-path",
  150. config_path,
  151. "--config-directory",
  152. config_dir,
  153. "--data-directory",
  154. data_dir,
  155. "--generate-config",
  156. "--open-private-ports",
  157. ]
  158. # log("running %s" % (args, ))
  159. flush_buffers()
  160. os.execv(sys.executable, args)
  161. def main(args: List[str], environ: MutableMapping[str, str]) -> None:
  162. mode = args[1] if len(args) > 1 else "run"
  163. # if we were given an explicit user to switch to, do so
  164. ownership = None
  165. if "UID" in environ:
  166. desired_uid = int(environ["UID"])
  167. desired_gid = int(environ.get("GID", "991"))
  168. ownership = f"{desired_uid}:{desired_gid}"
  169. elif os.getuid() == 0:
  170. # otherwise, if we are running as root, use user 991
  171. ownership = "991:991"
  172. synapse_worker = environ.get("SYNAPSE_WORKER", "synapse.app.homeserver")
  173. # In generate mode, generate a configuration and missing keys, then exit
  174. if mode == "generate":
  175. return run_generate_config(environ, ownership)
  176. if mode == "migrate_config":
  177. # generate a config based on environment vars.
  178. config_dir = environ.get("SYNAPSE_CONFIG_DIR", "/data")
  179. config_path = environ.get(
  180. "SYNAPSE_CONFIG_PATH", config_dir + "/homeserver.yaml"
  181. )
  182. return generate_config_from_template(
  183. config_dir, config_path, environ, ownership
  184. )
  185. if mode != "run":
  186. error("Unknown execution mode '%s'" % (mode,))
  187. args = args[2:]
  188. if "-m" not in args:
  189. args = ["-m", synapse_worker] + args
  190. jemallocpath = "/usr/lib/%s-linux-gnu/libjemalloc.so.2" % (platform.machine(),)
  191. if os.path.isfile(jemallocpath):
  192. environ["LD_PRELOAD"] = jemallocpath
  193. else:
  194. log("Could not find %s, will not use" % (jemallocpath,))
  195. # if there are no config files passed to synapse, try adding the default file
  196. if not any(p.startswith("--config-path") or p.startswith("-c") for p in args):
  197. config_dir = environ.get("SYNAPSE_CONFIG_DIR", "/data")
  198. config_path = environ.get(
  199. "SYNAPSE_CONFIG_PATH", config_dir + "/homeserver.yaml"
  200. )
  201. if not os.path.exists(config_path):
  202. if "SYNAPSE_SERVER_NAME" in environ:
  203. error(
  204. """\
  205. Config file '%s' does not exist.
  206. The synapse docker image no longer supports generating a config file on-the-fly
  207. based on environment variables. You can migrate to a static config file by
  208. running with 'migrate_config'. See the README for more details.
  209. """
  210. % (config_path,)
  211. )
  212. error(
  213. "Config file '%s' does not exist. You should either create a new "
  214. "config file by running with the `generate` argument (and then edit "
  215. "the resulting file before restarting) or specify the path to an "
  216. "existing config file with the SYNAPSE_CONFIG_PATH variable."
  217. % (config_path,)
  218. )
  219. args += ["--config-path", config_path]
  220. log("Starting synapse with args " + " ".join(args))
  221. args = [sys.executable] + args
  222. if ownership is not None:
  223. args = ["gosu", ownership] + args
  224. flush_buffers()
  225. os.execve("/usr/sbin/gosu", args, environ)
  226. else:
  227. flush_buffers()
  228. os.execve(sys.executable, args, environ)
  229. if __name__ == "__main__":
  230. main(sys.argv, os.environ)