Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

219 lines
7.0KB

  1. defmodule Pleroma.Cluster do
  2. @moduledoc """
  3. Facilities for managing a cluster of slave VM's for federated testing.
  4. ## Spawning the federated cluster
  5. `spawn_cluster/1` spawns a map of slave nodes that are started
  6. within the running VM. During startup, the slave node is sent all configuration
  7. from the parent node, as well as all code. After receiving configuration and
  8. code, the slave then starts all applications currently running on the parent.
  9. The configuration passed to `spawn_cluster/1` overrides any parent application
  10. configuration for the provided OTP app and key. This is useful for customizing
  11. the Ecto database, Phoenix webserver ports, etc.
  12. For example, to start a single federated VM named ":federated1", with the
  13. Pleroma Endpoint running on port 4123, and with a database named
  14. "pleroma_test1", you would run:
  15. endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
  16. repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
  17. Pleroma.Cluster.spawn_cluster(%{
  18. :"federated1@127.0.0.1" => [
  19. {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test1")},
  20. {:pleroma, Pleroma.Web.Endpoint,
  21. Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
  22. ]
  23. })
  24. *Note*: application configuration for a given key is not merged,
  25. so any customization requires first fetching the existing values
  26. and merging yourself by providing the merged configuration,
  27. such as above with the endpoint config and repo config.
  28. ## Executing code within a remote node
  29. Use the `within/2` macro to execute code within the context of a remote
  30. federated node. The code block captures all local variable bindings from
  31. the parent's context and returns the result of the expression after executing
  32. it on the remote node. For example:
  33. import Pleroma.Cluster
  34. parent_value = 123
  35. result =
  36. within :"federated1@127.0.0.1" do
  37. {node(), parent_value}
  38. end
  39. assert result == {:"federated1@127.0.0.1, 123}
  40. *Note*: while local bindings are captured and available within the block,
  41. other parent contexts like required, aliased, or imported modules are not
  42. in scope. Those will need to be reimported/aliases/required within the block
  43. as `within/2` is a remote procedure call.
  44. """
  45. @extra_apps Pleroma.Mixfile.application()[:extra_applications]
  46. @doc """
  47. Spawns the default Pleroma federated cluster.
  48. Values before may be customized as needed for the test suite.
  49. """
  50. def spawn_default_cluster do
  51. endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
  52. repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
  53. spawn_cluster(%{
  54. :"federated1@127.0.0.1" => [
  55. {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated1")},
  56. {:pleroma, Pleroma.Web.Endpoint,
  57. Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
  58. ],
  59. :"federated2@127.0.0.1" => [
  60. {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated2")},
  61. {:pleroma, Pleroma.Web.Endpoint,
  62. Keyword.merge(endpoint_conf, http: [port: 4012], url: [port: 4012], server: true)}
  63. ]
  64. })
  65. end
  66. @doc """
  67. Spawns a configured map of federated nodes.
  68. See `Pleroma.Cluster` module documentation for details.
  69. """
  70. def spawn_cluster(node_configs) do
  71. # Turn node into a distributed node with the given long name
  72. :net_kernel.start([:"primary@127.0.0.1"])
  73. # Allow spawned nodes to fetch all code from this node
  74. {:ok, _} = :erl_boot_server.start([])
  75. allow_boot("127.0.0.1")
  76. silence_logger_warnings(fn ->
  77. node_configs
  78. |> Enum.map(&Task.async(fn -> start_slave(&1) end))
  79. |> Enum.map(&Task.await(&1, 90_000))
  80. end)
  81. end
  82. @doc """
  83. Executes block of code again remote node.
  84. See `Pleroma.Cluster` module documentation for details.
  85. """
  86. defmacro within(node, do: block) do
  87. quote do
  88. rpc(unquote(node), unquote(__MODULE__), :eval_quoted, [
  89. unquote(Macro.escape(block)),
  90. binding()
  91. ])
  92. end
  93. end
  94. @doc false
  95. def eval_quoted(block, binding) do
  96. {result, _binding} = Code.eval_quoted(block, binding, __ENV__)
  97. result
  98. end
  99. defp start_slave({node_host, override_configs}) do
  100. log(node_host, "booting federated VM")
  101. {:ok, node} = :slave.start(~c"127.0.0.1", node_name(node_host), vm_args())
  102. add_code_paths(node)
  103. load_apps_and_transfer_configuration(node, override_configs)
  104. ensure_apps_started(node)
  105. {:ok, node}
  106. end
  107. def rpc(node, module, function, args) do
  108. :rpc.block_call(node, module, function, args)
  109. end
  110. defp vm_args do
  111. ~c"-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}"
  112. end
  113. defp allow_boot(host) do
  114. {:ok, ipv4} = :inet.parse_ipv4_address(~c"#{host}")
  115. :ok = :erl_boot_server.add_slave(ipv4)
  116. end
  117. defp add_code_paths(node) do
  118. rpc(node, :code, :add_paths, [:code.get_path()])
  119. end
  120. defp load_apps_and_transfer_configuration(node, override_configs) do
  121. Enum.each(Application.loaded_applications(), fn {app_name, _, _} ->
  122. app_name
  123. |> Application.get_all_env()
  124. |> Enum.each(fn {key, primary_config} ->
  125. rpc(node, Application, :put_env, [app_name, key, primary_config, [persistent: true]])
  126. end)
  127. end)
  128. Enum.each(override_configs, fn {app_name, key, val} ->
  129. rpc(node, Application, :put_env, [app_name, key, val, [persistent: true]])
  130. end)
  131. end
  132. defp log(node, msg), do: IO.puts("[#{node}] #{msg}")
  133. defp ensure_apps_started(node) do
  134. loaded_names = Enum.map(Application.loaded_applications(), fn {name, _, _} -> name end)
  135. app_names = @extra_apps ++ (loaded_names -- @extra_apps)
  136. rpc(node, Application, :ensure_all_started, [:mix])
  137. rpc(node, Mix, :env, [Mix.env()])
  138. rpc(node, __MODULE__, :prepare_database, [])
  139. log(node, "starting application")
  140. Enum.reduce(app_names, MapSet.new(), fn app, loaded ->
  141. if Enum.member?(loaded, app) do
  142. loaded
  143. else
  144. {:ok, started} = rpc(node, Application, :ensure_all_started, [app])
  145. MapSet.union(loaded, MapSet.new(started))
  146. end
  147. end)
  148. end
  149. @doc false
  150. def prepare_database do
  151. log(node(), "preparing database")
  152. repo_config = Application.get_env(:pleroma, Pleroma.Repo)
  153. repo_config[:adapter].storage_down(repo_config)
  154. repo_config[:adapter].storage_up(repo_config)
  155. {:ok, _, _} =
  156. Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
  157. Ecto.Migrator.run(repo, :up, log: false, all: true)
  158. end)
  159. Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
  160. {:ok, _} = Application.ensure_all_started(:ex_machina)
  161. end
  162. defp silence_logger_warnings(func) do
  163. prev_level = Logger.level()
  164. Logger.configure(level: :error)
  165. res = func.()
  166. Logger.configure(level: prev_level)
  167. res
  168. end
  169. defp node_name(node_host) do
  170. node_host
  171. |> to_string()
  172. |> String.split("@")
  173. |> Enum.at(0)
  174. |> String.to_atom()
  175. end
  176. end