rpcsetup.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. #!/usr/bin/python3
  2. import json # noqa
  3. import re
  4. from dataclasses import dataclass
  5. from typing import Any, Mapping
  6. import click
  7. """Convenience script for setting up silos in a dev environment.
  8. Prints devserver commands with the necessary environment variables.
  9. """
  10. @dataclass(frozen=True)
  11. class RegionConfig:
  12. number: int
  13. port: int
  14. api_token: str
  15. @property
  16. def name(self) -> str:
  17. return f"devregion{self.number}"
  18. @property
  19. def bind(self) -> str:
  20. return f"localhost:{self.port}"
  21. def get_env_repr(self) -> Mapping[str, Any]:
  22. return {
  23. "name": self.name,
  24. "id": self.number,
  25. "address": f"http://{self.bind}/",
  26. "category": "MULTI_TENANT",
  27. "api_token": self.api_token,
  28. }
  29. def format_env_vars(env_vars: Mapping[str, str]) -> str:
  30. def format_env_var(name: str, value: str) -> str:
  31. value = re.sub("([\"$'\\\\])", "\\\\\\1", value) # https://xkcd.com/1638/
  32. return f'{name}="{value}"'
  33. return " ".join(format_env_var(name, value) for (name, value) in env_vars.items())
  34. @click.command()
  35. @click.option(
  36. "--api-token",
  37. type=str,
  38. required=True,
  39. help=(
  40. """An API token to authorize RPC requests between servers.
  41. At the time this script is being written, the authentication system for RPCs
  42. hasn't been developed. An API token is needed to ensure that an incoming
  43. request hits a REST endpoint, but it doesn't need to have any particular
  44. permission scope.
  45. Because dev instances of the control and region silo will presumably share a
  46. single database, this script assumes that the same API token will work for both.
  47. """
  48. ),
  49. )
  50. @click.option("--region-count", type=int, default=2, help="Number of region silos")
  51. @click.option(
  52. "--control-port",
  53. type=int,
  54. default=8000,
  55. help=(
  56. """Port on which to bind the control silo.
  57. Region silos will be bound on ascending numbers after this one in steps of 10.
  58. """
  59. ),
  60. )
  61. def main(api_token: str, region_count: int, control_port: int) -> None:
  62. sender_credentials = {
  63. "is_allowed": True,
  64. "control_silo_api_token": api_token,
  65. "control_silo_address": f"http://localhost:{control_port}/",
  66. }
  67. regions = [
  68. RegionConfig(n, control_port + 10 * n, api_token) for n in range(1, region_count + 1)
  69. ]
  70. region_config = json.dumps([r.get_env_repr() for r in regions])
  71. common_env_vars = {}
  72. common_env_vars["SENTRY_REGION_CONFIG"] = region_config
  73. common_env_vars["SENTRY_DEV_HYBRID_CLOUD_RPC_SENDER"] = json.dumps(sender_credentials)
  74. control_env_vars = common_env_vars.copy()
  75. control_env_vars["SENTRY_SILO_MODE"] = "CONTROL"
  76. control_env_vars["SENTRY_DEVSERVER_BIND"] = f"localhost:{control_port}"
  77. print(f"# Control silo\n{format_env_vars(control_env_vars)} sentry devserver") # noqa
  78. for region in regions:
  79. region_env_vars = common_env_vars.copy()
  80. region_env_vars["SENTRY_SILO_MODE"] = "REGION"
  81. region_env_vars["SENTRY_REGION"] = region.name
  82. region_env_vars["SENTRY_DEVSERVER_BIND"] = region.bind
  83. print(f"\n# {region.name}\n{format_env_vars(region_env_vars)} sentry devserver") # noqa
  84. main()