start.py 9.1 KB

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