diff --git a/deps/rabbit/priv/schema/rabbit.schema b/deps/rabbit/priv/schema/rabbit.schema index f34a1ea70b30..e8d19fa21ec2 100644 --- a/deps/rabbit/priv/schema/rabbit.schema +++ b/deps/rabbit/priv/schema/rabbit.schema @@ -897,6 +897,19 @@ end}. end }. +{mapping, "vhost_max", "rabbit.vhost_max", + [{datatype, [{atom, infinity}, integer]}]}. + +{translation, "rabbit.vhost_max", + fun(Conf) -> + case cuttlefish:conf_get("vhost_max", Conf, undefined) of + undefined -> cuttlefish:unset(); + infinity -> infinity; + Val when is_integer(Val) -> Val; + _ -> cuttlefish:invalid("should be a non-negative integer") + end + end +}. {mapping, "max_message_size", "rabbit.max_message_size", [{datatype, integer}, {validators, ["max_message_size"]}]}. diff --git a/deps/rabbit/src/rabbit_vhost.erl b/deps/rabbit/src/rabbit_vhost.erl index 24bca6f80896..5c5f9afcfbb5 100644 --- a/deps/rabbit/src/rabbit_vhost.erl +++ b/deps/rabbit/src/rabbit_vhost.erl @@ -162,6 +162,7 @@ add(Name, Metadata, ActingUser) -> end. do_add(Name, Metadata, ActingUser) -> + ok = is_over_vhost_limit(Name), Description = maps:get(description, Metadata, undefined), Tags = maps:get(tags, Metadata, []), @@ -194,6 +195,7 @@ do_add(Name, Metadata, ActingUser) -> [Name, Description, Tags]) end, DefaultLimits = rabbit_db_vhost_defaults:list_limits(Name), + {NewOrNot, VHost} = rabbit_db_vhost:create_or_get(Name, DefaultLimits, Metadata), case NewOrNot of new -> @@ -331,6 +333,23 @@ put_vhost(Name, Description, Tags0, DefaultQueueType, Trace, Username) -> end, Result. +is_over_vhost_limit(Name) -> + Limit = rabbit_misc:get_env(rabbit, vhost_max, infinity), + is_over_vhost_limit(Name, Limit). + +is_over_vhost_limit(_Name, infinity) -> + ok; +is_over_vhost_limit(Name, Limit) when is_integer(Limit) -> + case length(rabbit_db_vhost:list()) >= Limit of + false -> + ok; + true -> + ErrorMsg = rabbit_misc:format("cannot create vhost '~ts': " + "vhost limit '~tp' is reached", + [Name, Limit]), + exit({vhost_precondition_failed, ErrorMsg}) + end. + %% when definitions are loaded on boot, Username here will be ?INTERNAL_USER, %% which does not actually exist maybe_grant_full_permissions(_Name, ?INTERNAL_USER) -> diff --git a/deps/rabbit/test/per_node_limit_SUITE.erl b/deps/rabbit/test/per_node_limit_SUITE.erl index 6d7c0499ed04..4a19bc04aa84 100644 --- a/deps/rabbit/test/per_node_limit_SUITE.erl +++ b/deps/rabbit/test/per_node_limit_SUITE.erl @@ -21,7 +21,8 @@ all() -> groups() -> [ {parallel_tests, [parallel], [ - node_connection_limit + node_connection_limit, + vhost_limit ]} ]. @@ -58,6 +59,9 @@ end_per_group(_Group, Config) -> init_per_testcase(Testcase, Config) -> rabbit_ct_helpers:testcase_started(Config, Testcase). +end_per_testcase(vhost_limit = Testcase, Config) -> + [rabbit_ct_broker_helpers:delete_vhost(Config, integer_to_binary(I)) || I <- lists:seq(1,4)], + rabbit_ct_helpers:testcase_finished(Config, Testcase); end_per_testcase(Testcase, Config) -> rabbit_ct_helpers:testcase_finished(Config, Testcase). @@ -78,7 +82,25 @@ node_connection_limit(Config) -> set_node_limit(Config, infinity), C = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 0), - true = is_pid(C). + true = is_pid(C), + close_all_connections([C]), + ok. + +vhost_limit(Config) -> + set_vhost_limit(Config, 0), + {'EXIT',{vhost_precondition_failed, _}} = rabbit_ct_broker_helpers:add_vhost(Config, <<"foo">>), + + set_vhost_limit(Config, 5), + [ok = rabbit_ct_broker_helpers:add_vhost(Config, integer_to_binary(I)) || I <- lists:seq(1,4)], + {'EXIT',{vhost_precondition_failed, _}} = rabbit_ct_broker_helpers:add_vhost(Config, <<"5">>), + [rabbit_ct_broker_helpers:delete_vhost(Config, integer_to_binary(I)) || I <- lists:seq(1,4)], + + set_vhost_limit(Config, infinity), + [ok = rabbit_ct_broker_helpers:add_vhost(Config, integer_to_binary(I)) || I <- lists:seq(1,4)], + ok = rabbit_ct_broker_helpers:add_vhost(Config, <<"5">>), + [rabbit_ct_broker_helpers:delete_vhost(Config, integer_to_binary(I)) || I <- lists:seq(1,5)], + ok. + %% ------------------------------------------------------------------- %% Implementation @@ -97,3 +119,8 @@ set_node_limit(Config, Limit) -> rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbit, connection_max, Limit]). + +set_vhost_limit(Config, Limit) -> + rabbit_ct_broker_helpers:rpc(Config, 0, + application, + set_env, [rabbit, vhost_max, Limit]). diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/add_vhost_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/add_vhost_command.ex index 62f4c8fca96a..4a4d743f2bfc 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/add_vhost_command.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/add_vhost_command.ex @@ -51,6 +51,10 @@ defmodule RabbitMQ.CLI.Ctl.Commands.AddVhostCommand do {:error, ExitCodes.exit_usage(), "Unsupported default queue type"} end + def output({:badrpc, {:EXIT, {:vhost_precondition_failed, msg}}}, _opts) do + {:error, ExitCodes.exit_usage(), msg} + end + use RabbitMQ.CLI.DefaultOutput def usage, diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhost.erl b/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhost.erl index 60075129cd12..ef40bf8fc6d2 100644 --- a/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhost.erl +++ b/deps/rabbitmq_management/src/rabbit_mgmt_wm_vhost.erl @@ -60,25 +60,28 @@ accept_content(ReqData0, Context = #context{user = #user{username = Username}}) rabbit_mgmt_util:with_decode( [], ReqData0, Context, fun(_, BodyMap, ReqData) -> - Trace = rabbit_mgmt_util:parse_bool(maps:get(tracing, BodyMap, undefined)), - Description = maps:get(description, BodyMap, <<"">>), - Tags = maps:get(tags, BodyMap, <<"">>), - %% defaultqueuetype was an unfortunate name picked originally for 3.11.0, - %% so fall back to it. See rabbitmq/rabbitmq-server#7734. - FallbackQT = maps:get(defaultqueuetype, BodyMap, undefined), - DefaultQT = maps:get(default_queue_type, BodyMap, FallbackQT), - case put_vhost(Name, Description, Tags, DefaultQT, Trace, Username) of - ok -> - {true, ReqData, Context}; - {error, timeout} = E -> - rabbit_mgmt_util:internal_server_error( - "Timed out while waiting for the vhost to initialise", E, - ReqData0, Context); - {error, E} -> - rabbit_mgmt_util:internal_server_error( - "Error occured while adding vhost", E, - ReqData0, Context) - end + Trace = rabbit_mgmt_util:parse_bool(maps:get(tracing, BodyMap, undefined)), + Description = maps:get(description, BodyMap, <<"">>), + Tags = maps:get(tags, BodyMap, <<"">>), + %% defaultqueuetype was an unfortunate name picked originally for 3.11.0, + %% so fall back to it. See rabbitmq/rabbitmq-server#7734. + FallbackQT = maps:get(defaultqueuetype, BodyMap, undefined), + DefaultQT = maps:get(default_queue_type, BodyMap, FallbackQT), + case put_vhost(Name, Description, Tags, DefaultQT, Trace, Username) of + ok -> + {true, ReqData, Context}; + {'EXIT', {vhost_precondition_failed, + Explanation}} -> + rabbit_mgmt_util:bad_request(list_to_binary(Explanation), ReqData, Context); + {error, timeout} = E -> + rabbit_mgmt_util:internal_server_error( + "Timed out while waiting for the vhost to initialise", E, + ReqData0, Context); + {error, E} -> + rabbit_mgmt_util:internal_server_error( + "Error occured while adding vhost", E, + ReqData0, Context) + end end). delete_resource(ReqData, Context = #context{user = #user{username = Username}}) ->