1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Cluster do
7 Facilities for managing a cluster of slave VM's for federated testing.
9 ## Spawning the federated cluster
11 `spawn_cluster/1` spawns a map of slave nodes that are started
12 within the running VM. During startup, the slave node is sent all configuration
13 from the parent node, as well as all code. After receiving configuration and
14 code, the slave then starts all applications currently running on the parent.
15 The configuration passed to `spawn_cluster/1` overrides any parent application
16 configuration for the provided OTP app and key. This is useful for customizing
17 the Ecto database, Phoenix webserver ports, etc.
19 For example, to start a single federated VM named ":federated1", with the
20 Pleroma Endpoint running on port 4123, and with a database named
21 "pleroma_test1", you would run:
23 endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
24 repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
26 Pleroma.Cluster.spawn_cluster(%{
27 :"federated1@127.0.0.1" => [
28 {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test1")},
29 {:pleroma, Pleroma.Web.Endpoint,
30 Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
34 *Note*: application configuration for a given key is not merged,
35 so any customization requires first fetching the existing values
36 and merging yourself by providing the merged configuration,
37 such as above with the endpoint config and repo config.
39 ## Executing code within a remote node
41 Use the `within/2` macro to execute code within the context of a remote
42 federated node. The code block captures all local variable bindings from
43 the parent's context and returns the result of the expression after executing
44 it on the remote node. For example:
46 import Pleroma.Cluster
51 within :"federated1@127.0.0.1" do
52 {node(), parent_value}
55 assert result == {:"federated1@127.0.0.1, 123}
57 *Note*: while local bindings are captured and available within the block,
58 other parent contexts like required, aliased, or imported modules are not
59 in scope. Those will need to be reimported/aliases/required within the block
60 as `within/2` is a remote procedure call.
63 @extra_apps Pleroma.Mixfile.application()[:extra_applications]
66 Spawns the default Pleroma federated cluster.
68 Values before may be customized as needed for the test suite.
70 def spawn_default_cluster do
71 endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
72 repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
75 :"federated1@127.0.0.1" => [
76 {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated1")},
77 {:pleroma, Pleroma.Web.Endpoint,
78 Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
80 :"federated2@127.0.0.1" => [
81 {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated2")},
82 {:pleroma, Pleroma.Web.Endpoint,
83 Keyword.merge(endpoint_conf, http: [port: 4012], url: [port: 4012], server: true)}
89 Spawns a configured map of federated nodes.
91 See `Pleroma.Cluster` module documentation for details.
93 def spawn_cluster(node_configs) do
94 # Turn node into a distributed node with the given long name
95 :net_kernel.start([:"primary@127.0.0.1"])
97 # Allow spawned nodes to fetch all code from this node
98 {:ok, _} = :erl_boot_server.start([])
99 allow_boot("127.0.0.1")
101 silence_logger_warnings(fn ->
103 |> Enum.map(&Task.async(fn -> start_slave(&1) end))
104 |> Enum.map(&Task.await(&1, 90_000))
109 Executes block of code again remote node.
111 See `Pleroma.Cluster` module documentation for details.
113 defmacro within(node, do: block) do
115 rpc(unquote(node), unquote(__MODULE__), :eval_quoted, [
116 unquote(Macro.escape(block)),
123 def eval_quoted(block, binding) do
124 {result, _binding} = Code.eval_quoted(block, binding, __ENV__)
128 defp start_slave({node_host, override_configs}) do
129 log(node_host, "booting federated VM")
132 do_start_slave(%{host: "127.0.0.1", name: node_name(node_host), args: vm_args()})
135 load_apps_and_transfer_configuration(node, override_configs)
136 ensure_apps_started(node)
140 def rpc(node, module, function, args) do
141 :rpc.block_call(node, module, function, args)
145 ~c"-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}"
148 defp allow_boot(host) do
149 {:ok, ipv4} = :inet.parse_ipv4_address(~c"#{host}")
150 :ok = :erl_boot_server.add_slave(ipv4)
153 defp add_code_paths(node) do
154 rpc(node, :code, :add_paths, [:code.get_path()])
157 defp load_apps_and_transfer_configuration(node, override_configs) do
158 Enum.each(Application.loaded_applications(), fn {app_name, _, _} ->
160 |> Application.get_all_env()
161 |> Enum.each(fn {key, primary_config} ->
162 rpc(node, Application, :put_env, [app_name, key, primary_config, [persistent: true]])
166 Enum.each(override_configs, fn {app_name, key, val} ->
167 rpc(node, Application, :put_env, [app_name, key, val, [persistent: true]])
171 defp log(node, msg), do: IO.puts("[#{node}] #{msg}")
173 defp ensure_apps_started(node) do
174 loaded_names = Enum.map(Application.loaded_applications(), fn {name, _, _} -> name end)
175 app_names = @extra_apps ++ (loaded_names -- @extra_apps)
177 rpc(node, Application, :ensure_all_started, [:mix])
178 rpc(node, Mix, :env, [Mix.env()])
179 rpc(node, __MODULE__, :prepare_database, [])
181 log(node, "starting application")
183 Enum.reduce(app_names, MapSet.new(), fn app, loaded ->
184 if Enum.member?(loaded, app) do
187 {:ok, started} = rpc(node, Application, :ensure_all_started, [app])
188 MapSet.union(loaded, MapSet.new(started))
194 def prepare_database do
195 log(node(), "preparing database")
196 repo_config = Application.get_env(:pleroma, Pleroma.Repo)
197 repo_config[:adapter].storage_down(repo_config)
198 repo_config[:adapter].storage_up(repo_config)
201 Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
202 Ecto.Migrator.run(repo, :up, log: false, all: true)
205 Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
206 {:ok, _} = Application.ensure_all_started(:ex_machina)
209 defp silence_logger_warnings(func) do
210 prev_level = Logger.level()
211 Logger.configure(level: :error)
213 Logger.configure(level: prev_level)
218 defp node_name(node_host) do
226 defp do_start_slave(%{host: host, name: name, args: args} = opts) do
227 peer_module = Application.get_env(__MODULE__, :peer_module)
229 if peer_module == :peer do
230 peer_module.start(opts)
232 peer_module.start(host, name, args)