Merge default
This commit is contained in:
commit
59be413836
|
|
@ -33,7 +33,7 @@ import socket
|
|||
LISTABLE = {'connections': False, 'channels': False, 'exchanges': True,
|
||||
'queues': True, 'bindings': True, 'users': False,
|
||||
'vhosts': False, 'permissions': False, 'nodes': False,
|
||||
'parameters': False}
|
||||
'parameters': False, 'policies': False}
|
||||
|
||||
SHOWABLE = {'overview': False}
|
||||
|
||||
|
|
@ -48,7 +48,8 @@ URIS = {
|
|||
'vhost': '/vhosts/{name}',
|
||||
'user': '/users/{name}',
|
||||
'permission': '/permissions/{vhost}/{user}',
|
||||
'parameter': '/parameters/{component}/{key}'
|
||||
'parameter': '/parameters/{component}/{key}',
|
||||
'policy': '/policies/{vhost}/{key}'
|
||||
}
|
||||
|
||||
DECLARABLE = {
|
||||
|
|
@ -69,7 +70,10 @@ DECLARABLE = {
|
|||
'optional': {}},
|
||||
'parameter': {'mandatory': ['component', 'key', 'value'],
|
||||
'json': ['value'],
|
||||
'optional': {}}
|
||||
'optional': {}},
|
||||
'policy': {'mandatory': ['vhost', 'key', 'pattern', 'definition'],
|
||||
'json': ['definition', 'priority'],
|
||||
'optional': {'priority' : 0}}
|
||||
}
|
||||
|
||||
DELETABLE = {
|
||||
|
|
@ -80,7 +84,8 @@ DELETABLE = {
|
|||
'vhost': {'mandatory': ['name']},
|
||||
'user': {'mandatory': ['name']},
|
||||
'permission': {'mandatory': ['vhost', 'user']},
|
||||
'parameter': {'mandatory': ['component', 'key']}
|
||||
'parameter': {'mandatory': ['component', 'key']},
|
||||
'policy': {'mandatory': ['vhost', 'key']}
|
||||
}
|
||||
|
||||
CLOSABLE = {
|
||||
|
|
|
|||
|
|
@ -535,6 +535,32 @@ or:
|
|||
<td class="path">/api/parameters/<i>component</i>/<i>vhost</i>/<i>key</i></td>
|
||||
<td>An individual parameter. To PUT a parameter, you will need a body looking something like this:
|
||||
<pre>{"vhost": "/","component":"federation","key":"local_username","value":"guest"}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>X</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="path">/api/policies</td>
|
||||
<td>A list of all policies.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>X</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="path">/api/policies/<i>vhost</i></td>
|
||||
<td>A list of all policies in a given virtual host.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>X</td>
|
||||
<td>X</td>
|
||||
<td>X</td>
|
||||
<td></td>
|
||||
<td class="path">/api/policies/<i>vhost</i>/<i>key</i></td>
|
||||
<td>An individual policy. To PUT a policy, you will need a body looking something like this:
|
||||
<pre>{"pattern":"^amq.", "definition": {"federation-upstream-set":"all"}, "priority":0}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -153,19 +153,19 @@ dispatcher_add(function(sammy) {
|
|||
update();
|
||||
return false;
|
||||
});
|
||||
path('#/policies', {'policies': '/parameters/policy',
|
||||
path('#/policies', {'policies': '/policies',
|
||||
'vhosts': '/vhosts'}, 'policies');
|
||||
sammy.get('#/policies/:vhost/:id', function() {
|
||||
render({'policy': '/parameters/policy/' + esc(this.params['vhost'])
|
||||
render({'policy': '/policies/' + esc(this.params['vhost'])
|
||||
+ '/' + esc(this.params['id'])},
|
||||
'policy', '#/policies');
|
||||
});
|
||||
sammy.put('#/policies', function() {
|
||||
put_parameter(this, ['key', 'pattern', 'policy'], ['priority'], []);
|
||||
put_policy(this, ['key', 'pattern', 'policy'], ['priority'], []);
|
||||
return false;
|
||||
});
|
||||
sammy.del('#/policies', function() {
|
||||
if (sync_delete(this, '/parameters/:component/:vhost/:key'))
|
||||
if (sync_delete(this, '/policies/:vhost/:key'))
|
||||
go_to('#/policies');
|
||||
return false;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -818,6 +818,21 @@ function put_parameter(sammy, mandatory_keys, num_keys, bool_keys) {
|
|||
if (sync_put(sammy, '/parameters/:component/:vhost/:key')) update();
|
||||
}
|
||||
|
||||
function put_policy(sammy, mandatory_keys, num_keys, bool_keys) {
|
||||
for (var i in sammy.params) {
|
||||
if (i === 'length' || !sammy.params.hasOwnProperty(i)) continue;
|
||||
if (sammy.params[i] == '' && mandatory_keys.indexOf(i) == -1) {
|
||||
delete sammy.params[i];
|
||||
}
|
||||
else if (num_keys.indexOf(i) != -1) {
|
||||
sammy.params[i] = parseInt(sammy.params[i]);
|
||||
}
|
||||
else if (bool_keys.indexOf(i) != -1) {
|
||||
sammy.params[i] = sammy.params[i] == 'true';
|
||||
}
|
||||
}
|
||||
if (sync_put(sammy, '/policies/:vhost/:key')) update();
|
||||
}
|
||||
|
||||
function debug(str) {
|
||||
$('<p>' + str + '</p>').appendTo('#debug');
|
||||
|
|
@ -854,4 +869,4 @@ function xmlHttpRequest() {
|
|||
});
|
||||
}
|
||||
});
|
||||
})(jQuery);
|
||||
})(jQuery);
|
||||
|
|
|
|||
|
|
@ -10,25 +10,24 @@
|
|||
<th>Virtual Host</th>
|
||||
<% } %>
|
||||
<th>Name</th>
|
||||
<th>Regexp</th>
|
||||
<th>Pattern</th>
|
||||
<th>Definition</th>
|
||||
<th>Priority</th>
|
||||
<th>Policy</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%
|
||||
var sorted_policies = policies.sort(function(a,b) {return b.value['priority'] || 0 - a.value['priority'] || 0} );
|
||||
for (var i = 0; i < sorted_policies.length; i++) {
|
||||
var policy = sorted_policies[i];
|
||||
for (var i = 0; i < policies.length; i++) {
|
||||
var policy = policies[i];
|
||||
%>
|
||||
<tr<%= alt_rows(i)%>>
|
||||
<% if (vhosts_interesting) { %>
|
||||
<td><%= fmt_string(policy.vhost) %></td>
|
||||
<% } %>
|
||||
<td><%= link_policy(policy.vhost, policy.key) %></td>
|
||||
<td><%= fmt_string(policy.value['pattern']) %></td>
|
||||
<td><%= fmt_string(policy.value['priority']) %></td>
|
||||
<td><%= fmt_table_short(policy.value['policy']) %></td>
|
||||
<td><%= fmt_string(policy.pattern) %></td>
|
||||
<td><%= fmt_table_short(policy.definition) %></td>
|
||||
<td><%= fmt_string(policy.priority) %></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
|
|
@ -44,7 +43,6 @@
|
|||
<div class="hider">
|
||||
<h3>
|
||||
<form action="#/policies" method="put">
|
||||
<input type="hidden" name="component" value="policy"/>
|
||||
<table class="form">
|
||||
<% if (vhosts_interesting) { %>
|
||||
<tr>
|
||||
|
|
@ -65,18 +63,18 @@
|
|||
<td><input type="text" name="key"/><span class="mand">*</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label>Regexp:</label></th>
|
||||
<th><label>Pattern:</label></th>
|
||||
<td><input type="text" name="pattern"/><span class="mand">*</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label>Definition:</label></th>
|
||||
<td><span class="multifield mand" id="definition"></span></td>
|
||||
<td><span class="mand">*</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label>Priority:</label></th>
|
||||
<td><input type="text" name="priority"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label>Policy:</label></th>
|
||||
<td><span class="multifield mand" id="policy"></span></td>
|
||||
<td><span class="mand">*</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
<input type="submit" value="Add policy"/>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -9,16 +9,16 @@
|
|||
<td><%= fmt_string(policy.vhost) %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Regexp</th>
|
||||
<td><%= fmt_string(policy.value.pattern) %></td>
|
||||
<th>Pattern</th>
|
||||
<td><%= fmt_string(policy.pattern) %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<td><%= fmt_table_short(policy.definition) %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Priority</th>
|
||||
<td><%= fmt_string(policy.value.priority) %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Policy</th>
|
||||
<td><%= fmt_table_short(policy.value.policy) %></td>
|
||||
<td><%= fmt_string(policy.priority) %></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ dispatcher() ->
|
|||
{["parameters", component], rabbit_mgmt_wm_parameters, []},
|
||||
{["parameters", component, vhost], rabbit_mgmt_wm_parameters, []},
|
||||
{["parameters", component, vhost, key], rabbit_mgmt_wm_parameter, []},
|
||||
{["policies"], rabbit_mgmt_wm_policies, []},
|
||||
{["policies", vhost], rabbit_mgmt_wm_policies, []},
|
||||
{["policies", vhost, key], rabbit_mgmt_wm_policy, []},
|
||||
{["connections"], rabbit_mgmt_wm_connections, []},
|
||||
{["connections", connection], rabbit_mgmt_wm_connection, []},
|
||||
{["connections", connection, "channels"], rabbit_mgmt_wm_connection_channels, []},
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ to_json(ReqData, Context) ->
|
|||
[{rabbit_version, list_to_binary(Vsn)}] ++
|
||||
filter(
|
||||
[{parameters, rabbit_mgmt_wm_parameters:basic(ReqData)},
|
||||
{policies, rabbit_mgmt_wm_policies:basic(ReqData)},
|
||||
{users, rabbit_mgmt_wm_users:users()},
|
||||
{vhosts, rabbit_mgmt_wm_vhosts:basic()},
|
||||
{permissions, rabbit_mgmt_wm_permissions:permissions()},
|
||||
|
|
@ -113,6 +114,7 @@ apply_defs(Body, SuccessFun, ErrorFun) ->
|
|||
{ok, _, All} ->
|
||||
try
|
||||
for_all(parameters, All, fun add_parameter/1),
|
||||
for_all(policies, All, fun add_policy/1),
|
||||
for_all(users, All, fun add_user/1),
|
||||
for_all(vhosts, All, fun add_vhost/1),
|
||||
for_all(permissions, All, fun add_permission/1),
|
||||
|
|
@ -156,6 +158,7 @@ export_name(_Name) -> true.
|
|||
|
||||
rw_state() ->
|
||||
[{parameters, [vhost, component, key, value]},
|
||||
{policies, [vhost, key, pattern, definition, priority]},
|
||||
{users, [name, password_hash, tags]},
|
||||
{vhosts, [name]},
|
||||
{permissions, [user, vhost, configure, write, read]},
|
||||
|
|
@ -202,6 +205,18 @@ add_parameter(Param) ->
|
|||
exit(list_to_binary(E ++ S))
|
||||
end.
|
||||
|
||||
add_policy(Param) ->
|
||||
VHost = pget(vhost, Param),
|
||||
Key = pget(key, Param),
|
||||
case rabbit_policy:set(
|
||||
VHost, Key, pget(pattern, Param),
|
||||
rabbit_misc:json_to_term(pget(definition, Param)),
|
||||
pget(priority, Param)) of
|
||||
ok -> ok;
|
||||
{error_string, E} -> S = rabbit_misc:format(" (~s/~s)", [VHost, Key]),
|
||||
exit(list_to_binary(E ++ S))
|
||||
end.
|
||||
|
||||
add_user(User) ->
|
||||
rabbit_mgmt_wm_user:put_user(User).
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
%% The contents of this file are subject to the Mozilla Public License
|
||||
%% Version 1.1 (the "License"); you may not use this file except in
|
||||
%% compliance with the License. You may obtain a copy of the License at
|
||||
%% http://www.mozilla.org/MPL/
|
||||
%%
|
||||
%% Software distributed under the License is distributed on an "AS IS"
|
||||
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
|
||||
%% License for the specific language governing rights and limitations
|
||||
%% under the License.
|
||||
%%
|
||||
%% The Original Code is RabbitMQ Management Plugin.
|
||||
%%
|
||||
%% The Initial Developer of the Original Code is VMware, Inc.
|
||||
%% Copyright (c) 2010-2012 VMware, Inc. All rights reserved.
|
||||
%%
|
||||
|
||||
-module(rabbit_mgmt_wm_policies).
|
||||
|
||||
-export([init/1, to_json/2, content_types_provided/2, is_authorized/2,
|
||||
resource_exists/2, basic/1]).
|
||||
|
||||
-include("rabbit_mgmt.hrl").
|
||||
-include_lib("webmachine/include/webmachine.hrl").
|
||||
-include_lib("rabbit_common/include/rabbit.hrl").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init(_Config) -> {ok, #context{}}.
|
||||
|
||||
content_types_provided(ReqData, Context) ->
|
||||
{[{"application/json", to_json}], ReqData, Context}.
|
||||
|
||||
resource_exists(ReqData, Context) ->
|
||||
{case basic(ReqData) of
|
||||
not_found -> false;
|
||||
_ -> true
|
||||
end, ReqData, Context}.
|
||||
|
||||
to_json(ReqData, Context) ->
|
||||
rabbit_mgmt_util:reply_list(basic(ReqData), ["priority"], ReqData, Context).
|
||||
|
||||
is_authorized(ReqData, Context) ->
|
||||
rabbit_mgmt_util:is_authorized_admin(ReqData, Context).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
basic(ReqData) ->
|
||||
case rabbit_mgmt_util:vhost(ReqData) of
|
||||
not_found -> not_found;
|
||||
none -> rabbit_policy:list();
|
||||
VHost -> rabbit_policy:list(VHost)
|
||||
end.
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
%% The contents of this file are subject to the Mozilla Public License
|
||||
%% Version 1.1 (the "License"); you may not use this file except in
|
||||
%% compliance with the License. You may obtain a copy of the License at
|
||||
%% http://www.mozilla.org/MPL/
|
||||
%%
|
||||
%% Software distributed under the License is distributed on an "AS IS"
|
||||
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
|
||||
%% License for the specific language governing rights and limitations
|
||||
%% under the License.
|
||||
%%
|
||||
%% The Original Code is RabbitMQ Management Plugin.
|
||||
%%
|
||||
%% The Initial Developer of the Original Code is VMware, Inc.
|
||||
%% Copyright (c) 2010-2012 VMware, Inc. All rights reserved.
|
||||
%%
|
||||
|
||||
-module(rabbit_mgmt_wm_policy).
|
||||
|
||||
-export([init/1, resource_exists/2, to_json/2,
|
||||
content_types_provided/2, content_types_accepted/2,
|
||||
is_authorized/2, allowed_methods/2, accept_content/2,
|
||||
delete_resource/2]).
|
||||
|
||||
-import(rabbit_misc, [pget/2]).
|
||||
|
||||
-include("rabbit_mgmt.hrl").
|
||||
-include_lib("webmachine/include/webmachine.hrl").
|
||||
-include_lib("rabbit_common/include/rabbit.hrl").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init(_Config) -> {ok, #context{}}.
|
||||
|
||||
content_types_provided(ReqData, Context) ->
|
||||
{[{"application/json", to_json}], ReqData, Context}.
|
||||
|
||||
content_types_accepted(ReqData, Context) ->
|
||||
{[{"application/json", accept_content}], ReqData, Context}.
|
||||
|
||||
allowed_methods(ReqData, Context) ->
|
||||
{['HEAD', 'GET', 'PUT', 'DELETE'], ReqData, Context}.
|
||||
|
||||
resource_exists(ReqData, Context) ->
|
||||
{case policy(ReqData) of
|
||||
not_found -> false;
|
||||
_ -> true
|
||||
end, ReqData, Context}.
|
||||
|
||||
to_json(ReqData, Context) ->
|
||||
rabbit_mgmt_util:reply(policy(ReqData), ReqData, Context).
|
||||
|
||||
accept_content(ReqData, Context) ->
|
||||
case rabbit_mgmt_util:vhost(ReqData) of
|
||||
not_found ->
|
||||
rabbit_mgmt_util:not_found(vhost_not_found, ReqData, Context);
|
||||
VHost ->
|
||||
rabbit_mgmt_util:with_decode(
|
||||
[pattern, definition], ReqData, Context,
|
||||
fun([Pattern, Definition], Body) ->
|
||||
case rabbit_policy:set(
|
||||
VHost, key(ReqData), Pattern,
|
||||
rabbit_misc:json_to_term(Definition),
|
||||
proplists:get_value(priority, Body)) of
|
||||
ok ->
|
||||
{true, ReqData, Context};
|
||||
{error_string, Reason} ->
|
||||
rabbit_mgmt_util:bad_request(
|
||||
list_to_binary(Reason), ReqData, Context)
|
||||
end
|
||||
end)
|
||||
end.
|
||||
|
||||
delete_resource(ReqData, Context) ->
|
||||
ok = rabbit_policy:delete(
|
||||
rabbit_mgmt_util:vhost(ReqData), key(ReqData)),
|
||||
{true, ReqData, Context}.
|
||||
|
||||
is_authorized(ReqData, Context) ->
|
||||
rabbit_mgmt_util:is_authorized_admin(ReqData, Context).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
policy(ReqData) ->
|
||||
rabbit_policy:lookup(
|
||||
rabbit_mgmt_util:vhost(ReqData), key(ReqData)).
|
||||
|
||||
key(ReqData) -> rabbit_mgmt_util:id(key, ReqData).
|
||||
|
|
@ -64,11 +64,9 @@ ha_test_() ->
|
|||
{timeout, 60, fun ha/0}.
|
||||
|
||||
ha() ->
|
||||
Policy = [{value, [{pattern, <<".*">>},
|
||||
{policy, [{'ha-mode', <<"all">>}]
|
||||
}]
|
||||
}],
|
||||
http_put("/parameters/policy/%2f/HA", Policy, ?NO_CONTENT),
|
||||
Policy = [{pattern, <<".*">>},
|
||||
{definition, [{'ha-mode', <<"all">>}]}],
|
||||
http_put("/policies/%2f/HA", Policy, ?NO_CONTENT),
|
||||
QArgs = [{node, <<"hare">>}],
|
||||
http_put("/queues/%2f/ha-queue", QArgs, ?NO_CONTENT),
|
||||
Q = wait_for("/queues/%2f/ha-queue"),
|
||||
|
|
|
|||
|
|
@ -553,6 +553,7 @@ unicode_test() ->
|
|||
ok.
|
||||
|
||||
definitions_test() ->
|
||||
rabbit_runtime_parameters_test:register_policy_validator(),
|
||||
XArgs = [{type, <<"direct">>}],
|
||||
QArgs = [],
|
||||
http_put("/queues/%2f/my-queue", QArgs, ?NO_CONTENT),
|
||||
|
|
@ -576,13 +577,12 @@ definitions_test() ->
|
|||
[{users, []},
|
||||
{vhosts, []},
|
||||
{permissions, []},
|
||||
{parameters, [[{value, [{<<"pattern">>, <<".*">>},
|
||||
{<<"priority">>, 1},
|
||||
{<<"policy">>, [{<<"a">>, <<"b">>}]}
|
||||
]},
|
||||
{vhost, <<"/">>},
|
||||
{component,<<"policy">>},
|
||||
{key, <<"test">>}]]},
|
||||
{policies, [[{vhost, <<"/">>},
|
||||
{key, <<"test">>},
|
||||
{pattern, <<".*">>},
|
||||
{definition, [{testpos, [1, 2, 3]}]},
|
||||
{priority, 1}
|
||||
]]},
|
||||
{queues, [[{name, <<"another-queue">>},
|
||||
{vhost, <<"/">>},
|
||||
{durable, true},
|
||||
|
|
@ -607,7 +607,8 @@ definitions_test() ->
|
|||
http_post("/definitions", ExtraConfig, ?CREATED),
|
||||
http_post("/definitions", BrokenConfig, ?BAD_REQUEST),
|
||||
http_delete("/queues/%2f/another-queue", ?NO_CONTENT),
|
||||
http_delete("/parameters/policy/%2f/test", ?NO_CONTENT),
|
||||
http_delete("/policies/%2f/test", ?NO_CONTENT),
|
||||
rabbit_runtime_parameters_test:unregister_policy_validator(),
|
||||
ok.
|
||||
|
||||
definitions_remove_things_test() ->
|
||||
|
|
@ -935,6 +936,39 @@ parameters_test() ->
|
|||
rabbit_runtime_parameters_test:unregister(),
|
||||
ok.
|
||||
|
||||
policy_test() ->
|
||||
rabbit_runtime_parameters_test:register_policy_validator(),
|
||||
PolicyPos = [{vhost,<<"/">>},
|
||||
{key,<<"policy_pos">>},
|
||||
{pattern,<<".*">>},
|
||||
{definition,[{testpos,[1,2,3]}]},
|
||||
{priority,10}],
|
||||
PolicyEven = [{vhost,<<"/">>},
|
||||
{key,<<"policy_even">>},
|
||||
{pattern,<<".*">>},
|
||||
{definition,[{testeven,[1,2,3,4]}]},
|
||||
{priority,10}],
|
||||
http_put(
|
||||
"/policies/%2f/policy_pos",
|
||||
lists:keydelete(key, 1, PolicyPos),
|
||||
?NO_CONTENT),
|
||||
http_put(
|
||||
"/policies/%2f/policy_even",
|
||||
lists:keydelete(key, 1, PolicyEven),
|
||||
?NO_CONTENT),
|
||||
assert_item(PolicyPos, http_get("/policies/%2f/policy_pos", ?OK)),
|
||||
assert_item(PolicyEven, http_get("/policies/%2f/policy_even", ?OK)),
|
||||
List = [PolicyPos, PolicyEven],
|
||||
assert_list(List, http_get("/policies", ?OK)),
|
||||
assert_list(List, http_get("/policies/%2f", ?OK)),
|
||||
|
||||
http_delete("/policies/%2f/policy_pos", ?NO_CONTENT),
|
||||
http_delete("/policies/%2f/policy_even", ?NO_CONTENT),
|
||||
0 = length(http_get("/policies")),
|
||||
0 = length(http_get("/policies/%2f")),
|
||||
rabbit_runtime_parameters_test:unregister_policy_validator(),
|
||||
ok.
|
||||
|
||||
%%---------------------------------------------------------------------------
|
||||
|
||||
msg(Key, Headers, Body) ->
|
||||
|
|
|
|||
Loading…
Reference in New Issue