diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index 141eacdd1..e57806d38 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -148,7 +148,8 @@ code_change(_OldVsn, State, _Extra) -> deep_put_handler([], Handlers, Mod) -> {ok, Handlers#{?MOD => Mod}}; -deep_put_handler([Key | KeyPath], Handlers, Mod) -> +deep_put_handler([Key0 | KeyPath], Handlers, Mod) -> + Key = atom(Key0), SubHandlers = maps:get(Key, Handlers, #{}), case deep_put_handler(KeyPath, SubHandlers, Mod) of {ok, NewSubHandlers} -> diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 69e19df88..6830eda00 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -117,17 +117,20 @@ fields("cluster") -> #{ mapping => "ekka.cluster_name" , default => emqxcl , desc => "Human-friendly name of the EMQX cluster." + , 'readOnly' => true })} , {"discovery_strategy", sc(hoconsc:enum([manual, static, mcast, dns, etcd, k8s]), #{ default => manual , desc => "Service discovery method for the cluster nodes." + , 'readOnly' => true })} , {"autoclean", sc(emqx_schema:duration(), #{ mapping => "ekka.cluster_autoclean" , default => "5m" , desc => "Remove disconnected nodes from the cluster after this interval." + , 'readOnly' => true })} , {"autoheal", sc(boolean(), @@ -135,12 +138,14 @@ fields("cluster") -> , default => true , desc => "If true, the node will try to heal network partitions automatically." - })} + , 'readOnly' => true + })} , {"proto_dist", sc(hoconsc:enum([inet_tcp, inet6_tcp, inet_tls]), #{ mapping => "ekka.proto_dist" , default => inet_tcp - })} + , 'readOnly' => true + })} , {"static", sc(ref(cluster_static), #{ desc => "Service discovery via static nodes. The new node joins the cluster by @@ -169,7 +174,8 @@ fields(cluster_static) -> sc(hoconsc:array(atom()), #{ default => [] , desc => "List EMQX node names in the static cluster. See node.name." - })} + , 'readOnly' => true + })} ]; fields(cluster_mcast) -> @@ -177,10 +183,12 @@ fields(cluster_mcast) -> sc(string(), #{ default => "239.192.0.1" , desc => "Multicast IPv4 address." - })} + , 'readOnly' => true + })} , {"ports", sc(hoconsc:array(integer()), #{ default => [4369, 4370] + , 'readOnly' => true , desc => "List of UDP ports used for service discovery.
Note: probe messages are broadcast to all the specified ports." })} @@ -188,32 +196,38 @@ Note: probe messages are broadcast to all the specified ports." sc(string(), #{ default => "0.0.0.0" , desc => "Local IP address the node discovery service needs to bind to." - })} + , 'readOnly' => true + })} , {"ttl", sc(range(0, 255), #{ default => 255 , desc => "Time-to-live (TTL) for the outgoing UDP datagrams." - })} + , 'readOnly' => true + })} , {"loop", sc(boolean(), #{ default => true , desc => "If true, loop UDP datagrams back to the local socket." - })} + , 'readOnly' => true + })} , {"sndbuf", sc(emqx_schema:bytesize(), #{ default => "16KB" , desc => "Size of the kernel-level buffer for outgoing datagrams." - })} + , 'readOnly' => true + })} , {"recbuf", sc(emqx_schema:bytesize(), #{ default => "16KB" , desc => "Size of the kernel-level buffer for incoming datagrams." - })} + , 'readOnly' => true + })} , {"buffer", sc(emqx_schema:bytesize(), #{ default =>"32KB" , desc => "Size of the user-level buffer." - })} + , 'readOnly' => true + })} ]; fields(cluster_dns) -> @@ -221,11 +235,13 @@ fields(cluster_dns) -> sc(string(), #{ default => "localhost" , desc => "The domain name of the EMQX cluster." - })} + , 'readOnly' => true + })} , {"app", sc(string(), #{ default => "emqx" , desc => "The symbolic name of the EMQX service." + , 'readOnly' => true })} ]; @@ -233,21 +249,25 @@ fields(cluster_etcd) -> [ {"server", sc(emqx_schema:comma_separated_list(), #{ desc => "List of endpoint URLs of the etcd cluster" + , 'readOnly' => true })} , {"prefix", sc(string(), #{ default => "emqxcl" , desc => "Key prefix used for EMQX service discovery." + , 'readOnly' => true })} , {"node_ttl", sc(emqx_schema:duration(), #{ default => "1m" + , 'readOnly' => true , desc => "Expiration time of the etcd key associated with the node. It is refreshed automatically, as long as the node is alive." })} , {"ssl", sc(hoconsc:ref(emqx_schema, ssl_client_opts), #{ desc => "Options for the TLS connection to the etcd cluster." + , 'readOnly' => true })} ]; @@ -255,19 +275,23 @@ fields(cluster_k8s) -> [ {"apiserver", sc(string(), #{ desc => "Kubernetes API endpoint URL." + , 'readOnly' => true })} , {"service_name", sc(string(), #{ default => "emqx" , desc => "EMQX broker service name." + , 'readOnly' => true })} , {"address_type", sc(hoconsc:enum([ip, dns, hostname]), #{ desc => "Address type used for connecting to the discovered nodes." + , 'readOnly' => true })} , {"app_name", sc(string(), #{ default => "emqx" + , 'readOnly' => true , desc => "This parameter should be set to the part of the node.name before the '@'.
For example, if the node.name is emqx@127.0.0.1, then this parameter @@ -277,10 +301,12 @@ should be set to emqx." sc(string(), #{ default => "default" , desc => "Kubernetes namespace." + , 'readOnly' => true })} , {"suffix", sc(string(), #{ default => "pod.local" + , 'readOnly' => true , desc => "Node name suffix.
Note: this parameter is only relevant when address_type is dns or hostname." @@ -290,14 +316,16 @@ or hostname." fields("node") -> [ {"name", sc(string(), - #{ default => "emqx@127.0.0.1", - desc => "Unique name of the EMQX node. It must follow %name%@FQDN or + #{ default => "emqx@127.0.0.1" + , 'readOnly' => true + , desc => "Unique name of the EMQX node. It must follow %name%@FQDN or %name%@IPv4 format." })} , {"cookie", sc(string(), #{ mapping => "vm_args.-setcookie", default => "emqxsecretcookie", + 'readOnly' => true, sensitive => true, desc => "Secret cookie is a random string that should be the same on all nodes in the given EMQX cluster, but unique per EMQX cluster. It is used to prevent EMQX nodes that @@ -306,6 +334,7 @@ fields("node") -> , {"data_dir", sc(string(), #{ required => true, + 'readOnly' => true, mapping => "emqx.data_dir", desc => """ @@ -327,6 +356,7 @@ Possible auto-created subdirectories are: sc(list(string()), #{ mapping => "emqx.config_files" , default => undefined + , 'readOnly' => true , desc => "List of configuration files that are read during startup. The order is significant: later configuration files override the previous ones." })} @@ -335,11 +365,13 @@ Possible auto-created subdirectories are: #{ mapping => "emqx_machine.global_gc_interval" , default => "15m" , desc => "Periodic garbage collection interval." + , 'readOnly' => true })} , {"crash_dump_file", sc(file(), #{ mapping => "vm_args.-env ERL_CRASH_DUMP" , desc => "Location of the crash dump file" + , 'readOnly' => true })} , {"crash_dump_seconds", sc(emqx_schema:duration_s(), @@ -347,17 +379,20 @@ Possible auto-created subdirectories are: , default => "30s" , desc => "The number of seconds that the broker is allowed to spend writing a crash dump" + , 'readOnly' => true })} , {"crash_dump_bytes", sc(emqx_schema:bytesize(), #{ mapping => "vm_args.-env ERL_CRASH_DUMP_BYTES" , default => "100MB" , desc => "The maximum size of a crash dump file in bytes." + , 'readOnly' => true })} , {"dist_net_ticktime", sc(emqx_schema:duration(), #{ mapping => "vm_args.-kernel net_ticktime" , default => "2m" + , 'readOnly' => true , desc => "This is the approximate time an EMQX node may be unresponsive " "until it is considered down and thereby disconnected." })} @@ -365,6 +400,7 @@ a crash dump" sc(integer(), #{ mapping => "emqx_machine.backtrace_depth" , default => 23 + , 'readOnly' => true , desc => "Maximum depth of the call stack printed in error messages and process_info." })} @@ -372,18 +408,21 @@ a crash dump" sc(emqx_schema:comma_separated_atoms(), #{ mapping => "emqx_machine.applications" , default => [] + , 'readOnly' => true , desc => "List of Erlang applications that shall be rebooted when the EMQX broker joins the cluster." })} , {"etc_dir", sc(string(), #{ desc => "etc dir for the node" + , 'readOnly' => true } )} , {"cluster_call", sc(ref("cluster_call"), #{ desc => "Options for the 'cluster call' feature that allows to execute a callback on all nodes in the cluster." + , 'readOnly' => true } )} ]; @@ -393,6 +432,7 @@ fields("db") -> sc(hoconsc:enum([mnesia, rlog]), #{ mapping => "mria.db_backend" , default => rlog + , 'readOnly' => true , desc => """ Select the backend for the embedded database.
rlog is the default backend, a new experimental backend @@ -404,6 +444,7 @@ that is suitable for very large clusters.
sc(hoconsc:enum([core, replicant]), #{ mapping => "mria.node_role" , default => core + , 'readOnly' => true , desc => """ Select a node role.
core nodes provide durability of the data, and take care of writes. @@ -419,6 +460,7 @@ to rlog. sc(emqx_schema:comma_separated_atoms(), #{ mapping => "mria.core_nodes" , default => [] + , 'readOnly' => true , desc => """ List of core nodes that the replicant will connect to.
Note: this parameter only takes effect when the backend is set @@ -432,6 +474,7 @@ there is no need to set this value. sc(hoconsc:enum([gen_rpc, rpc]), #{ mapping => "mria.rlog_rpc_module" , default => gen_rpc + , 'readOnly' => true , desc => """ Protocol used for pushing transaction logs to the replicant nodes. """ @@ -440,6 +483,7 @@ Protocol used for pushing transaction logs to the replicant nodes. sc(hoconsc:enum([sync, async]), #{ mapping => "mria.tlog_push_mode" , default => async + , 'readOnly' => true , desc => """ In sync mode the core node waits for an ack from the replicant nodes before sending the next transaction log entry. diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 74c7dd0c2..a0b733d33 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -20,6 +20,8 @@ -export([ start_listeners/0 + , start_listeners/1 + , stop_listeners/1 , stop_listeners/0]). %% Authorization @@ -37,6 +39,14 @@ %%-------------------------------------------------------------------- start_listeners() -> + Listeners = emqx_conf:get([dashboard, listeners], []), + start_listeners(Listeners). + +stop_listeners() -> + Listeners = emqx_conf:get([dashboard, listeners], []), + stop_listeners(Listeners). + +start_listeners(Listeners) -> {ok, _} = application:ensure_all_started(minirest), Authorization = {?MODULE, authorize}, GlobalSpec = #{ @@ -73,13 +83,13 @@ start_listeners() -> %% Don't record the reason because minirest already does(too much logs noise). [Name | Acc] end - end, [], listeners()), + end, [], listeners(Listeners)), case Res of [] -> ok; _ -> {error, Res} end. -stop_listeners() -> +stop_listeners(Listeners) -> [begin case minirest:stop(Name) of ok -> @@ -87,7 +97,8 @@ stop_listeners() -> {error, not_found} -> ?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port}) end - end || {Name, _, Port, _} <- listeners()]. + end || {Name, _, Port, _} <- listeners(Listeners)], + ok. %%-------------------------------------------------------------------- %% internal @@ -99,14 +110,14 @@ apps() -> _ -> false end]. -listeners() -> +listeners(Listeners) -> [begin Protocol = maps:get(protocol, ListenerOption0, http), {ListenerOption, Bind} = ip_port(ListenerOption0), Name = listener_name(Protocol, ListenerOption), RanchOptions = ranch_opts(maps:without([protocol], ListenerOption)), {Name, Protocol, Bind, RanchOptions} - end || ListenerOption0 <- emqx_conf:get([dashboard, listeners], [])]. + end || ListenerOption0 <- Listeners]. ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_app.erl b/apps/emqx_dashboard/src/emqx_dashboard_app.erl index ac54296c4..869db84f1 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_app.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_app.erl @@ -31,10 +31,12 @@ start(_StartType, _StartArgs) -> ok -> emqx_dashboard_cli:load(), {ok, _Result} = emqx_dashboard_admin:add_default_user(), + ok = emqx_dashboard_config:add_handler(), {ok, Sup}; {error, Reason} -> {error, Reason} end. stop(_State) -> + ok = emqx_dashboard_config:remove_handler(), emqx_dashboard_cli:unload(), emqx_dashboard:stop_listeners(). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_config.erl b/apps/emqx_dashboard/src/emqx_dashboard_config.erl new file mode 100644 index 000000000..2df500797 --- /dev/null +++ b/apps/emqx_dashboard/src/emqx_dashboard_config.erl @@ -0,0 +1,43 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_dashboard_config). + +-behaviour(emqx_config_handler). + +%% API +-export([add_handler/0, remove_handler/0]). +-export([post_config_update/5]). + +add_handler() -> + Roots = emqx_dashboard_schema:roots(), + ok = emqx_config_handler:add_handler(Roots, ?MODULE), + ok. + +remove_handler() -> + Roots = emqx_dashboard_schema:roots(), + ok = emqx_config_handler:remove_handler(Roots), + ok. + +post_config_update(_, _Req, NewConf, OldConf, _AppEnvs) -> + #{listeners := NewListeners} = NewConf, + #{listeners := OldListeners} = OldConf, + case NewListeners =:= OldListeners of + true -> ok; + false -> + ok = emqx_dashboard:stop_listeners(OldListeners), + ok = emqx_dashboard:start_listeners(NewListeners) + end, + ok. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 3cd2cd195..bde970a53 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -105,11 +105,13 @@ default_username(type) -> string(); default_username(default) -> "admin"; default_username(required) -> true; default_username(desc) -> "The default username of the automatically created dashboard user."; +default_username('readOnly') -> true; default_username(_) -> undefined. default_password(type) -> string(); default_password(default) -> "public"; default_password(required) -> true; +default_password('readOnly') -> true; default_password(sensitive) -> true; default_password(desc) -> """ The initial default password for dashboard 'admin' user.