27
28 -module(ejabberd_admin).
29
30 -author('[email protected]').
31
32 -export([start/0, stop/0, status/0, reopen_log/0, stop_kindly/2,
33 send_service_message_all_mucs/2, update_list/0, update/1, register/3,
34 unregister/2, registered_users/1, get_last_info/0, get_last_info/2,
35 import_file/1, import_dir/1, delete_expired_messages/0, delete_old_messages/1,
36 set_master/1, backup_mnesia/1, restore_mnesia/1, dump_mnesia/1, dump_table/2,
37 load_mnesia/1, install_fallback_mnesia/1, dump_to_textfile/1, dump_to_textfile/2,
38 mnesia_change_nodename/4, restore/1]).
39
40
42
43
45
46
48
49
51
52
54
55
57
58
60
61 -include("ejabberd.hrl").
62
63 -include("ejabberd_commands.hrl").
64
65 start() -> ejabberd_commands:register_commands(commands()).
66
67 stop() -> ejabberd_commands:unregister_commands(commands()).
68
69
73
74 commands() ->
75 [
76
77 #ejabberd_commands{name = status, tags = [server],
78 desc = "Get status of the ejabberd server", module = ?MODULE,
79 function = status, args = [], result = {res, restuple}},
80 #ejabberd_commands{name = stop, tags = [server], desc = "Stop ejabberd gracefully",
81 module = init, function = stop, args = [],
82 result = {res, rescode}},
83 #ejabberd_commands{name = restart, tags = [server],
84 desc = "Restart ejabberd gracefully", module = init,
85 function = restart, args = [], result = {res, rescode}},
86 #ejabberd_commands{name = reopen_log, tags = [logs, server],
87 desc = "Reopen the log files", module = ?MODULE,
88 function = reopen_log, args = [], result = {res, rescode}},
89 #ejabberd_commands{name = stop_kindly, tags = [server],
90 desc = "Inform users and rooms, wait, and stop the server",
91 module = ?MODULE, function = stop_kindly,
92 args = [{delay, integer}, {announcement, string}],
93 result = {res, rescode}},
94 #ejabberd_commands{name = get_loglevel, tags = [logs, server],
95 desc = "Get the current loglevel", module = ejabberd_loglevel,
96 function = get, args = [],
97 result =
98 {leveltuple,
99 {tuple,
100 [{levelnumber, integer}, {levelatom, atom},
101 {leveldesc, string}]}}},
102 #ejabberd_commands{name = update_list, tags = [server],
103 desc = "List modified modules that can be updated",
104 module = ?MODULE, function = update_list, args = [],
105 result = {modules, {list, {module, string}}}},
106 #ejabberd_commands{name = update, tags = [server],
107 desc = "Update the given module, or use the keyword: all",
108 module = ?MODULE, function = update, args = [{module, string}],
109 result = {res, restuple}},
110 #ejabberd_commands{name = register, tags = [accounts], desc = "Register a user",
111 module = ?MODULE, function = register,
112 args = [{user, string}, {host, string}, {password, string}],
113 result = {res, restuple}},
114 #ejabberd_commands{name = unregister, tags = [accounts], desc = "Unregister a user",
115 module = ?MODULE, function = unregister,
116 args = [{user, string}, {host, string}],
117 result = {res, restuple}},
118 #ejabberd_commands{name = registered_users, tags = [accounts],
119 desc = "List all registered users in HOST", module = ?MODULE,
120 function = registered_users, args = [{host, string}],
121 result = {users, {list, {username, string}}}},
122 #ejabberd_commands{name = gli, tags = [accounts],
123 desc = "Get information about last access of an account",
124 module = ?MODULE, function = get_last_info, args = [],
125 result =
126 {lastinfo,
127 {tuple, [{timestamp, integer}, {status, string}]}}},
128 #ejabberd_commands{name = get_last_info, tags = [accounts],
129 desc = "Get information about last access of an account",
130 module = ?MODULE, function = get_last_info,
131 args = [{user, string}, {host, string}],
132 result =
133 {lastinfo,
134 {tuple, [{timestamp, integer}, {status, string}]}}},
135 #ejabberd_commands{name = import_file, tags = [mnesia],
136 desc = "Import user data from jabberd14 spool file",
137 module = ?MODULE, function = import_file, args = [{file, string}],
138 result = {res, restuple}},
139 #ejabberd_commands{name = import_dir, tags = [mnesia],
140 desc = "Import users data from jabberd14 spool dir",
141 module = ?MODULE, function = import_dir, args = [{file, string}],
142 result = {res, restuple}},
143 #ejabberd_commands{name = import_piefxis, tags = [mnesia],
144 desc = "Import users data from a PIEFXIS file (XEP-0227)",
145 module = ejabberd_piefxis, function = import_file,
146 args = [{file, string}], result = {res, rescode}},
147 #ejabberd_commands{name = export_piefxis, tags = [mnesia],
148 desc =
149 "Export data of all users in the server to PIEFXIS files (XEP-0227)",
150 module = ejabberd_piefxis, function = export_server,
151 args = [{dir, string}], result = {res, rescode}},
152 #ejabberd_commands{name = export_piefxis_host, tags = [mnesia],
153 desc =
154 "Export data of users in a host to PIEFXIS files (XEP-0227)",
155 module = ejabberd_piefxis, function = export_host,
156 args = [{dir, string}, {host, string}], result = {res, rescode}},
157 #ejabberd_commands{name = delete_expired_messages, tags = [purge],
158 desc = "Delete expired offline messages from database",
159 module = ?MODULE, function = delete_expired_messages, args = [],
160 result = {res, rescode}},
161 #ejabberd_commands{name = delete_old_messages, tags = [purge],
162 desc = "Delete offline messages older than DAYS",
163 module = ?MODULE, function = delete_old_messages,
164 args = [{days, integer}], result = {res, rescode}},
165 #ejabberd_commands{name = set_master, tags = [mnesia],
166 desc = "Set master node of the clustered Mnesia tables",
167 longdesc =
168 "If you provide as nodename \"self\", this node will be set as its own master.",
169 module = ?MODULE, function = set_master,
170 args = [{nodename, string}], result = {res, restuple}},
171 #ejabberd_commands{name = mnesia_change_nodename, tags = [mnesia],
172 desc = "Change the erlang node name in a backup file",
173 module = ?MODULE, function = mnesia_change_nodename,
174 args =
175 [{oldnodename, string}, {newnodename, string},
176 {oldbackup, string}, {newbackup, string}],
177 result = {res, restuple}},
178 #ejabberd_commands{name = backup, tags = [mnesia],
179 desc = "Store the database to backup file", module = ?MODULE,
180 function = backup_mnesia, args = [{file, string}],
181 result = {res, restuple}},
182 #ejabberd_commands{name = restore, tags = [mnesia],
183 desc = "Restore the database from backup file", module = ?MODULE,
184 function = restore_mnesia, args = [{file, string}],
185 result = {res, restuple}},
186 #ejabberd_commands{name = dump, tags = [mnesia],
187 desc = "Dump the database to text file", module = ?MODULE,
188 function = dump_mnesia, args = [{file, string}],
189 result = {res, restuple}},
190 #ejabberd_commands{name = dump_table, tags = [mnesia],
191 desc = "Dump a table to text file", module = ?MODULE,
192 function = dump_table, args = [{file, string}, {table, string}],
193 result = {res, restuple}},
194 #ejabberd_commands{name = load, tags = [mnesia],
195 desc = "Restore the database from text file", module = ?MODULE,
196 function = load_mnesia, args = [{file, string}],
197 result = {res, restuple}},
198 #ejabberd_commands{name = install_fallback, tags = [mnesia],
199 desc = "Install the database from a fallback file",
200 module = ?MODULE, function = install_fallback_mnesia,
201 args = [{file, string}], result = {res, restuple}}].
202
203
207
208 status() ->
209 {InternalStatus, ProvidedStatus} = init:get_status(),
210 String1 = io_lib:format("The node ~p is ~p. Status: ~p",
211 [node(), InternalStatus, ProvidedStatus]),
212 {Is_running, String2} = case lists:keysearch(ejabberd, 1,
213 application:which_applications())
214 of
215 false ->
216 {ejabberd_not_running,
217 "ejabberd is not running in that node."};
218 {value, {_, _, Version}} ->
219 {ok,
220 io_lib:format("ejabberd ~s is running in that node",
221 [Version])}
222 end,
223 {Is_running, String1 ++ String2}.
224
225 reopen_log() ->
226 ejabberd_hooks:run(reopen_log_hook, []),
227
228 ejabberd_logger_h:reopen_log(),
229 case application:get_env(sasl, sasl_error_logger) of
230 {ok, {file, SASLfile}} ->
231 error_logger:delete_report_handler(sasl_report_file_h),
232 ejabberd_logger_h:rotate_log(SASLfile),
233 error_logger:add_report_handler(sasl_report_file_h,
234 {SASLfile, get_sasl_error_logger_type()});
235 _ -> false
236 end,
237 ok.
238
239
240 get_sasl_error_logger_type() ->
241 case application:get_env(sasl, errlog_type) of
242 {ok, error} -> error;
243 {ok, progress} -> progress;
244 {ok, all} -> all;
245 {ok, Bad} -> exit({bad_config, {sasl, {errlog_type, Bad}}});
246 _ -> all
247 end.
248
249
253
254 stop_kindly(DelaySeconds, AnnouncementText) ->
255 Subject = io_lib:format("Server stop in ~p seconds!", [DelaySeconds]),
256 WaitingDesc = io_lib:format("Waiting ~p seconds", [DelaySeconds]),
257 Steps = [{"Stopping ejabberd port listeners", ejabberd_listener, stop_listeners, []},
258 {"Sending announcement to connected users", mod_announce,
259 send_announcement_to_all, [?MYNAME, Subject, AnnouncementText]},
260 {"Sending service message to MUC rooms", ejabberd_admin,
261 send_service_message_all_mucs, [Subject, AnnouncementText]},
262 {WaitingDesc, timer, sleep, [DelaySeconds * 1000]},
263 {"Stopping ejabberd", application, stop, [ejabberd]},
264 {"Stopping Mnesia", mnesia, stop, []},
265 {"Stopping Erlang node", init, stop, []}],
266 NumberLast = length(Steps),
267 TimestampStart = calendar:datetime_to_gregorian_seconds({date(), time()}),
268 lists:foldl(fun ({Desc, Mod, Func, Args}, NumberThis) ->
269 SecondsDiff = calendar:datetime_to_gregorian_seconds({date(),
270 time()})
271 - TimestampStart,
272 io:format("[~p/~p ~ps] ~s... ",
273 [NumberThis, NumberLast, SecondsDiff, Desc]),
274 Result = apply(Mod, Func, Args),
275 io:format("~p~n", [Result]),
276 NumberThis + 1
277 end,
278 1, Steps),
279 ok.
280
281 send_service_message_all_mucs(Subject, AnnouncementText) ->
282 Message = io_lib:format("~s~n~s", [Subject, AnnouncementText]),
283 lists:foreach(fun (ServerHost) ->
284 MUCHost = gen_mod:get_module_opt_host(ServerHost, mod_muc,
285 "conference.@HOST@"),
286 MUCHostB = list_to_binary(MUCHost),
287 mod_muc:broadcast_service_message(MUCHostB, Message)
288 end,
289 ?MYHOSTS).
290
291
295
296 update_list() ->
297 {ok, _Dir, UpdatedBeams, _Script, _LowLevelScript, _Check} =
298 ejabberd_update:update_info(),
299 [atom_to_list(Beam) || Beam <- UpdatedBeams].
300
301 update("all") -> [update_module(ModStr) || ModStr <- update_list()], {ok, []};
302 update(ModStr) -> update_module(ModStr).
303
304 update_module(ModuleNameString) ->
305 ModuleName = list_to_atom(ModuleNameString),
306 case ejabberd_update:update([ModuleName]) of
307 {ok, _Res} -> {ok, []};
308 {error, Reason} -> {error, Reason}
309 end.
310
311
315
316 register(User, Host, Password) ->
317 case ejabberd_auth:try_register(User, Host, Password) of
318 {atomic, ok} ->
319 {ok, io_lib:format("User ~s@~s successfully registered", [User, Host])};
320 {atomic, exists} ->
321 String = io_lib:format("User ~s@~s already registered at node ~p",
322 [User, Host, node()]),
323 {exists, String};
324 {error, Reason} ->
325 String = io_lib:format("Can't register user ~s@~s at node ~p: ~p",
326 [User, Host, node(), Reason]),
327 {cannot_register, String}
328 end.
329
330 unregister(User, Host) -> ejabberd_auth:remove_user(User, Host), {ok, ""}.
331
332 registered_users(Host) ->
333 Users = ejabberd_auth:get_vh_registered_users(Host),
334 SUsers = lists:sort(Users),
335 lists:map(fun ({U, _S}) -> U end, SUsers).
336
337 get_last_info() -> get_last_info("badlop", "localhost").
338
339 get_last_info(User, Server) ->
340 case mod_last:get_last_info(User, Server) of
341 {ok, TimeStamp, Status} -> {TimeStamp, Status};
342 not_found -> {"never", ""}
343 end.
344
345
349
350 import_file(Path) ->
351 case jd2ejd:import_file(Path) of
352 ok -> {ok, ""};
353 {error, Reason} ->
354 String = io_lib:format("Can't import jabberd14 spool file ~p at node ~p: ~p",
355 [filename:absname(Path), node(), Reason]),
356 {cannot_import_file, String}
357 end.
358
359 import_dir(Path) ->
360 case jd2ejd:import_dir(Path) of
361 ok -> {ok, ""};
362 {error, Reason} ->
363 String = io_lib:format("Can't import jabberd14 spool dir ~p at node ~p: ~p",
364 [filename:absname(Path), node(), Reason]),
365 {cannot_import_dir, String}
366 end.
367
368
372
373 delete_expired_messages() -> mod_offline:remove_expired_messages(), ok.
374
375 delete_old_messages(Days) -> mod_offline:remove_old_messages(Days), ok.
376
377
381
382 set_master("self") -> set_master(node());
383 set_master(NodeString) when is_list(NodeString) -> set_master(list_to_atom(NodeString));
384 set_master(Node) when is_atom(Node) ->
385 case mnesia:set_master_nodes([Node]) of
386 ok -> {ok, ""};
387 {error, Reason} ->
388 String = io_lib:format("Can't set master node ~p at node ~p:~n~p",
389 [Node, node(), Reason]),
390 {error, String}
391 end.
392
393 backup_mnesia(Path) ->
394 case mnesia:backup(Path) of
395 ok -> {ok, ""};
396 {error, Reason} ->
397 String = io_lib:format("Can't store backup in ~p at node ~p: ~p",
398 [filename:absname(Path), node(), Reason]),
399 {cannot_backup, String}
400 end.
401
402 restore_mnesia(Path) ->
403 case ejabberd_admin:restore(Path) of
404 {atomic, _} -> {ok, ""};
405 {error, Reason} ->
406 String = io_lib:format("Can't restore backup from ~p at node ~p: ~p",
407 [filename:absname(Path), node(), Reason]),
408 {cannot_restore, String};
409 {aborted, {no_exists, Table}} ->
410 String =
411 io_lib:format("Can't restore backup from ~p at node ~p: Table ~p does not exist.",
412 [filename:absname(Path), node(), Table]),
413 {table_not_exists, String};
414 {aborted, enoent} ->
415 String =
416 io_lib:format("Can't restore backup from ~p at node ~p: File not found.",
417 [filename:absname(Path), node()]),
418 {file_not_found, String}
419 end.
420
421
422
423
424 restore(Path) ->
425 mnesia:restore(Path, [{keep_tables, keep_tables()}, {default_op, skip_tables}]).
426
427
428
429
430
431 keep_tables() ->
432 lists:flatten([acl, passwd, config, local_config, keep_modules_tables()]).
433
434
435
436 keep_modules_tables() ->
437 lists:map(fun (Module) -> module_tables(Module) end, gen_mod:loaded_modules(?MYNAME)).
438
439
440
441
442 module_tables(mod_announce) -> [motd, motd_users];
443 module_tables(mod_irc) -> [irc_custom];
444 module_tables(mod_last) -> [last_activity];
445 module_tables(mod_muc) -> [muc_room, muc_registered];
446 module_tables(mod_offline) -> [offline_msg];
447 module_tables(mod_privacy) -> [privacy];
448 module_tables(mod_private) -> [private_storage];
449 module_tables(mod_pubsub) -> [pubsub_node];
450 module_tables(mod_roster) -> [roster];
451 module_tables(mod_shared_roster) -> [sr_group, sr_user];
452 module_tables(mod_vcard) -> [vcard, vcard_search];
453 module_tables(_Other) -> [].
454
455 get_local_tables() ->
456 Tabs1 = lists:delete(schema, mnesia:system_info(local_tables)),
457 Tabs = lists:filter(fun (T) ->
458 case mnesia:table_info(T, storage_type) of
459 disc_copies -> true;
460 disc_only_copies -> true;
461 _ -> false
462 end
463 end,
464 Tabs1),
465 Tabs.
466
467 dump_mnesia(Path) -> Tabs = get_local_tables(), dump_tables(Path, Tabs).
468
469 dump_table(Path, STable) -> Table = list_to_atom(STable), dump_tables(Path, [Table]).
470
471 dump_tables(Path, Tables) ->
472 case dump_to_textfile(Path, Tables) of
473 ok -> {ok, ""};
474 {error, Reason} ->
475 String = io_lib:format("Can't store dump in ~p at node ~p: ~p",
476 [filename:absname(Path), node(), Reason]),
477 {cannot_dump, String}
478 end.
479
480 dump_to_textfile(File) -> Tabs = get_local_tables(), dump_to_textfile(File, Tabs).
481
482 dump_to_textfile(File, Tabs) ->
483 dump_to_textfile(mnesia:system_info(is_running), Tabs, file:open(File, [write])).
484
485 dump_to_textfile(yes, Tabs, {ok, F}) ->
486 Defs = lists:map(fun (T) ->
487 {T,
488 [{record_name, mnesia:table_info(T, record_name)},
489 {attributes, mnesia:table_info(T, attributes)}]}
490 end,
491 Tabs),
492 io:format(F, "~p.~n", [{tables, Defs}]),
493 lists:foreach(fun (T) -> dump_tab(F, T) end, Tabs),
494 file:close(F);
495 dump_to_textfile(_, _, {ok, F}) -> file:close(F), {error, mnesia_not_running};
496 dump_to_textfile(_, _, {error, Reason}) -> {error, Reason}.
497
498 dump_tab(F, T) ->
499 W = mnesia:table_info(T, wild_pattern),
500 {atomic, All} = mnesia:transaction(fun () -> mnesia:match_object(T, W, read) end),
501 lists:foreach(fun (Term) -> io:format(F, "~p.~n", [setelement(1, Term, T)]) end, All).
502
503 load_mnesia(Path) ->
504 case mnesia:load_textfile(Path) of
505 {atomic, ok} -> {ok, ""};
506 {error, Reason} ->
507 String = io_lib:format("Can't load dump in ~p at node ~p: ~p",
508 [filename:absname(Path), node(), Reason]),
509 {cannot_load, String}
510 end.
511
512 install_fallback_mnesia(Path) ->
513 case mnesia:install_fallback(Path) of
514 ok -> {ok, ""};
515 {error, Reason} ->
516 String = io_lib:format("Can't install fallback from ~p at node ~p: ~p",
517 [filename:absname(Path), node(), Reason]),
518 {cannot_fallback, String}
519 end.
520
521 mnesia_change_nodename(FromString, ToString, Source, Target) ->
522 From = list_to_atom(FromString),
523 To = list_to_atom(ToString),
524 Switch = fun (Node) when Node == From ->
525 io:format(" - Replacing nodename: '~p' with: '~p'~n",
526 [From, To]),
527 To;
528 (Node) when Node == To ->
529
530 io:format(" - Node: '~p' will not be modified (it is already '~p')~n",
531 [Node, To]),
532 Node;
533 (Node) ->
534 io:format(" - Node: '~p' will not be modified (it is not '~p')~n",
535 [Node, From]),
536 Node
537 end,
538 Convert = fun ({schema, db_nodes, Nodes}, Acc) ->
539 io:format(" +++ db_nodes ~p~n", [Nodes]),
540 {[{schema, db_nodes, lists:map(Switch, Nodes)}], Acc};
541 ({schema, version, Version}, Acc) ->
542 io:format(" +++ version: ~p~n", [Version]),
543 {[{schema, version, Version}], Acc};
544 ({schema, cookie, Cookie}, Acc) ->
545 io:format(" +++ cookie: ~p~n", [Cookie]),
546 {[{schema, cookie, Cookie}], Acc};
547 ({schema, Tab, CreateList}, Acc) ->
548 io:format("~n * Checking table: '~p'~n", [Tab]),
549 Keys = [ram_copies, disc_copies, disc_only_copies],
550 OptSwitch = fun ({Key, Val}) ->
551 case lists:member(Key, Keys) of
552 true ->
553 io:format(" + Checking key: '~p'~n",
554 [Key]),
555 {Key, lists:map(Switch, Val)};
556 false -> {Key, Val}
557 end
558 end,
559 Res = {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc},
560 Res;
561 (Other, Acc) -> {[Other], Acc}
562 end,
563 mnesia:traverse_backup(Source, Target, Convert, switched).