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_version).
9
10-export([recorded/0, matches/2, desired/0, desired_for_scope/1,
11         record_desired/0, record_desired_for_scope/1,
12         upgrades_required/1, all_upgrades_required/1,
13         check_version_consistency/3,
14         check_version_consistency/4, check_otp_consistency/1,
15         version_error/3]).
16
17%% -------------------------------------------------------------------
18
19-export_type([scope/0, step/0]).
20
21-type scope() :: atom().
22-type scope_version() :: [atom()].
23-type step() :: {atom(), atom()}.
24
25-type version() :: [atom()].
26
27%% -------------------------------------------------------------------
28
29-define(VERSION_FILENAME, "schema_version").
30-define(SCOPES, [mnesia, local]).
31
32%% -------------------------------------------------------------------
33
34-spec recorded() -> rabbit_types:ok_or_error2(version(), any()).
35
36recorded() -> case rabbit_file:read_term_file(schema_filename()) of
37                  {ok, [V]}        -> {ok, V};
38                  {error, _} = Err -> Err
39              end.
40
41record(V) -> ok = rabbit_file:write_term_file(schema_filename(), [V]).
42
43recorded_for_scope(Scope) ->
44    case recorded() of
45        {error, _} = Err ->
46            Err;
47        {ok, Version} ->
48            {ok, case lists:keysearch(Scope, 1, categorise_by_scope(Version)) of
49                     false                 -> [];
50                     {value, {Scope, SV1}} -> SV1
51                 end}
52    end.
53
54record_for_scope(Scope, ScopeVersion) ->
55    case recorded() of
56        {error, _} = Err ->
57            Err;
58        {ok, Version} ->
59            Version1 = lists:keystore(Scope, 1, categorise_by_scope(Version),
60                                      {Scope, ScopeVersion}),
61            ok = record([Name || {_Scope, Names} <- Version1, Name <- Names])
62    end.
63
64%% -------------------------------------------------------------------
65
66-spec matches([A], [A]) -> boolean().
67
68matches(VerA, VerB) ->
69    lists:usort(VerA) =:= lists:usort(VerB).
70
71%% -------------------------------------------------------------------
72
73-spec desired() -> version().
74
75desired() -> [Name || Scope <- ?SCOPES, Name <- desired_for_scope(Scope)].
76
77-spec desired_for_scope(scope()) -> scope_version().
78
79desired_for_scope(Scope) -> with_upgrade_graph(fun heads/1, Scope).
80
81-spec record_desired() -> 'ok'.
82
83record_desired() -> record(desired()).
84
85-spec record_desired_for_scope
86        (scope()) -> rabbit_types:ok_or_error(any()).
87
88record_desired_for_scope(Scope) ->
89    record_for_scope(Scope, desired_for_scope(Scope)).
90
91-spec upgrades_required
92        (scope()) -> rabbit_types:ok_or_error2([step()], any()).
93
94upgrades_required(Scope) ->
95    case recorded_for_scope(Scope) of
96        {error, enoent} ->
97            case filelib:is_file(rabbit_guid:filename()) of
98                false -> {error, starting_from_scratch};
99                true  -> {error, version_not_available}
100            end;
101        {ok, CurrentHeads} ->
102            with_upgrade_graph(
103              fun (G) ->
104                      case unknown_heads(CurrentHeads, G) of
105                          []      -> {ok, upgrades_to_apply(CurrentHeads, G)};
106                          Unknown -> {error, {future_upgrades_found, Unknown}}
107                      end
108              end, Scope)
109    end.
110
111all_upgrades_required(Scopes) ->
112    case recorded() of
113        {error, enoent} ->
114            case filelib:is_file(rabbit_guid:filename()) of
115                false -> {error, starting_from_scratch};
116                true  -> {error, version_not_available}
117            end;
118        {ok, _} ->
119            lists:foldl(
120                fun
121                (_, {error, Err}) -> {error, Err};
122                (Scope, {ok, Acc}) ->
123                    case upgrades_required(Scope) of
124                        %% Lift errors from any scope.
125                        {error, Err}   -> {error, Err};
126                        %% Filter non-upgradable scopes
127                        {ok, []}       -> {ok, Acc};
128                        {ok, Upgrades} -> {ok, [{Scope, Upgrades} | Acc]}
129                    end
130                end,
131                {ok, []},
132                Scopes)
133    end.
134
135%% -------------------------------------------------------------------
136
137with_upgrade_graph(Fun, Scope) ->
138    case rabbit_misc:build_acyclic_graph(
139           fun ({_App, Module, Steps}) -> vertices(Module, Steps, Scope) end,
140           fun ({_App, Module, Steps}) -> edges(Module, Steps, Scope) end,
141           rabbit_misc:all_module_attributes(rabbit_upgrade)) of
142        {ok, G} -> try
143                       Fun(G)
144                   after
145                       true = digraph:delete(G)
146                   end;
147        {error, {vertex, duplicate, StepName}} ->
148            throw({error, {duplicate_upgrade_step, StepName}});
149        {error, {edge, {bad_vertex, StepName}, _From, _To}} ->
150            throw({error, {dependency_on_unknown_upgrade_step, StepName}});
151        {error, {edge, {bad_edge, StepNames}, _From, _To}} ->
152            throw({error, {cycle_in_upgrade_steps, StepNames}})
153    end.
154
155vertices(Module, Steps, Scope0) ->
156    [{StepName, {Module, StepName}} || {StepName, Scope1, _Reqs} <- Steps,
157                                       Scope0 == Scope1].
158
159edges(_Module, Steps, Scope0) ->
160    [{Require, StepName} || {StepName, Scope1, Requires} <- Steps,
161                            Require <- Requires,
162                            Scope0 == Scope1].
163unknown_heads(Heads, G) ->
164    [H || H <- Heads, digraph:vertex(G, H) =:= false].
165
166upgrades_to_apply(Heads, G) ->
167    %% Take all the vertices which can reach the known heads. That's
168    %% everything we've already applied. Subtract that from all
169    %% vertices: that's what we have to apply.
170    Unsorted = sets:to_list(
171                 sets:subtract(
172                   sets:from_list(digraph:vertices(G)),
173                   sets:from_list(digraph_utils:reaching(Heads, G)))),
174    %% Form a subgraph from that list and find a topological ordering
175    %% so we can invoke them in order.
176    [element(2, digraph:vertex(G, StepName)) ||
177        StepName <- digraph_utils:topsort(digraph_utils:subgraph(G, Unsorted))].
178
179heads(G) ->
180    lists:sort([V || V <- digraph:vertices(G), digraph:out_degree(G, V) =:= 0]).
181
182%% -------------------------------------------------------------------
183
184categorise_by_scope(Version) when is_list(Version) ->
185    Categorised =
186        [{Scope, Name} || {_App, _Module, Attributes} <-
187                              rabbit_misc:all_module_attributes(rabbit_upgrade),
188                          {Name, Scope, _Requires} <- Attributes,
189                          lists:member(Name, Version)],
190    maps:to_list(
191      lists:foldl(fun ({Scope, Name}, CatVersion) ->
192                          rabbit_misc:maps_cons(Scope, Name, CatVersion)
193                  end, maps:new(), Categorised)).
194
195dir() -> rabbit_mnesia:dir().
196
197schema_filename() -> filename:join(dir(), ?VERSION_FILENAME).
198
199%% --------------------------------------------------------------------
200
201-spec check_version_consistency
202        (string(), string(), string()) -> rabbit_types:ok_or_error(any()).
203
204check_version_consistency(This, Remote, Name) ->
205    check_version_consistency(This, Remote, Name, fun (A, B) -> A =:= B end).
206
207-spec check_version_consistency
208        (string(), string(), string(),
209         fun((string(), string()) -> boolean())) ->
210    rabbit_types:ok_or_error(any()).
211
212check_version_consistency(This, Remote, Name, Comp) ->
213    case Comp(This, Remote) of
214        true  -> ok;
215        false -> version_error(Name, This, Remote)
216    end.
217
218version_error(Name, This, Remote) ->
219    {error, {inconsistent_cluster,
220             rabbit_misc:format("~s version mismatch: local node is ~s, "
221                                "remote node ~s", [Name, This, Remote])}}.
222
223-spec check_otp_consistency
224        (string()) -> rabbit_types:ok_or_error(any()).
225
226check_otp_consistency(Remote) ->
227    check_version_consistency(rabbit_misc:otp_release(), Remote, "OTP").
228