|
@@ -19,9 +19,17 @@
|
|
|
|
|
|
|
|
-include_lib("typerefl/include/types.hrl").
|
|
-include_lib("typerefl/include/types.hrl").
|
|
|
-include_lib("hocon/include/hoconsc.hrl").
|
|
-include_lib("hocon/include/hoconsc.hrl").
|
|
|
|
|
+-include_lib("emqx/include/logger.hrl").
|
|
|
|
|
|
|
|
-export([api_spec/0, fields/1, paths/0, schema/1, namespace/0]).
|
|
-export([api_spec/0, fields/1, paths/0, schema/1, namespace/0]).
|
|
|
--export([cluster_info/2, invite_node/2, force_leave/2, join/1]).
|
|
|
|
|
|
|
+-export([
|
|
|
|
|
+ cluster_info/2,
|
|
|
|
|
+ cluster_topology/2,
|
|
|
|
|
+ invite_node/2,
|
|
|
|
|
+ force_leave/2,
|
|
|
|
|
+ join/1,
|
|
|
|
|
+ connected_replicants/0
|
|
|
|
|
+]).
|
|
|
|
|
|
|
|
namespace() -> "cluster".
|
|
namespace() -> "cluster".
|
|
|
|
|
|
|
@@ -31,6 +39,7 @@ api_spec() ->
|
|
|
paths() ->
|
|
paths() ->
|
|
|
[
|
|
[
|
|
|
"/cluster",
|
|
"/cluster",
|
|
|
|
|
+ "/cluster/topology",
|
|
|
"/cluster/:node/invite",
|
|
"/cluster/:node/invite",
|
|
|
"/cluster/:node/force_leave"
|
|
"/cluster/:node/force_leave"
|
|
|
].
|
|
].
|
|
@@ -50,6 +59,17 @@ schema("/cluster") ->
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
+schema("/cluster/topology") ->
|
|
|
|
|
+ #{
|
|
|
|
|
+ 'operationId' => cluster_topology,
|
|
|
|
|
+ get => #{
|
|
|
|
|
+ desc => ?DESC(get_cluster_topology),
|
|
|
|
|
+ tags => [<<"Cluster">>],
|
|
|
|
|
+ responses => #{
|
|
|
|
|
+ 200 => ?HOCON(?ARRAY(?REF(core_replicants)), #{desc => <<"Cluster topology">>})
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
schema("/cluster/:node/invite") ->
|
|
schema("/cluster/:node/invite") ->
|
|
|
#{
|
|
#{
|
|
|
'operationId' => invite_node,
|
|
'operationId' => invite_node,
|
|
@@ -89,6 +109,28 @@ fields(node) ->
|
|
|
validator => fun validate_node/1
|
|
validator => fun validate_node/1
|
|
|
}
|
|
}
|
|
|
)}
|
|
)}
|
|
|
|
|
+ ];
|
|
|
|
|
+fields(replicant_info) ->
|
|
|
|
|
+ [
|
|
|
|
|
+ {node,
|
|
|
|
|
+ ?HOCON(
|
|
|
|
|
+ atom(),
|
|
|
|
|
+ #{desc => <<"Replicant node name">>, example => <<"emqx-replicant@127.0.0.2">>}
|
|
|
|
|
+ )},
|
|
|
|
|
+ {streams,
|
|
|
|
|
+ ?HOCON(
|
|
|
|
|
+ non_neg_integer(),
|
|
|
|
|
+ #{desc => <<"The number of RLOG (replicated log) streams">>, example => <<"10">>}
|
|
|
|
|
+ )}
|
|
|
|
|
+ ];
|
|
|
|
|
+fields(core_replicants) ->
|
|
|
|
|
+ [
|
|
|
|
|
+ {core_node,
|
|
|
|
|
+ ?HOCON(
|
|
|
|
|
+ atom(),
|
|
|
|
|
+ #{desc => <<"Core node name">>, example => <<"emqx-core@127.0.0.1">>}
|
|
|
|
|
+ )},
|
|
|
|
|
+ {replicant_nodes, ?HOCON(?ARRAY(?REF(replicant_info)))}
|
|
|
].
|
|
].
|
|
|
|
|
|
|
|
validate_node(Node) ->
|
|
validate_node(Node) ->
|
|
@@ -106,6 +148,46 @@ cluster_info(get, _) ->
|
|
|
},
|
|
},
|
|
|
{200, Info}.
|
|
{200, Info}.
|
|
|
|
|
|
|
|
|
|
+cluster_topology(get, _) ->
|
|
|
|
|
+ RunningCores = running_cores(),
|
|
|
|
|
+ {Replicants, BadNodes} = emqx_mgmt_cluster_proto_v2:connected_replicants(RunningCores),
|
|
|
|
|
+ CoreReplicants = lists:zip(
|
|
|
|
|
+ lists:filter(
|
|
|
|
|
+ fun(N) -> not lists:member(N, BadNodes) end,
|
|
|
|
|
+ RunningCores
|
|
|
|
|
+ ),
|
|
|
|
|
+ Replicants
|
|
|
|
|
+ ),
|
|
|
|
|
+ Topology = lists:map(
|
|
|
|
|
+ fun
|
|
|
|
|
+ ({Core, {badrpc, Reason}}) ->
|
|
|
|
|
+ ?SLOG(error, #{
|
|
|
|
|
+ msg => "failed_to_get_replicant_nodes",
|
|
|
|
|
+ core_node => Core,
|
|
|
|
|
+ reason => Reason
|
|
|
|
|
+ }),
|
|
|
|
|
+ #{core_node => Core, replicant_nodes => []};
|
|
|
|
|
+ ({Core, Repls}) ->
|
|
|
|
|
+ #{core_node => Core, replicant_nodes => format_replicants(Repls)}
|
|
|
|
|
+ end,
|
|
|
|
|
+ CoreReplicants
|
|
|
|
|
+ ),
|
|
|
|
|
+ BadNodes =/= [] andalso ?SLOG(error, #{msg => "rpc_call_failed", bad_nodes => BadNodes}),
|
|
|
|
|
+ {200, Topology}.
|
|
|
|
|
+
|
|
|
|
|
+format_replicants(Replicants) ->
|
|
|
|
|
+ maps:fold(
|
|
|
|
|
+ fun(K, V, Acc) ->
|
|
|
|
|
+ [#{node => K, streams => length(V)} | Acc]
|
|
|
|
|
+ end,
|
|
|
|
|
+ [],
|
|
|
|
|
+ maps:groups_from_list(fun({_, N, _}) -> N end, Replicants)
|
|
|
|
|
+ ).
|
|
|
|
|
+
|
|
|
|
|
+running_cores() ->
|
|
|
|
|
+ Running = emqx:running_nodes(),
|
|
|
|
|
+ lists:filter(fun(C) -> lists:member(C, Running) end, emqx:cluster_nodes(cores)).
|
|
|
|
|
+
|
|
|
invite_node(put, #{bindings := #{node := Node0}}) ->
|
|
invite_node(put, #{bindings := #{node := Node0}}) ->
|
|
|
Node = ekka_node:parse_name(binary_to_list(Node0)),
|
|
Node = ekka_node:parse_name(binary_to_list(Node0)),
|
|
|
case emqx_mgmt_cluster_proto_v1:invite_node(Node, node()) of
|
|
case emqx_mgmt_cluster_proto_v1:invite_node(Node, node()) of
|
|
@@ -134,5 +216,9 @@ force_leave(delete, #{bindings := #{node := Node0}}) ->
|
|
|
join(Node) ->
|
|
join(Node) ->
|
|
|
ekka:join(Node).
|
|
ekka:join(Node).
|
|
|
|
|
|
|
|
|
|
+-spec connected_replicants() -> [{atom(), node(), pid()}].
|
|
|
|
|
+connected_replicants() ->
|
|
|
|
|
+ mria_status:agents().
|
|
|
|
|
+
|
|
|
error_message(Msg) ->
|
|
error_message(Msg) ->
|
|
|
iolist_to_binary(io_lib:format("~p", [Msg])).
|
|
iolist_to_binary(io_lib:format("~p", [Msg])).
|