1%% This Source Code Form is subject to the terms of the Mozilla Public
2%% License, v. 2.0. If a copy of the MPL was not distributed with this
3%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4%%
5%% Copyright (c) 2007-2021 VMware, Inc. or its affiliates.  All rights reserved.
6%%
7
8-module(rabbit_runtime_parameters).
9
10%% Runtime parameters are bits of configuration that are
11%% set, as the name implies, at runtime and not in the config file.
12%%
13%% The benefits of storing some bits of configuration at runtime vary:
14%%
15%%  * Some parameters are vhost-specific
16%%  * Others are specific to individual nodes
17%%  * ...or even queues, exchanges, etc
18%%
19%% The most obvious use case for runtime parameters is policies but
20%% there are others:
21%%
22%% * Plugin-specific parameters that only make sense at runtime,
23%%   e.g. Federation and Shovel link settings
24%% * Exchange and queue decorators
25%%
26%% Parameters are grouped by components, e.g. <<"policy">> or <<"shovel">>.
27%% Components are mapped to modules that perform validation.
28%% Runtime parameter values are then looked up by the modules that
29%% need to use them.
30%%
31%% Parameters are stored in Mnesia and can be global. Their changes
32%% are broadcasted over rabbit_event.
33%%
34%% Global parameters keys are atoms and values are JSON documents.
35%%
36%% See also:
37%%
38%%  * rabbit_policies
39%%  * rabbit_policy
40%%  * rabbit_registry
41%%  * rabbit_event
42
43-include_lib("rabbit_common/include/rabbit.hrl").
44
45-export([parse_set/5, set/5, set_any/5, clear/4, clear_any/4, list/0, list/1,
46         list_component/1, list/2, list_formatted/1, list_formatted/3,
47         lookup/3, value/3, value/4, info_keys/0, clear_component/2]).
48
49-export([parse_set_global/3, set_global/3, value_global/1, value_global/2,
50         list_global/0, list_global_formatted/0, list_global_formatted/2,
51         lookup_global/1, global_info_keys/0, clear_global/2]).
52
53%%----------------------------------------------------------------------------
54
55-type ok_or_error_string() :: 'ok' | {'error_string', string()}.
56-type ok_thunk_or_error_string() :: ok_or_error_string() | fun(() -> 'ok').
57
58%%---------------------------------------------------------------------------
59
60-import(rabbit_misc, [pget/2]).
61
62-define(TABLE, rabbit_runtime_parameters).
63
64%%---------------------------------------------------------------------------
65
66-spec parse_set(rabbit_types:vhost(), binary(), binary(), string(),
67                rabbit_types:user() | rabbit_types:username() | 'none')
68               -> ok_or_error_string().
69
70parse_set(_, <<"policy">>, _, _, _) ->
71    {error_string, "policies may not be set using this method"};
72parse_set(VHost, Component, Name, String, User) ->
73    Definition = rabbit_data_coercion:to_binary(String),
74    case rabbit_json:try_decode(Definition) of
75        {ok, Term} when is_map(Term) -> set(VHost, Component, Name, maps:to_list(Term), User);
76        {ok, Term} -> set(VHost, Component, Name, Term, User);
77        {error, Reason} ->
78            {error_string,
79                rabbit_misc:format("JSON decoding error. Reason: ~ts", [Reason])}
80    end.
81
82-spec set(rabbit_types:vhost(), binary(), binary(), term(),
83                rabbit_types:user() | rabbit_types:username() | 'none')
84         -> ok_or_error_string().
85
86set(_, <<"policy">>, _, _, _) ->
87    {error_string, "policies may not be set using this method"};
88set(VHost, Component, Name, Term, User) ->
89    set_any(VHost, Component, Name, Term, User).
90
91parse_set_global(Name, String, ActingUser) ->
92    Definition = rabbit_data_coercion:to_binary(String),
93    case rabbit_json:try_decode(Definition) of
94        {ok, Term} when is_map(Term) -> set_global(Name, maps:to_list(Term), ActingUser);
95        {ok, Term} -> set_global(Name, Term, ActingUser);
96        {error, Reason} ->
97            {error_string,
98                rabbit_misc:format("JSON decoding error. Reason: ~ts", [Reason])}
99    end.
100
101-spec set_global(atom(), term(), rabbit_types:username()) -> 'ok'.
102
103set_global(Name, Term, ActingUser)  ->
104    NameAsAtom = rabbit_data_coercion:to_atom(Name),
105    rabbit_log:debug("Setting global parameter '~s' to ~p", [NameAsAtom, Term]),
106    mnesia_update(NameAsAtom, Term),
107    event_notify(parameter_set, none, global, [{name,  NameAsAtom},
108                                               {value, Term},
109                                               {user_who_performed_action, ActingUser}]),
110    ok.
111
112format_error(L) ->
113    {error_string, rabbit_misc:format_many([{"Validation failed~n", []} | L])}.
114
115-spec set_any(rabbit_types:vhost(), binary(), binary(), term(),
116              rabbit_types:user() | rabbit_types:username() | 'none')
117             -> ok_or_error_string().
118
119set_any(VHost, Component, Name, Term, User) ->
120    case set_any0(VHost, Component, Name, Term, User) of
121        ok          -> ok;
122        {errors, L} -> format_error(L)
123    end.
124
125set_any0(VHost, Component, Name, Term, User) ->
126    rabbit_log:debug("Asked to set or update runtime parameter '~s' in vhost '~s' "
127                     "for component '~s', value: ~p",
128                     [Name, VHost, Component, Term]),
129    case lookup_component(Component) of
130        {ok, Mod} ->
131            case flatten_errors(
132                   Mod:validate(VHost, Component, Name, Term, get_user(User))) of
133                ok ->
134                    case mnesia_update(VHost, Component, Name, Term) of
135                        {old, Term} ->
136                            ok;
137                        _           ->
138                            ActingUser = get_username(User),
139                            event_notify(
140                              parameter_set, VHost, Component,
141                              [{name,  Name},
142                               {value, Term},
143                               {user_who_performed_action, ActingUser}]),
144                            Mod:notify(VHost, Component, Name, Term, ActingUser)
145                    end,
146                    ok;
147                E ->
148                    E
149            end;
150        E ->
151            E
152    end.
153
154%% Validate only an user record as expected by the API before #rabbitmq-event-exchange-10
155get_user(#user{} = User) ->
156    User;
157get_user(_) ->
158    none.
159
160get_username(#user{username = Username}) ->
161    Username;
162get_username(none) ->
163    ?INTERNAL_USER;
164get_username(Any) ->
165    Any.
166
167mnesia_update(Key, Term) ->
168    rabbit_misc:execute_mnesia_transaction(mnesia_update_fun(Key, Term)).
169
170mnesia_update(VHost, Comp, Name, Term) ->
171    rabbit_misc:execute_mnesia_transaction(
172      rabbit_vhost:with(VHost, mnesia_update_fun({VHost, Comp, Name}, Term))).
173
174mnesia_update_fun(Key, Term) ->
175    fun () ->
176            Res = case mnesia:read(?TABLE, Key, read) of
177                      []       -> new;
178                      [Params] -> {old, Params#runtime_parameters.value}
179                      end,
180            ok = mnesia:write(?TABLE, c(Key, Term), write),
181            Res
182    end.
183
184-spec clear(rabbit_types:vhost(), binary(), binary(), rabbit_types:username())
185           -> ok_thunk_or_error_string().
186
187clear(_, <<"policy">> , _, _) ->
188    {error_string, "policies may not be cleared using this method"};
189clear(VHost, Component, Name, ActingUser) ->
190    clear_any(VHost, Component, Name, ActingUser).
191
192clear_global(Key, ActingUser) ->
193    KeyAsAtom = rabbit_data_coercion:to_atom(Key),
194    Notify = fun() ->
195                    event_notify(parameter_set, none, global,
196                                 [{name,  KeyAsAtom},
197                                  {user_who_performed_action, ActingUser}]),
198                    ok
199             end,
200    case value_global(KeyAsAtom) of
201        not_found ->
202            {error_string, "Parameter does not exist"};
203        _         ->
204            F = fun () ->
205                ok = mnesia:delete(?TABLE, KeyAsAtom, write)
206                end,
207            ok = rabbit_misc:execute_mnesia_transaction(F),
208            case mnesia:is_transaction() of
209                true  -> Notify;
210                false -> Notify()
211            end
212    end.
213
214clear_component(Component, ActingUser) ->
215    case list_component(Component) of
216        [] ->
217            ok;
218        Xs ->
219            [clear(pget(vhost, X),
220                   pget(component, X),
221                   pget(name, X),
222                   ActingUser) || X <- Xs],
223            ok
224    end.
225
226-spec clear_any(rabbit_types:vhost(), binary(), binary(), rabbit_types:username())
227                     -> ok_thunk_or_error_string().
228
229clear_any(VHost, Component, Name, ActingUser) ->
230    Notify = fun () ->
231                     case lookup_component(Component) of
232                         {ok, Mod} -> event_notify(
233                                        parameter_cleared, VHost, Component,
234                                        [{name, Name},
235                                         {user_who_performed_action, ActingUser}]),
236                                      Mod:notify_clear(VHost, Component, Name, ActingUser);
237                         _         -> ok
238                     end
239             end,
240    case lookup(VHost, Component, Name) of
241        not_found -> {error_string, "Parameter does not exist"};
242        _         -> mnesia_clear(VHost, Component, Name),
243                     case mnesia:is_transaction() of
244                         true  -> Notify;
245                         false -> Notify()
246                     end
247    end.
248
249mnesia_clear(VHost, Component, Name) ->
250    F = fun () ->
251                ok = mnesia:delete(?TABLE, {VHost, Component, Name}, write)
252        end,
253    ok = rabbit_misc:execute_mnesia_transaction(rabbit_vhost:with(VHost, F)).
254
255event_notify(_Event, _VHost, <<"policy">>, _Props) ->
256    ok;
257event_notify(Event, none, Component, Props) ->
258    rabbit_event:notify(Event, [{component, Component} | Props]);
259event_notify(Event, VHost, Component, Props) ->
260    rabbit_event:notify(Event, [{vhost,     VHost},
261                                {component, Component} | Props]).
262
263-spec list() -> [rabbit_types:infos()].
264
265list() ->
266    [p(P) || #runtime_parameters{ key = {_VHost, Comp, _Name}} = P <-
267             rabbit_misc:dirty_read_all(?TABLE), Comp /= <<"policy">>].
268
269-spec list(rabbit_types:vhost() | '_') -> [rabbit_types:infos()].
270
271list(VHost) -> list(VHost, '_').
272
273-spec list_component(binary()) -> [rabbit_types:infos()].
274
275list_component(Component) -> list('_',   Component).
276
277%% Not dirty_match_object since that would not be transactional when used in a
278%% tx context
279-spec list(rabbit_types:vhost() | '_', binary() | '_')
280                -> [rabbit_types:infos()].
281
282list(VHost, Component) ->
283    mnesia:async_dirty(
284      fun () ->
285              case VHost of
286                  '_' -> ok;
287                  _   -> rabbit_vhost:assert(VHost)
288              end,
289              Match = #runtime_parameters{key = {VHost, Component, '_'},
290                                          _   = '_'},
291              [p(P) || #runtime_parameters{key = {_VHost, Comp, _Name}} = P <-
292                           mnesia:match_object(?TABLE, Match, read),
293                       Comp =/= <<"policy">> orelse Component =:= <<"policy">>]
294      end).
295
296list_global() ->
297    %% list only atom keys
298    mnesia:async_dirty(
299        fun () ->
300            Match = #runtime_parameters{key = '_', _ = '_'},
301            [p(P) || P <- mnesia:match_object(?TABLE, Match, read),
302                is_atom(P#runtime_parameters.key)]
303        end).
304
305-spec list_formatted(rabbit_types:vhost()) -> [rabbit_types:infos()].
306
307list_formatted(VHost) ->
308    [ format_parameter(info_keys(), P) || P <- list(VHost) ].
309
310format_parameter(InfoKeys, P) ->
311    lists:foldr(fun
312                    (value, Acc) ->
313                        [{value, rabbit_json:encode(pget(value, P))} | Acc];
314                    (Key, Acc)   ->
315                        case lists:keyfind(Key, 1, P) of
316                            false      -> Acc;
317                            {Key, Val} -> [{Key, Val} | Acc]
318                        end
319                end,
320                [], InfoKeys).
321
322-spec list_formatted(rabbit_types:vhost(), reference(), pid()) -> 'ok'.
323
324list_formatted(VHost, Ref, AggregatorPid) ->
325    rabbit_control_misc:emitting_map(
326      AggregatorPid, Ref,
327      fun(P) -> format_parameter(info_keys(), P) end, list(VHost)).
328
329list_global_formatted() ->
330    [ format_parameter(global_info_keys(), P) || P <- list_global() ].
331
332list_global_formatted(Ref, AggregatorPid) ->
333    rabbit_control_misc:emitting_map(
334        AggregatorPid, Ref,
335        fun(P) -> format_parameter(global_info_keys(), P) end, list_global()).
336
337-spec lookup(rabbit_types:vhost(), binary(), binary())
338                  -> rabbit_types:infos() | 'not_found'.
339
340lookup(VHost, Component, Name) ->
341    case lookup0({VHost, Component, Name}, rabbit_misc:const(not_found)) of
342        not_found -> not_found;
343        Params    -> p(Params)
344    end.
345
346lookup_global(Name)  ->
347    case lookup0(Name, rabbit_misc:const(not_found)) of
348        not_found -> not_found;
349        Params    -> p(Params)
350    end.
351
352-spec value(rabbit_types:vhost(), binary(), binary()) -> term().
353
354value(VHost, Comp, Name) -> value0({VHost, Comp, Name}).
355
356-spec value(rabbit_types:vhost(), binary(), binary(), term()) -> term().
357
358value(VHost, Comp, Name, Def) -> value0({VHost, Comp, Name}, Def).
359
360-spec value_global(atom()) -> term() | 'not_found'.
361
362value_global(Key) ->
363    value0(Key).
364
365-spec value_global(atom(), term()) -> term().
366
367value_global(Key, Default) ->
368    value0(Key, Default).
369
370value0(Key) ->
371    case lookup0(Key, rabbit_misc:const(not_found)) of
372        not_found -> not_found;
373        Params    -> Params#runtime_parameters.value
374    end.
375
376value0(Key, Default) ->
377    Params = lookup0(Key, fun () -> lookup_missing(Key, Default) end),
378    Params#runtime_parameters.value.
379
380lookup0(Key, DefaultFun) ->
381    case mnesia:dirty_read(?TABLE, Key) of
382        []  -> DefaultFun();
383        [R] -> R
384    end.
385
386lookup_missing(Key, Default) ->
387    rabbit_misc:execute_mnesia_transaction(
388      fun () ->
389              case mnesia:read(?TABLE, Key, read) of
390                  []  -> Record = c(Key, Default),
391                         mnesia:write(?TABLE, Record, write),
392                         Record;
393                  [R] -> R
394              end
395      end).
396
397c(Key, Default) ->
398    #runtime_parameters{key   = Key,
399                        value = Default}.
400
401p(#runtime_parameters{key = {VHost, Component, Name}, value = Value}) ->
402    [{vhost,     VHost},
403     {component, Component},
404     {name,      Name},
405     {value,     Value}];
406
407p(#runtime_parameters{key = Key, value = Value}) when is_atom(Key) ->
408    [{name,      Key},
409     {value,     Value}].
410
411-spec info_keys() -> rabbit_types:info_keys().
412
413info_keys() -> [component, name, value].
414
415global_info_keys() -> [name, value].
416
417%%---------------------------------------------------------------------------
418
419lookup_component(Component) ->
420    case rabbit_registry:lookup_module(
421           runtime_parameter, list_to_atom(binary_to_list(Component))) of
422        {error, not_found} -> {errors,
423                               [{"component ~s not found", [Component]}]};
424        {ok, Module}       -> {ok, Module}
425    end.
426
427flatten_errors(L) ->
428    case [{F, A} || I <- lists:flatten([L]), {error, F, A} <- [I]] of
429        [] -> ok;
430        E  -> {errors, E}
431    end.
432