1 |
|
%%%------------------------------------------------------------------- |
2 |
|
%%% File : ejabberd_admin.erl |
3 |
|
%%% Author : Mickael Remond <mremond@process-one.net> |
4 |
|
%%% Purpose : Administrative functions and commands |
5 |
|
%%% Created : 7 May 2006 by Mickael Remond <mremond@process-one.net> |
6 |
|
%%% |
7 |
|
%%% |
8 |
|
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne |
9 |
|
%%% |
10 |
|
%%% This program is free software; you can redistribute it and/or |
11 |
|
%%% modify it under the terms of the GNU General Public License as |
12 |
|
%%% published by the Free Software Foundation; either version 2 of the |
13 |
|
%%% License, or (at your option) any later version. |
14 |
|
%%% |
15 |
|
%%% This program is distributed in the hope that it will be useful, |
16 |
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 |
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
18 |
|
%%% General Public License for more details. |
19 |
|
%%% |
20 |
|
%%% You should have received a copy of the GNU General Public License |
21 |
|
%%% along with this program; if not, write to the Free Software |
22 |
|
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
23 |
|
%%% |
24 |
|
%%%------------------------------------------------------------------- |
25 |
|
|
26 |
|
-module(ejabberd_admin). |
27 |
|
-author('mickael.remond@process-one.net'). |
28 |
|
|
29 |
|
-define(REGISTER_WORKERS_NUM, 10). |
30 |
|
|
31 |
|
-export([start/0, stop/0, |
32 |
|
%% Server |
33 |
|
status/0, |
34 |
|
%% Accounts |
35 |
|
register/3, register/2, unregister/2, |
36 |
|
registered_users/1, |
37 |
|
import_users/1, |
38 |
|
%% Purge DB |
39 |
|
delete_expired_messages/1, delete_old_messages/2, |
40 |
|
%% Mnesia |
41 |
|
set_master/1, |
42 |
|
backup_mnesia/1, restore_mnesia/1, |
43 |
|
dump_mnesia/1, dump_table/2, load_mnesia/1, |
44 |
|
install_fallback_mnesia/1, |
45 |
|
dump_to_textfile/1, dump_to_textfile/2, |
46 |
|
mnesia_change_nodename/4, |
47 |
|
restore/1, % Still used by some modules%% |
48 |
|
get_loglevel/0, |
49 |
|
join_cluster/1, leave_cluster/0, |
50 |
|
remove_from_cluster/1]). |
51 |
|
|
52 |
|
-export([registrator_proc/1]). |
53 |
|
|
54 |
|
-ignore_xref([ |
55 |
|
backup_mnesia/1, delete_expired_messages/1, delete_old_messages/2, |
56 |
|
dump_mnesia/1, dump_table/2, dump_to_textfile/1, dump_to_textfile/2, |
57 |
|
get_loglevel/0, import_users/1, install_fallback_mnesia/1, |
58 |
|
join_cluster/1, leave_cluster/0, load_mnesia/1, mnesia_change_nodename/4, |
59 |
|
register/2, register/3, registered_users/1, remove_from_cluster/1, |
60 |
|
restore_mnesia/1, set_master/1, status/0, |
61 |
|
stop/0, unregister/2]). |
62 |
|
|
63 |
|
-include("mongoose.hrl"). |
64 |
|
-include("ejabberd_commands.hrl"). |
65 |
|
|
66 |
|
start() -> |
67 |
83 |
ejabberd_commands:register_commands(commands()). |
68 |
|
|
69 |
|
stop() -> |
70 |
:-( |
ejabberd_commands:unregister_commands(commands()). |
71 |
|
|
72 |
|
%%% |
73 |
|
%%% ejabberd commands |
74 |
|
%%% |
75 |
|
|
76 |
|
-spec commands() -> [ejabberd_commands:cmd()]. |
77 |
|
commands() -> |
78 |
83 |
[ |
79 |
|
%% The commands status, stop and restart are implemented also in ejabberd_ctl |
80 |
|
%% They are defined here so that other interfaces can use them too |
81 |
|
#ejabberd_commands{name = status, tags = [server], |
82 |
|
desc = "Get status of the ejabberd server", |
83 |
|
module = ?MODULE, function = status, |
84 |
|
args = [], result = {res, restuple}}, |
85 |
|
#ejabberd_commands{name = restart, tags = [server], |
86 |
|
desc = "Restart ejabberd gracefully", |
87 |
|
module = init, function = restart, |
88 |
|
args = [], result = {res, rescode}}, |
89 |
|
#ejabberd_commands{name = get_loglevel, tags = [logs, server], |
90 |
|
desc = "Get the current loglevel", |
91 |
|
module = ?MODULE, function = get_loglevel, |
92 |
|
args = [], |
93 |
|
result = {res, restuple}}, |
94 |
|
#ejabberd_commands{name = register, tags = [accounts], |
95 |
|
desc = "Register a user", |
96 |
|
module = ?MODULE, function = register, |
97 |
|
args = [{host, binary}, {password, binary}], |
98 |
|
result = {res, restuple}}, |
99 |
|
#ejabberd_commands{name = register_identified, tags = [accounts], |
100 |
|
desc = "Register a user with a specific jid", |
101 |
|
module = ?MODULE, function = register, |
102 |
|
args = [{user, binary}, {host, binary}, {password, binary}], |
103 |
|
result = {res, restuple}}, |
104 |
|
#ejabberd_commands{name = unregister, tags = [accounts], |
105 |
|
desc = "Unregister a user", |
106 |
|
module = ?MODULE, function = unregister, |
107 |
|
args = [{user, binary}, {host, binary}], |
108 |
|
result = {res, restuple}}, |
109 |
|
#ejabberd_commands{name = registered_users, tags = [accounts], |
110 |
|
desc = "List all registered users in HOST", |
111 |
|
module = ?MODULE, function = registered_users, |
112 |
|
args = [{host, binary}], |
113 |
|
result = {users, {list, {user_jid, binary}}}}, |
114 |
|
#ejabberd_commands{name = import_users, tags = [accounts], |
115 |
|
desc = "Import users from CSV file", |
116 |
|
module = ?MODULE, function = import_users, |
117 |
|
args = [{file, string}], |
118 |
|
result = {users, {list, {res, {tuple, |
119 |
|
[{result, atom}, |
120 |
|
{user, binary}]}}}}}, |
121 |
|
#ejabberd_commands{name = delete_expired_messages, tags = [purge], |
122 |
|
desc = "Delete expired offline messages from database", |
123 |
|
module = ?MODULE, function = delete_expired_messages, |
124 |
|
args = [{host, binary}], result = {res, restuple}}, |
125 |
|
#ejabberd_commands{name = delete_old_messages, tags = [purge], |
126 |
|
desc = "Delete offline messages older than DAYS", |
127 |
|
module = ?MODULE, function = delete_old_messages, |
128 |
|
args = [{host, binary}, {days, integer}], result = {res, restuple}}, |
129 |
|
#ejabberd_commands{name = set_master, tags = [mnesia], |
130 |
|
desc = "Set master node of the clustered Mnesia tables", |
131 |
|
longdesc = "If you provide as nodename \"self\", this " |
132 |
|
"node will be set as its own master.", |
133 |
|
module = ?MODULE, function = set_master, |
134 |
|
args = [{nodename, string}], result = {res, restuple}}, |
135 |
|
#ejabberd_commands{name = mnesia_change_nodename, tags = [mnesia], |
136 |
|
desc = "Change the erlang node name in a backup file", |
137 |
|
module = ?MODULE, function = mnesia_change_nodename, |
138 |
|
args = [{oldnodename, string}, {newnodename, string}, |
139 |
|
{oldbackup, string}, {newbackup, string}], |
140 |
|
result = {res, restuple}}, |
141 |
|
#ejabberd_commands{name = backup, tags = [mnesia], |
142 |
|
desc = "Store the database to backup file (only Mnesia)", |
143 |
|
module = ?MODULE, function = backup_mnesia, |
144 |
|
args = [{file, string}], result = {res, restuple}}, |
145 |
|
#ejabberd_commands{name = restore, tags = [mnesia], |
146 |
|
desc = "Restore the database from backup file (only Mnesia)", |
147 |
|
module = ?MODULE, function = restore_mnesia, |
148 |
|
args = [{file, string}], result = {res, restuple}}, |
149 |
|
#ejabberd_commands{name = dump, tags = [mnesia], |
150 |
|
desc = "Dump the database to text file (only Mnesia)", |
151 |
|
module = ?MODULE, function = dump_mnesia, |
152 |
|
args = [{file, string}], result = {res, restuple}}, |
153 |
|
#ejabberd_commands{name = dump_table, tags = [mnesia], |
154 |
|
desc = "Dump a table to text file (only Mnesia)", |
155 |
|
module = ?MODULE, function = dump_table, |
156 |
|
args = [{file, string}, {table, string}], result = {res, restuple}}, |
157 |
|
#ejabberd_commands{name = load, tags = [mnesia], |
158 |
|
desc = "Restore the database from text file (only Mnesia)", |
159 |
|
module = ?MODULE, function = load_mnesia, |
160 |
|
args = [{file, string}], result = {res, restuple}}, |
161 |
|
#ejabberd_commands{name = install_fallback, tags = [mnesia], |
162 |
|
desc = "Install the database from a fallback file (only Mnesia)", |
163 |
|
module = ?MODULE, function = install_fallback_mnesia, |
164 |
|
args = [{file, string}], result = {res, restuple}}, |
165 |
|
#ejabberd_commands{name = join_cluster, tags = [server], |
166 |
|
desc = "Join the node to a cluster. Call it from the joining node. |
167 |
|
Use `-f` or `--force` flag to avoid question prompt and force join the node", |
168 |
|
module = ?MODULE, function = join_cluster, |
169 |
|
args = [{node, string}], |
170 |
|
result = {res, restuple}}, |
171 |
|
#ejabberd_commands{name = leave_cluster, tags = [server], |
172 |
|
desc = "Leave a cluster. Call it from the node that is going to leave. |
173 |
|
Use `-f` or `--force` flag to avoid question prompt and force leave the node from cluster", |
174 |
|
module = ?MODULE, function = leave_cluster, |
175 |
|
args = [], |
176 |
|
result = {res, restuple}}, |
177 |
|
#ejabberd_commands{name = remove_from_cluster, tags = [server], |
178 |
|
desc = "Remove dead node from the cluster. Call it from the member of the cluster. |
179 |
|
Use `-f` or `--force` flag to avoid question prompt and force remove the node", |
180 |
|
module = ?MODULE, function = remove_from_cluster, |
181 |
|
args = [{node, string}], |
182 |
|
result = {res, restuple}} |
183 |
|
]. |
184 |
|
|
185 |
|
|
186 |
|
%%% |
187 |
|
%%% Server management |
188 |
|
%%% |
189 |
|
-spec remove_from_cluster(string()) -> {ok, string()} | |
190 |
|
{node_is_alive, string()} | |
191 |
|
{mnesia_error, string()} | |
192 |
|
{rpc_error, string()}. |
193 |
|
remove_from_cluster(NodeString) -> |
194 |
1 |
Node = list_to_atom(NodeString), |
195 |
1 |
IsNodeAlive = mongoose_cluster:is_node_alive(Node), |
196 |
1 |
case IsNodeAlive of |
197 |
|
true -> |
198 |
1 |
remove_rpc_alive_node(Node); |
199 |
|
false -> |
200 |
:-( |
remove_dead_node(Node) |
201 |
|
end. |
202 |
|
|
203 |
|
remove_dead_node(DeadNode) -> |
204 |
:-( |
try mongoose_cluster:remove_from_cluster(DeadNode) of |
205 |
|
ok -> |
206 |
:-( |
String = io_lib:format("The dead node ~p has been removed from the cluster~n", [DeadNode]), |
207 |
:-( |
{ok, String} |
208 |
|
catch |
209 |
|
error:{node_is_alive, DeadNode} -> |
210 |
:-( |
String = io_lib:format("The node ~p is alive but shoud not be.~n", [DeadNode]), |
211 |
:-( |
{node_is_alive, String}; |
212 |
|
error:{del_table_copy_schema, R} -> |
213 |
:-( |
String = io_lib:format("Cannot delete table schema~n. Reason: ~p", [R]), |
214 |
:-( |
{mnesia_error, String} |
215 |
|
end. |
216 |
|
|
217 |
|
remove_rpc_alive_node(AliveNode) -> |
218 |
1 |
case rpc:call(AliveNode, mongoose_cluster, leave, []) of |
219 |
|
{badrpc, Reason} -> |
220 |
:-( |
String = io_lib:format("Cannot remove the node ~p~n. RPC Reason: ~p", [AliveNode, Reason]), |
221 |
:-( |
{rpc_error, String}; |
222 |
|
ok -> |
223 |
1 |
String = io_lib:format("The node ~p has been removed from the cluster~n", [AliveNode]), |
224 |
1 |
{ok, String}; |
225 |
|
Unknown -> |
226 |
:-( |
String = io_lib:format("Unknown error: ~p~n", [Unknown]), |
227 |
:-( |
{rpc_error, String} |
228 |
|
end. |
229 |
|
|
230 |
|
-spec join_cluster(string()) -> {ok, string()} | {pang, string()} | {already_joined, string()} | |
231 |
|
{mnesia_error, string()} | {error, string()}. |
232 |
|
join_cluster(NodeString) -> |
233 |
6 |
NodeAtom = list_to_atom(NodeString), |
234 |
6 |
NodeList = mnesia:system_info(db_nodes), |
235 |
6 |
case lists:member(NodeAtom, NodeList) of |
236 |
|
true -> |
237 |
1 |
String = io_lib:format("The node ~s has already joined the cluster~n", [NodeString]), |
238 |
1 |
{already_joined, String}; |
239 |
|
_ -> |
240 |
5 |
do_join_cluster(NodeAtom) |
241 |
|
end. |
242 |
|
|
243 |
|
do_join_cluster(Node) -> |
244 |
5 |
try mongoose_cluster:join(Node) of |
245 |
|
ok -> |
246 |
5 |
String = io_lib:format("You have successfully joined the node ~p to the cluster with node member ~p~n", [node(), Node]), |
247 |
5 |
{ok, String} |
248 |
|
catch |
249 |
|
error:pang -> |
250 |
:-( |
String = io_lib:format("Timeout while attempting to connect to node ~s~n", [Node]), |
251 |
:-( |
{pang, String}; |
252 |
|
error:{cant_get_storage_type, {T, E, R}} -> |
253 |
:-( |
String = io_lib:format("Cannot get storage type for table ~p~n. Reason: ~p:~p", [T, E, R]), |
254 |
:-( |
{mnesia_error, String}; |
255 |
|
E:R:S -> |
256 |
:-( |
{error, {E, R, S}} |
257 |
|
end. |
258 |
|
|
259 |
|
-spec leave_cluster() -> {ok, string()} | {error, term()} | {not_in_cluster, string()}. |
260 |
|
leave_cluster() -> |
261 |
8 |
NodeList = mnesia:system_info(running_db_nodes), |
262 |
8 |
ThisNode = node(), |
263 |
8 |
case NodeList of |
264 |
|
[ThisNode] -> |
265 |
2 |
String = io_lib:format("The node ~p is not in the cluster~n", [node()]), |
266 |
2 |
{not_in_cluster, String}; |
267 |
|
_ -> |
268 |
6 |
do_leave_cluster() |
269 |
|
end. |
270 |
|
|
271 |
|
do_leave_cluster() -> |
272 |
6 |
try mongoose_cluster:leave() of |
273 |
|
ok -> |
274 |
6 |
String = io_lib:format("The node ~p has successfully left the cluster~n", [node()]), |
275 |
6 |
{ok, String} |
276 |
|
catch |
277 |
|
E:R -> |
278 |
:-( |
{error, {E, R}} |
279 |
|
end. |
280 |
|
|
281 |
|
-spec status() -> {'mongooseim_not_running', io_lib:chars()} | {'ok', io_lib:chars()}. |
282 |
|
status() -> |
283 |
:-( |
{InternalStatus, ProvidedStatus} = init:get_status(), |
284 |
:-( |
String1 = io_lib:format("The node ~p is ~p. Status: ~p", |
285 |
|
[node(), InternalStatus, ProvidedStatus]), |
286 |
:-( |
{Is_running, String2} = |
287 |
|
case lists:keysearch(mongooseim, 1, application:which_applications()) of |
288 |
|
false -> |
289 |
:-( |
{mongooseim_not_running, "mongooseim is not running in that node."}; |
290 |
|
{value, {_, _, Version}} -> |
291 |
:-( |
{ok, io_lib:format("mongooseim ~s is running in that node", [Version])} |
292 |
|
end, |
293 |
:-( |
{Is_running, String1 ++ String2}. |
294 |
|
|
295 |
|
%%% |
296 |
|
%%% Account management |
297 |
|
%%% |
298 |
|
|
299 |
|
-spec register(Host :: jid:server(), |
300 |
|
Password :: binary()) -> mongoose_account_api:register_result(). |
301 |
|
register(Host, Password) -> |
302 |
1 |
{Result, _} = mongoose_account_api:register_generated_user(Host, Password), |
303 |
1 |
Result. |
304 |
|
|
305 |
|
-spec register(User :: jid:user(), |
306 |
|
Host :: jid:server(), |
307 |
|
Password :: binary()) -> mongoose_account_api:register_result(). |
308 |
|
register(User, Host, Password) -> |
309 |
2817 |
mongoose_account_api:register_user(User, Host, Password). |
310 |
|
|
311 |
|
-spec unregister(User :: jid:user(), |
312 |
|
Host :: jid:server()) -> mongoose_account_api:unregister_result(). |
313 |
|
unregister(User, Host) -> |
314 |
2847 |
mongoose_account_api:unregister_user(User, Host). |
315 |
|
|
316 |
|
|
317 |
|
-spec registered_users(Host :: jid:server()) -> [binary()]. |
318 |
|
registered_users(Host) -> |
319 |
3 |
mongoose_account_api:list_users(Host). |
320 |
|
|
321 |
|
-spec import_users(Filename :: string()) -> [{ok, jid:user()} | |
322 |
|
{exists, jid:user()} | |
323 |
|
{not_allowed, jid:user()} | |
324 |
|
{invalid_jid, jid:user()} | |
325 |
|
{null_password, jid:user()} | |
326 |
|
{bad_csv, binary()}]. |
327 |
|
import_users(File) -> |
328 |
:-( |
{ok, CsvStream} = erl_csv:decode_new_s(File), |
329 |
:-( |
Workers = spawn_link_workers(), |
330 |
:-( |
WorkersQueue = queue:from_list(Workers), |
331 |
:-( |
do_import(CsvStream, WorkersQueue). |
332 |
|
|
333 |
|
-spec do_import(erl_csv:csv_stream(), Workers :: queue:queue()) -> |
334 |
|
[{ok, jid:user()} | |
335 |
|
{exists, jid:user()} | |
336 |
|
{not_allowed, jid:user()} | |
337 |
|
{invalid_jid, jid:user()} | |
338 |
|
{null_password, jid:user()} | |
339 |
|
{bad_csv, binary()}]. |
340 |
|
do_import(stream_end, WQueue) -> |
341 |
:-( |
Workers = queue:to_list(WQueue), |
342 |
:-( |
lists:flatmap(fun get_results_from_registrator/1, Workers); |
343 |
|
do_import(Stream, WQueue) -> |
344 |
:-( |
{ok, Decoded, MoreStream} = erl_csv:decode_s(Stream), |
345 |
:-( |
WQueue1 = send_job_to_next_worker(Decoded, WQueue), |
346 |
:-( |
do_import(MoreStream, WQueue1). |
347 |
|
|
348 |
|
-spec spawn_link_workers() -> [pid()]. |
349 |
|
spawn_link_workers() -> |
350 |
:-( |
[ spawn_link(?MODULE, registrator_proc, [self()]) || |
351 |
:-( |
_ <- lists:seq(1, ?REGISTER_WORKERS_NUM)]. |
352 |
|
|
353 |
|
-spec get_results_from_registrator(Worker :: pid()) -> |
354 |
|
[{ok, jid:user()} | |
355 |
|
{exists, jid:user()} | |
356 |
|
{not_allowed, jid:user()} | |
357 |
|
{invalid_jid, jid:user()} | |
358 |
|
{null_password, jid:user()} | |
359 |
|
{bad_csv, binary()}]. |
360 |
|
get_results_from_registrator(Pid) -> |
361 |
:-( |
Pid ! get_result, |
362 |
:-( |
receive |
363 |
:-( |
{result, Result} -> Result |
364 |
|
end. |
365 |
|
|
366 |
|
send_job_to_next_worker([], WQueue) -> |
367 |
:-( |
WQueue; |
368 |
|
send_job_to_next_worker([Record], WQueue) -> |
369 |
:-( |
{{value, Worker}, Q1} = queue:out(WQueue), |
370 |
:-( |
Worker ! {proccess, Record}, |
371 |
:-( |
queue:in(Worker, Q1). |
372 |
|
|
373 |
|
-spec registrator_proc(Manager :: pid()) -> ok. |
374 |
|
registrator_proc(Manager) -> |
375 |
:-( |
registrator_proc(Manager, []). |
376 |
|
|
377 |
|
-spec registrator_proc(Manager :: pid(), any()) -> ok. |
378 |
|
registrator_proc(Manager, Result) -> |
379 |
:-( |
receive |
380 |
|
{proccess, Data} -> |
381 |
:-( |
RegisterResult = do_register(Data), |
382 |
:-( |
registrator_proc(Manager, [RegisterResult | Result]); |
383 |
:-( |
get_result -> Manager ! {result, Result} |
384 |
|
end, |
385 |
:-( |
ok. |
386 |
|
|
387 |
|
-spec do_register([binary()]) -> {ok, jid:user()} | |
388 |
|
{exists, jid:user()} | |
389 |
|
{not_allowed, jid:user()} | |
390 |
|
{invalid_jid, jid:user()} | |
391 |
|
{null_password, jid:user()} | |
392 |
|
{bad_csv, binary()}. |
393 |
|
do_register([User, Host, Password]) -> |
394 |
:-( |
case ejabberd_auth:try_register(jid:make(User, Host, <<>>), Password) of |
395 |
:-( |
{error, Reason} -> {Reason, User}; |
396 |
:-( |
_ -> {ok, User} |
397 |
|
end; |
398 |
|
|
399 |
|
do_register(List) -> |
400 |
:-( |
JoinBinary = fun(Elem, <<"">>) -> Elem; |
401 |
:-( |
(Elem, Acc) -> <<Elem/binary, ",", Acc/binary>> |
402 |
|
end, |
403 |
:-( |
Info = lists:foldr(JoinBinary, <<"">>, List), |
404 |
:-( |
{bad_csv, Info}. |
405 |
|
|
406 |
|
get_loglevel() -> |
407 |
1 |
Level = mongoose_logs:get_global_loglevel(), |
408 |
1 |
Number = mongoose_logs:loglevel_keyword_to_number(Level), |
409 |
1 |
String = io_lib:format("global loglevel is ~p, which means '~p'", [Number, Level]), |
410 |
1 |
{ok, String}. |
411 |
|
|
412 |
|
%%% |
413 |
|
%%% Purge DB |
414 |
|
%%% |
415 |
|
|
416 |
|
-spec delete_expired_messages(jid:lserver()) -> {ok, iolist()} | {error, iolist()}. |
417 |
|
delete_expired_messages(Domain) -> |
418 |
1 |
case mod_offline_api:delete_expired_messages(Domain) of |
419 |
1 |
{ok, _} = Result -> Result; |
420 |
:-( |
{_, Message} -> {error, Message} |
421 |
|
end. |
422 |
|
|
423 |
|
-spec delete_old_messages(jid:lserver(), Days :: integer()) -> {ok, iolist()} | {error, iolist()}. |
424 |
|
delete_old_messages(Domain, Days) -> |
425 |
1 |
case mod_offline_api:delete_old_messages(Domain, Days) of |
426 |
1 |
{ok, _} = Result -> Result; |
427 |
:-( |
{_, Message} -> {error, Message} |
428 |
|
end. |
429 |
|
|
430 |
|
%%% |
431 |
|
%%% Mnesia management |
432 |
|
%%% |
433 |
|
|
434 |
|
-spec set_master(Node :: atom() | string()) -> {'error', io_lib:chars()} | {'ok', []}. |
435 |
|
set_master("self") -> |
436 |
2 |
set_master(node()); |
437 |
|
set_master(NodeString) when is_list(NodeString) -> |
438 |
1 |
set_master(list_to_atom(NodeString)); |
439 |
|
set_master(Node) when is_atom(Node) -> |
440 |
3 |
case mnesia:set_master_nodes([Node]) of |
441 |
|
ok -> |
442 |
3 |
{ok, ""}; |
443 |
|
{error, Reason} -> |
444 |
:-( |
String = io_lib:format("Can't set master node ~p at node ~p:~n~p", |
445 |
|
[Node, node(), Reason]), |
446 |
:-( |
{error, String} |
447 |
|
end. |
448 |
|
|
449 |
|
-spec backup_mnesia(file:name()) -> {'cannot_backup', io_lib:chars()} | {'ok', []}. |
450 |
|
backup_mnesia(Path) -> |
451 |
1 |
case mnesia:backup(Path) of |
452 |
|
ok -> |
453 |
1 |
{ok, ""}; |
454 |
|
{error, Reason} -> |
455 |
:-( |
String = io_lib:format("Can't store backup in ~p at node ~p: ~p", |
456 |
|
[filename:absname(Path), node(), Reason]), |
457 |
:-( |
{cannot_backup, String} |
458 |
|
end. |
459 |
|
|
460 |
|
-spec restore_mnesia(file:name()) -> {'cannot_restore', io_lib:chars()} |
461 |
|
| {'file_not_found', io_lib:chars()} |
462 |
|
| {'ok', []} |
463 |
|
| {'table_not_exists', io_lib:chars()}. |
464 |
|
restore_mnesia(Path) -> |
465 |
3 |
ErrorString=lists:flatten( io_lib:format("Can't restore backup from ~p at node ~p: ", |
466 |
|
[filename:absname(Path), node()])), |
467 |
3 |
case ejabberd_admin:restore(Path) of |
468 |
|
{atomic, _} -> |
469 |
1 |
{ok, ""}; |
470 |
|
{aborted, {no_exists, Table}} -> |
471 |
:-( |
String = io_lib:format("~sTable ~p does not exist.", [ErrorString, Table]), |
472 |
:-( |
{table_not_exists, String}; |
473 |
|
{aborted, enoent} -> |
474 |
2 |
String = ErrorString ++ "File not found.", |
475 |
2 |
{file_not_found, String}; |
476 |
|
{aborted, Reason} -> |
477 |
:-( |
String = io_lib:format("~s~p", [ErrorString, Reason]), |
478 |
:-( |
{cannot_restore, String} |
479 |
|
end. |
480 |
|
|
481 |
|
%% @doc Mnesia database restore |
482 |
|
%% This function is called from ejabberd_ctl, ejabberd_web_admin and |
483 |
|
%% mod_configure/adhoc |
484 |
|
restore(Path) -> |
485 |
3 |
mnesia:restore(Path, [{keep_tables, keep_tables()}, |
486 |
|
{default_op, skip_tables}]). |
487 |
|
|
488 |
|
%% @doc This function return a list of tables that should be kept from a |
489 |
|
%% previous version backup. |
490 |
|
%% Obsolete tables or tables created by module who are no longer used are not |
491 |
|
%% restored and are ignored. |
492 |
|
-spec keep_tables() -> [atom()]. |
493 |
|
keep_tables() -> |
494 |
3 |
lists:flatten([acl, passwd, disco_publish, keep_modules_tables()]). |
495 |
|
|
496 |
|
%% @doc Returns the list of modules tables in use, according to the list of |
497 |
|
%% actually loaded modules |
498 |
|
-spec keep_modules_tables() -> [[atom()]]. % list of lists |
499 |
|
keep_modules_tables() -> |
500 |
3 |
lists:map(fun(Module) -> module_tables(Module) end, |
501 |
|
gen_mod:loaded_modules()). |
502 |
|
|
503 |
|
%% TODO: This mapping should probably be moved to a callback function in each module. |
504 |
|
%% @doc Mapping between modules and their tables |
505 |
|
-spec module_tables(_) -> [atom()]. |
506 |
:-( |
module_tables(mod_announce) -> [motd, motd_users]; |
507 |
:-( |
module_tables(mod_irc) -> [irc_custom]; |
508 |
3 |
module_tables(mod_last) -> [last_activity]; |
509 |
:-( |
module_tables(mod_muc) -> [muc_room, muc_registered]; |
510 |
3 |
module_tables(mod_offline) -> [offline_msg]; |
511 |
:-( |
module_tables(mod_privacy) -> [privacy]; |
512 |
:-( |
module_tables(mod_private) -> [private_storage]; |
513 |
:-( |
module_tables(mod_pubsub) -> [pubsub_node]; |
514 |
3 |
module_tables(mod_roster) -> [roster]; |
515 |
:-( |
module_tables(mod_shared_roster) -> [sr_group, sr_user]; |
516 |
3 |
module_tables(mod_vcard) -> [vcard, vcard_search]; |
517 |
36 |
module_tables(_Other) -> []. |
518 |
|
|
519 |
|
-spec get_local_tables() -> [any()]. |
520 |
|
get_local_tables() -> |
521 |
1 |
Tabs1 = lists:delete(schema, mnesia:system_info(local_tables)), |
522 |
1 |
Tabs = lists:filter( |
523 |
|
fun(T) -> |
524 |
36 |
case mnesia:table_info(T, storage_type) of |
525 |
17 |
disc_copies -> true; |
526 |
5 |
disc_only_copies -> true; |
527 |
14 |
_ -> false |
528 |
|
end |
529 |
|
end, Tabs1), |
530 |
1 |
Tabs. |
531 |
|
|
532 |
|
-spec dump_mnesia(file:name()) -> {'cannot_dump', io_lib:chars()} | {'ok', []}. |
533 |
|
dump_mnesia(Path) -> |
534 |
1 |
Tabs = get_local_tables(), |
535 |
1 |
dump_tables(Path, Tabs). |
536 |
|
|
537 |
|
-spec dump_table(file:name(), STable :: string()) -> |
538 |
|
{'cannot_dump', io_lib:chars()} | {'ok', []}. |
539 |
|
dump_table(Path, STable) -> |
540 |
1 |
Table = list_to_atom(STable), |
541 |
1 |
dump_tables(Path, [Table]). |
542 |
|
|
543 |
|
-spec dump_tables(file:name(), Tables :: [atom()]) -> |
544 |
|
{'cannot_dump', io_lib:chars()} | {'ok', []}. |
545 |
|
dump_tables(Path, Tables) -> |
546 |
2 |
case dump_to_textfile(Path, Tables) of |
547 |
|
ok -> |
548 |
2 |
{ok, ""}; |
549 |
|
{error, Reason} -> |
550 |
:-( |
String = io_lib:format("Can't store dump in ~p at node ~p: ~p", |
551 |
|
[filename:absname(Path), node(), Reason]), |
552 |
:-( |
{cannot_dump, String} |
553 |
|
end. |
554 |
|
|
555 |
|
-spec dump_to_textfile(file:name()) -> 'ok' | {'error', atom()}. |
556 |
|
dump_to_textfile(File) -> |
557 |
:-( |
Tabs = get_local_tables(), |
558 |
:-( |
dump_to_textfile(File, Tabs). |
559 |
|
|
560 |
|
-spec dump_to_textfile(file:name(), Tabs :: list()) -> 'ok' | {'error', atom()}. |
561 |
|
dump_to_textfile(File, Tabs) -> |
562 |
2 |
dump_to_textfile(mnesia:system_info(is_running), Tabs, file:open(File, [write])). |
563 |
|
|
564 |
|
-spec dump_to_textfile(any(), |
565 |
|
any(), |
566 |
|
{'error', atom()} | {'ok', pid() | {'file_descriptor', atom() | tuple(), _}} |
567 |
|
) -> 'ok' | {'error', atom()}. |
568 |
|
dump_to_textfile(yes, Tabs, {ok, F}) -> |
569 |
2 |
Defs = lists:map( |
570 |
23 |
fun(T) -> {T, [{record_name, mnesia:table_info(T, record_name)}, |
571 |
|
{attributes, mnesia:table_info(T, attributes)}]} |
572 |
|
end, |
573 |
|
Tabs), |
574 |
2 |
io:format(F, "~p.~n", [{tables, Defs}]), |
575 |
2 |
lists:foreach(fun(T) -> dump_tab(F, T) end, Tabs), |
576 |
2 |
file:close(F); |
577 |
|
dump_to_textfile(_, _, {ok, F}) -> |
578 |
:-( |
file:close(F), |
579 |
:-( |
{error, mnesia_not_running}; |
580 |
|
dump_to_textfile(_, _, {error, Reason}) -> |
581 |
:-( |
{error, Reason}. |
582 |
|
|
583 |
|
-spec dump_tab(pid(), atom()) -> 'ok'. |
584 |
|
dump_tab(F, T) -> |
585 |
23 |
W = mnesia:table_info(T, wild_pattern), |
586 |
23 |
{atomic, All} = mnesia:transaction( |
587 |
23 |
fun() -> mnesia:match_object(T, W, read) end), |
588 |
23 |
lists:foreach( |
589 |
84 |
fun(Term) -> io:format(F, "~p.~n", [setelement(1, Term, T)]) end, All). |
590 |
|
|
591 |
|
-spec load_mnesia(file:name()) -> {'cannot_load', io_lib:chars()} | {'ok', []}. |
592 |
|
load_mnesia(Path) -> |
593 |
2 |
case mnesia:load_textfile(Path) of |
594 |
|
{atomic, ok} -> |
595 |
2 |
{ok, ""}; |
596 |
|
{error, Reason} -> |
597 |
:-( |
String = io_lib:format("Can't load dump in ~p at node ~p: ~p", |
598 |
|
[filename:absname(Path), node(), Reason]), |
599 |
:-( |
{cannot_load, String} |
600 |
|
end. |
601 |
|
|
602 |
|
-spec install_fallback_mnesia(file:name()) -> |
603 |
|
{'cannot_fallback', io_lib:chars()} | {'ok', []}. |
604 |
|
install_fallback_mnesia(Path) -> |
605 |
:-( |
case mnesia:install_fallback(Path) of |
606 |
|
ok -> |
607 |
:-( |
{ok, ""}; |
608 |
|
{error, Reason} -> |
609 |
:-( |
String = io_lib:format("Can't install fallback from ~p at node ~p: ~p", |
610 |
|
[filename:absname(Path), node(), Reason]), |
611 |
:-( |
{cannot_fallback, String} |
612 |
|
end. |
613 |
|
|
614 |
|
-spec mnesia_change_nodename(string(), string(), _, _) -> {ok, _} | {error, _}. |
615 |
|
mnesia_change_nodename(FromString, ToString, Source, Target) -> |
616 |
:-( |
From = list_to_atom(FromString), |
617 |
:-( |
To = list_to_atom(ToString), |
618 |
:-( |
Switch = |
619 |
|
fun |
620 |
|
(Node) when Node == From -> |
621 |
:-( |
io:format(" - Replacing nodename: '~p' with: '~p'~n", [From, To]), |
622 |
:-( |
To; |
623 |
|
(Node) when Node == To -> |
624 |
|
%% throw({error, already_exists}); |
625 |
:-( |
io:format(" - Node: '~p' will not be modified (it is already '~p')~n", [Node, To]), |
626 |
:-( |
Node; |
627 |
|
(Node) -> |
628 |
:-( |
io:format(" - Node: '~p' will not be modified (it is not '~p')~n", [Node, From]), |
629 |
:-( |
Node |
630 |
|
end, |
631 |
:-( |
Convert = |
632 |
|
fun |
633 |
|
({schema, db_nodes, Nodes}, Acc) -> |
634 |
:-( |
io:format(" +++ db_nodes ~p~n", [Nodes]), |
635 |
:-( |
{[{schema, db_nodes, lists:map(Switch, Nodes)}], Acc}; |
636 |
|
({schema, version, Version}, Acc) -> |
637 |
:-( |
io:format(" +++ version: ~p~n", [Version]), |
638 |
:-( |
{[{schema, version, Version}], Acc}; |
639 |
|
({schema, cookie, Cookie}, Acc) -> |
640 |
:-( |
io:format(" +++ cookie: ~p~n", [Cookie]), |
641 |
:-( |
{[{schema, cookie, Cookie}], Acc}; |
642 |
|
({schema, Tab, CreateList}, Acc) -> |
643 |
:-( |
io:format("~n * Checking table: '~p'~n", [Tab]), |
644 |
:-( |
Keys = [ram_copies, disc_copies, disc_only_copies], |
645 |
:-( |
OptSwitch = |
646 |
|
fun({Key, Val}) -> |
647 |
:-( |
case lists:member(Key, Keys) of |
648 |
|
true -> |
649 |
:-( |
io:format(" + Checking key: '~p'~n", [Key]), |
650 |
:-( |
{Key, lists:map(Switch, Val)}; |
651 |
:-( |
false-> {Key, Val} |
652 |
|
end |
653 |
|
end, |
654 |
:-( |
Res = {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc}, |
655 |
:-( |
Res; |
656 |
|
(Other, Acc) -> |
657 |
:-( |
{[Other], Acc} |
658 |
|
end, |
659 |
:-( |
mnesia:traverse_backup(Source, Target, Convert, switched). |