blob: d0ecd2cd9e5d392c37220fe46a50294fc0ee2bd8 [file] [log] [blame]
% 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(setup).
-export([enable_cluster/1, finish_cluster/1, add_node/1, receive_cookie/1]).
-export([is_cluster_enabled/0, has_cluster_system_dbs/1, cluster_system_dbs/0]).
-export([enable_single_node/1, is_single_node_enabled/1]).
-include_lib("../couch/include/couch_db.hrl").
require_admins(undefined, {undefined, undefined}) ->
% no admin in CouchDB, no admin in request
throw({error, "Cluster setup requires admin account to be configured"});
require_admins(_,_) ->
ok.
require_node_count(undefined) ->
throw({error, "Cluster setup requires node_count to be configured"});
require_node_count(_) ->
ok.
error_bind_address() ->
throw({error, "Cluster setup requires bind_addres != 127.0.0.1"}).
require_bind_address("127.0.0.1", undefined) ->
error_bind_address();
require_bind_address("127.0.0.1", <<"127.0.0.1">>) ->
error_bind_address();
require_bind_address(_, _) ->
ok.
is_cluster_enabled() ->
% bind_address != 127.0.0.1 AND admins != empty
BindAddress = config:get("chttpd", "bind_address"),
Admins = config:get("admins"),
case {BindAddress, Admins} of
{"127.0.0.1", _} -> false;
{_,[]} -> false;
{_,_} -> true
end.
is_single_node_enabled(Dbs) ->
% admins != empty AND dbs exist
Admins = config:get("admins"),
HasDbs = has_cluster_system_dbs(Dbs),
case {Admins, HasDbs} of
{[], _} -> false;
{_, false} -> false;
{_,_} -> true
end.
cluster_system_dbs() ->
["_users", "_replicator", "_global_changes"].
has_cluster_system_dbs([]) ->
true;
has_cluster_system_dbs([Db|Dbs]) ->
case catch fabric:get_db_info(Db) of
{ok, _} -> has_cluster_system_dbs(Dbs);
_ -> false
end.
enable_cluster(Options) ->
case couch_util:get_value(remote_node, Options, undefined) of
undefined ->
enable_cluster_int(Options, is_cluster_enabled());
_ ->
enable_cluster_http(Options)
end.
get_remote_request_options(Options) ->
case couch_util:get_value(remote_current_user, Options, undefined) of
undefined ->
[];
_ ->
[
{basic_auth, {
binary_to_list(couch_util:get_value(remote_current_user, Options)),
binary_to_list(couch_util:get_value(remote_current_password, Options))
}}
]
end.
enable_cluster_http(Options) ->
% POST to nodeB/_setup
RequestOptions = get_remote_request_options(Options),
AdminUsername = couch_util:get_value(username, Options),
AdminPasswordHash = config:get("admins", binary_to_list(AdminUsername)),
Body = ?JSON_ENCODE({[
{<<"action">>, <<"enable_cluster">>},
{<<"username">>, AdminUsername},
{<<"password_hash">>, ?l2b(AdminPasswordHash)},
{<<"bind_address">>, couch_util:get_value(bind_address, Options)},
{<<"port">>, couch_util:get_value(port, Options)},
{<<"node_count">>, couch_util:get_value(node_count, Options)}
]}),
Headers = [
{"Content-Type","application/json"}
],
RemoteNode = couch_util:get_value(remote_node, Options),
Port = get_port(couch_util:get_value(port, Options, 5984)),
Url = binary_to_list(<<"http://", RemoteNode/binary, ":", Port/binary, "/_cluster_setup">>),
case ibrowse:send_req(Url, Headers, post, Body, RequestOptions) of
{ok, "201", _, _} ->
ok;
Else ->
couch_log:notice("send_req: ~p~n", [Else]),
{error, Else}
end.
enable_cluster_int(_Options, true) ->
{error, cluster_enabled};
enable_cluster_int(Options, false) ->
% if no admin in config and no admin in req -> error
CurrentAdmins = config:get("admins"),
NewCredentials = {
proplists:get_value(username, Options),
case proplists:get_value(password_hash, Options) of
undefined -> proplists:get_value(password, Options);
Pw -> Pw
end
},
ok = require_admins(CurrentAdmins, NewCredentials),
% if bind_address == 127.0.0.1 and no bind_address in req -> error
CurrentBindAddress = config:get("chttpd","bind_address"),
NewBindAddress = proplists:get_value(bind_address, Options),
ok = require_bind_address(CurrentBindAddress, NewBindAddress),
NodeCount = couch_util:get_value(node_count, Options),
ok = require_node_count(NodeCount),
Port = proplists:get_value(port, Options),
setup_node(NewCredentials, NewBindAddress, NodeCount, Port),
couch_log:notice("Enable Cluster: ~p~n", [Options]).
set_admin(Username, Password) ->
config:set("admins", binary_to_list(Username), binary_to_list(Password)).
setup_node(NewCredentials, NewBindAddress, NodeCount, Port) ->
case NewCredentials of
{undefined, undefined} ->
ok;
{Username, Password} ->
set_admin(Username, Password)
end,
case NewBindAddress of
undefined ->
config:set("chttpd", "bind_address", "0.0.0.0");
NewBindAddress ->
config:set("chttpd", "bind_address", binary_to_list(NewBindAddress))
end,
config:set_integer("cluster", "n", NodeCount),
case Port of
undefined ->
ok;
Port when is_binary(Port) ->
config:set("chttpd", "port", binary_to_list(Port));
Port when is_integer(Port) ->
config:set_integer("chttpd", "port", Port)
end.
finish_cluster(Options) ->
Dbs = proplists:get_value(ensure_dbs_exist, Options, cluster_system_dbs()),
finish_cluster_int(Dbs, has_cluster_system_dbs(Dbs)).
finish_cluster_int(_Dbs, true) ->
{error, cluster_finished};
finish_cluster_int(Dbs, false) ->
lists:foreach(fun fabric:create_db/1, Dbs).
enable_single_node(Options) ->
% if no admin in config and no admin in req -> error
CurrentAdmins = config:get("admins"),
NewCredentials = {
proplists:get_value(username, Options),
case proplists:get_value(password_hash, Options) of
undefined -> proplists:get_value(password, Options);
Pw -> Pw
end
},
ok = require_admins(CurrentAdmins, NewCredentials),
% skip bind_address validation, anything is fine
NewBindAddress = proplists:get_value(bind_address, Options),
Port = proplists:get_value(port, Options),
setup_node(NewCredentials, NewBindAddress, 1, Port),
Dbs = proplists:get_value(ensure_dbs_exist, Options, cluster_system_dbs()),
finish_cluster_int(Dbs, has_cluster_system_dbs(Dbs)),
couch_log:notice("Enable Single Node: ~p~n", [Options]).
add_node(Options) ->
add_node_int(Options, is_cluster_enabled()).
add_node_int(_Options, false) ->
{error, cluster_not_enabled};
add_node_int(Options, true) ->
couch_log:notice("add node_int: ~p~n", [Options]),
ErlangCookie = erlang:get_cookie(),
% POST to nodeB/_setup
RequestOptions = [
{basic_auth, {
binary_to_list(proplists:get_value(username, Options)),
binary_to_list(proplists:get_value(password, Options))
}}
],
Body = ?JSON_ENCODE({[
{<<"action">>, <<"receive_cookie">>},
{<<"cookie">>, atom_to_binary(ErlangCookie, utf8)}
]}),
Headers = [
{"Content-Type","application/json"}
],
Host = proplists:get_value(host, Options),
Port = get_port(proplists:get_value(port, Options, 5984)),
Name = proplists:get_value(name, Options, get_default_name(Port)),
Url = binary_to_list(<<"http://", Host/binary, ":", Port/binary, "/_cluster_setup">>),
case ibrowse:send_req(Url, Headers, post, Body, RequestOptions) of
{ok, "201", _, _} ->
% when done, PUT :5986/nodes/nodeB
create_node_doc(Host, Name);
Else ->
couch_log:notice("send_req: ~p~n", [Else]),
Else
end.
get_port(Port) when is_integer(Port) ->
list_to_binary(integer_to_list(Port));
get_port(Port) when is_list(Port) ->
list_to_binary(Port);
get_port(Port) when is_binary(Port) ->
Port.
create_node_doc(Host, Name) ->
{ok, Db} = couch_db:open_int(<<"_nodes">>, []),
Doc = {[{<<"_id">>, <<Name/binary, "@", Host/binary>>}]},
Options = [],
CouchDoc = couch_doc:from_json_obj(Doc),
couch_db:update_doc(Db, CouchDoc, Options).
get_default_name(Port) ->
case Port of
% shortcut for easier development
<<"15984">> ->
<<"node1">>;
<<"25984">> ->
<<"node2">>;
<<"35984">> ->
<<"node3">>;
% by default, all nodes have the user `couchdb`
_ ->
<<"couchdb">>
end.
receive_cookie(Options) ->
Cookie = proplists:get_value(cookie, Options),
erlang:set_cookie(node(), binary_to_atom(Cookie, latin1)).