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