diff options
author | Joan Touzet <wohali@users.noreply.github.com> | 2018-03-27 03:06:39 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-27 03:06:39 -0400 |
commit | b163663050f5da62ee9aec5a48dbead8391594c1 (patch) | |
tree | 2ec53d84de53f6d3d5c9fd9fedbeeb5140bbe7a8 | |
parent | 6f987aee42107d1f7da467bb9b9513301abb8166 (diff) | |
parent | 1a040a44a2887cc640a536beae14f633343536a1 (diff) | |
download | couchdb-b163663050f5da62ee9aec5a48dbead8391594c1.tar.gz |
Merge branch 'master' into daemon-spacesdaemon-spaces
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | rebar.config.script | 4 | ||||
-rw-r--r-- | src/setup/.gitignore | 4 | ||||
-rw-r--r-- | src/setup/LICENSE | 203 | ||||
-rw-r--r-- | src/setup/README.md | 193 | ||||
-rw-r--r-- | src/setup/src/setup.app.src | 27 | ||||
-rw-r--r-- | src/setup/src/setup.erl | 289 | ||||
-rw-r--r-- | src/setup/src/setup_app.erl | 28 | ||||
-rw-r--r-- | src/setup/src/setup_epi.erl | 49 | ||||
-rw-r--r-- | src/setup/src/setup_httpd.erl | 169 | ||||
-rw-r--r-- | src/setup/src/setup_httpd_handlers.erl | 22 | ||||
-rw-r--r-- | src/setup/src/setup_sup.erl | 38 | ||||
-rwxr-xr-x | src/setup/test/t-frontend-setup.sh | 63 | ||||
-rwxr-xr-x | src/setup/test/t-single-node.sh | 46 | ||||
-rwxr-xr-x | src/setup/test/t.sh | 63 |
15 files changed, 1196 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore index 3f5c4b284..a1a3040c9 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,6 @@ src/meck/ src/mochiweb/ src/oauth/ src/rebar/ -src/setup/ src/snappy/ tmp/ diff --git a/rebar.config.script b/rebar.config.script index 4f6f82def..7d9b7feb5 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -41,6 +41,7 @@ SubDirs = [ "src/mango", "src/mem3", "src/rexi", + "src/setup", "rel" ], @@ -51,7 +52,6 @@ DepDescs = [ {ets_lru, "ets-lru", {tag, "1.0.0"}}, {khash, "khash", {tag, "1.0.1"}}, {snappy, "snappy", {tag, "CouchDB-1.0.0"}}, -{setup, "setup", {tag, "1.0.1"}}, {ioq, "ioq", {tag, "1.0.1"}}, %% Non-Erlang deps @@ -100,7 +100,7 @@ AddConfig = [ {plt_location, COUCHDB_ROOT}, {plt_extra_apps, [ asn1, bcrypt, compiler, crypto, inets, kernel, os_mon, runtime_tools, - sasl, ssl, stdlib, syntax_tools, xmerl]}, + sasl, setup, ssl, stdlib, syntax_tools, xmerl]}, {warnings, [unmatched_returns, error_handling, race_conditions]}]}, {post_hooks, [{compile, "escript support/build_js.escript"}]} ], diff --git a/src/setup/.gitignore b/src/setup/.gitignore new file mode 100644 index 000000000..f84f14c93 --- /dev/null +++ b/src/setup/.gitignore @@ -0,0 +1,4 @@ +ebin +.rebar +*~ +*.swp diff --git a/src/setup/LICENSE b/src/setup/LICENSE new file mode 100644 index 000000000..94ad231b8 --- /dev/null +++ b/src/setup/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. + diff --git a/src/setup/README.md b/src/setup/README.md new file mode 100644 index 000000000..e30c40027 --- /dev/null +++ b/src/setup/README.md @@ -0,0 +1,193 @@ +This module implements /_cluster_setup and manages the setting up, duh, of a CouchDB cluster. + +### Testing + +```bash +git clone https://git-wip-us.apache.org/repos/asf/couchdb.git +cd couchdb +git checkout setup +./configure +make +dev/run --no-join -n 2 --admin a:b +``` + +Then, in a new terminal: + + $ src/setup/test/t.sh + +Before running each test, kill the `dev/run` script, then reset the +CouchDB instances with: + + $ rm -rf dev/lib/ dev/logs/ + $ dev/run --no-join -n 2 --admin a:b + +before running the next shell script. + +The Plan: + +N. End User Action +- What happens behind the scenes. + + +1. Launch CouchDB with `$ couchdb`, or init.d, or any other way, exactly +like it is done in 1.x.x. +- CouchDB launches and listens on 127.0.0.1:5984 + +From here on, there are two paths, one is via Fauxton (a) the other is +using a HTTP endpoint (b). Fauxton just uses the HTTP endpoint in (b). +(b) can be used to set up a cluster programmatically. + +When using (b) you POST HTTP requests with a JSON request body (the request content type has to be set to application/json). + +If you have already setup a server admin account, you might need to pass the credentials to the HTTP calls using HTTP basic authentication. +Alternativaly, if you use the cURL command you can can add username and password inline, like so: + +``` +curl -X PUT "http://admin:password@127.0.0.1:5984/mydb" +``` + +2.a. Go to Fauxton. There is a “Cluster Setup” tab in the sidebar. Go +to the tab and get presented with a form that asks you to enter an admin +username, admin password and optionally a bind_address and port to bind +to publicly. Submit the form with the [Enable Cluster] button. + +If this is a single node install that already has an admin set up, there +is no need to ask for admin credentials here. If the bind_address is != +127.0.0.1, we can skip this entirely and Fauxton can show the add_node +UI right away. + +- POST a JSON entity to /_cluster_setup, the entity looks like: +``` +{ + "action":"enable_cluster", + "username":"username", + "password":"password", + "bind_address":"0.0.0.0", + "port": 5984 +} +``` + +This sets up the admin user on the current node and binds to 0.0.0.0:5984 +or the specified ip:port. Logs admin user into Fauxton automatically. + +2.b. POST to /_cluster_setup as shown above. + +Repeat on all nodes. +- keep the same username/password everywhere. + + +3. Pick any one node, for simplicity use the first one, to be the +“setup coordination node”. +- this is a “master” node that manages the setup and requires all + other nodes to be able to see it and vice versa. Setup won’t work + with unavailable nodes (duh). The notion of “master” will be gone + once the setup is finished. At that point, the system has no + master node. Ignore I ever said “master”. + +a. Go to Fauxton / Cluster Setup, once we have enabled the cluster, the +UI shows an “Add Node” interface with the fields admin, and node: +- POST a JSON entity to /_cluster_setup, the entity looks like: +``` +{ + "action":"add_node", + "username":"username", + "password":"password", + "host":"192.168.1.100", + ["port": 5984], + "name": "node1" // as in “node1@hostname”, same as in vm.args +} +``` + +In the example above, this adds the node with IP address 192.168.1.100 to the cluster. + +b. as in a, but without the Fauxton bits, just POST to /_cluster_setup +- this request will do this: + - on the “setup coordination node”: + - check if we have an Erlang Cookie Secret. If not, generate + a UUID and set the erlang cookie to to that UUID. + - store the cookie in config.ini, re-set_cookie() on startup. + - make a POST request to the node specified in the body above + using the admin credentials in the body above: + POST to http://username:password@node_b:5984/_cluster_setup with: +``` + { + "action": "receive_cookie", + "cookie": "<secretcookie>", + } +``` + + - when the request to node B returns, we know the Erlang-level + inter-cluster communication is enabled and we can start adding + the node on the CouchDB level. To do that, the “setup + coordination node” does this to it’s own HTTP endpoint: + PUT /nodes/node_b:5984 or the same thing with internal APIs. + +- Repeat for all nodes. +- Fauxton keeps a list of all set up nodes for users to see. + + +4.a. When all nodes are added, click the [Finish Cluster Setup] button +in Fauxton. +- this does POST /_cluster_setup +``` + { + "action": "finish_cluster" + } +``` + +b. Same as in a. + +- this manages the final setup bits, like creating the _users, + _replicator and _metadata, _db_updates endpoints and + whatever else is needed. // TBD: collect what else is needed. + + +## The Setup Endpoint + +This is not a REST-y endpoint, it is a simple state machine operated +by HTTP POST with JSON bodies that have an `action` field. + +### State 1: No Cluster Enabled + +This is right after starting a node for the first time, and any time +before the cluster is enabled as outlined above. + +``` +GET /_cluster_setup +{"state": "cluster_disabled"} + +POST /_cluster_setup {"action":"enable_cluster"...} -> Transition to State 2 +POST /_cluster_setup {"action":"enable_cluster"...} with empty admin user/pass or invalid host/post or host/port not available -> Error +POST /_cluster_setup {"action":"anything_but_enable_cluster"...} -> Error +``` + +### State 2: Cluster enabled, admin user set, waiting for nodes to be added. + +``` +GET /_cluster_setup +{"state":"cluster_enabled","nodes":[]} + +POST /_cluster_setup {"action":"enable_cluster"...} -> Error +POST /_cluster_setup {"action":"add_node"...} -> Stay in State 2, but return "nodes":["node B"}] on GET +POST /_cluster_setup {"action":"add_node"...} -> if target node not available, Error +POST /_cluster_setup {"action":"finish_cluster"} with no nodes set up -> Error +POST /_cluster_setup {"action":"finish_cluster"} -> Transition to State 3 +POST /_cluster_setup {"action":"delete_node"...} -> Stay in State 2, but delete node from /nodes, reflect the change in GET /_cluster_setup +POST /_cluster_setup {"action":"delete_node","node":"unknown"} -> Error Unknown Node +``` + +### State 3: Cluster set up, all nodes operational + +``` +GET /_cluster_setup +{"state":"cluster_finished","nodes":["node a", "node b", ...]} + +POST /_cluster_setup {"action":"enable_cluster"...} -> Error +POST /_cluster_setup {"action":"finish_cluster"...} -> Stay in State 3, do nothing +POST /_cluster_setup {"action":"add_node"...} -> Error +POST /_cluster_setup?i_know_what_i_am_doing=true {"action":"add_node"...} -> Add node, stay in State 3. +POST /_cluster_setup {"action":"delete_node"...} -> Stay in State 3, but delete node from /nodes, reflect the change in GET /_cluster_setup +POST /_cluster_setup {"action":"delete_node","node":"unknown"} -> Error Unknown Node +``` + +// TBD: we need to persist the setup state somewhere. diff --git a/src/setup/src/setup.app.src b/src/setup/src/setup.app.src new file mode 100644 index 000000000..ae685c971 --- /dev/null +++ b/src/setup/src/setup.app.src @@ -0,0 +1,27 @@ +% 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. + +{application, setup, + [ + {description, ""}, + {vsn, "1"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + couch_epi, + chttpd, + couch_log + ]}, + {mod, { setup_app, []}}, + {env, []} + ]}. diff --git a/src/setup/src/setup.erl b/src/setup/src/setup.erl new file mode 100644 index 000000000..943318675 --- /dev/null +++ b/src/setup/src/setup.erl @@ -0,0 +1,289 @@ +% 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, + + % for single node setups, set n=1, for larger setups, don’t + % exceed n=3 as a default + config:set_integer("cluster", "n", min(NodeCount, 3)), + + 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)). diff --git a/src/setup/src/setup_app.erl b/src/setup/src/setup_app.erl new file mode 100644 index 000000000..330450131 --- /dev/null +++ b/src/setup/src/setup_app.erl @@ -0,0 +1,28 @@ +% 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_app). + +-behaviour(application). + +%% Application callbacks +-export([start/2, stop/1]). + +%% =================================================================== +%% Application callbacks +%% =================================================================== + +start(_StartType, _StartArgs) -> + setup_sup:start_link(). + +stop(_State) -> + ok. diff --git a/src/setup/src/setup_epi.erl b/src/setup/src/setup_epi.erl new file mode 100644 index 000000000..c3f2636f0 --- /dev/null +++ b/src/setup/src/setup_epi.erl @@ -0,0 +1,49 @@ +% 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_epi). + +-behaviour(couch_epi_plugin). + +-export([ + app/0, + providers/0, + services/0, + data_subscriptions/0, + data_providers/0, + processes/0, + notify/3 +]). + +app() -> + setup. + +providers() -> + [ + {chttpd_handlers, setup_httpd_handlers} + ]. + +services() -> + []. + +data_subscriptions() -> + []. + +data_providers() -> + []. + +processes() -> + []. + +notify(_Key, _Old, _New) -> + ok. diff --git a/src/setup/src/setup_httpd.erl b/src/setup/src/setup_httpd.erl new file mode 100644 index 000000000..f4e05ce09 --- /dev/null +++ b/src/setup/src/setup_httpd.erl @@ -0,0 +1,169 @@ +% 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_httpd). +-include_lib("couch/include/couch_db.hrl"). + +-export([handle_setup_req/1]). + +handle_setup_req(#httpd{method='POST'}=Req) -> + ok = chttpd:verify_is_server_admin(Req), + couch_httpd:validate_ctype(Req, "application/json"), + Setup = get_body(Req), + couch_log:notice("Setup: ~p~n", [Setup]), + Action = binary_to_list(couch_util:get_value(<<"action">>, Setup, <<"missing">>)), + case handle_action(Action, Setup) of + ok -> + chttpd:send_json(Req, 201, {[{ok, true}]}); + {error, Message} -> + couch_httpd:send_error(Req, 400, <<"bad_request">>, Message) + end; +handle_setup_req(#httpd{method='GET'}=Req) -> + ok = chttpd:verify_is_server_admin(Req), + Dbs = chttpd:qs_json_value(Req, "ensure_dbs_exist", setup:cluster_system_dbs()), + couch_log:notice("Dbs: ~p~n", [Dbs]), + case erlang:list_to_integer(config:get("cluster", "n", undefined)) of + 1 -> + case setup:is_single_node_enabled(Dbs) of + false -> + chttpd:send_json(Req, 200, {[{state, single_node_disabled}]}); + true -> + chttpd:send_json(Req, 200, {[{state, single_node_enabled}]}) + end; + _ -> + case setup:is_cluster_enabled() of + false -> + chttpd:send_json(Req, 200, {[{state, cluster_disabled}]}); + true -> + case setup:has_cluster_system_dbs(Dbs) of + false -> + chttpd:send_json(Req, 200, {[{state, cluster_enabled}]}); + true -> + chttpd:send_json(Req, 200, {[{state, cluster_finished}]}) + end + end + end; +handle_setup_req(#httpd{}=Req) -> + chttpd:send_method_not_allowed(Req, "GET,POST"). + + +get_options(Options, Setup) -> + ExtractValues = fun({Tag, Option}, OptionsAcc) -> + case couch_util:get_value(Option, Setup) of + undefined -> OptionsAcc; + Value -> [{Tag, Value} | OptionsAcc] + end + end, + lists:foldl(ExtractValues, [], Options). + +handle_action("enable_cluster", Setup) -> + Options = get_options([ + {username, <<"username">>}, + {password, <<"password">>}, + {password_hash, <<"password_hash">>}, + {bind_address, <<"bind_address">>}, + {port, <<"port">>}, + {remote_node, <<"remote_node">>}, + {remote_current_user, <<"remote_current_user">>}, + {remote_current_password, <<"remote_current_password">>}, + {node_count, <<"node_count">>} + ], Setup), + case setup:enable_cluster(Options) of + {error, cluster_enabled} -> + {error, <<"Cluster is already enabled">>}; + _ -> ok + end; + + +handle_action("finish_cluster", Setup) -> + couch_log:notice("finish_cluster: ~p~n", [Setup]), + + Options = get_options([ + {ensure_dbs_exist, <<"ensure_dbs_exist">>} + ], Setup), + case setup:finish_cluster(Options) of + {error, cluster_finished} -> + {error, <<"Cluster is already finished">>}; + Else -> + couch_log:notice("finish_cluster: ~p~n", [Else]), + ok + end; + +handle_action("enable_single_node", Setup) -> + couch_log:notice("enable_single_node: ~p~n", [Setup]), + + Options = get_options([ + {ensure_dbs_exist, <<"ensure_dbs_exist">>}, + {username, <<"username">>}, + {password, <<"password">>}, + {password_hash, <<"password_hash">>}, + {bind_address, <<"bind_address">>}, + {port, <<"port">>} + ], Setup), + case setup:enable_single_node(Options) of + {error, cluster_finished} -> + {error, <<"Cluster is already finished">>}; + Else -> + couch_log:notice("Else: ~p~n", [Else]), + ok + end; + + +handle_action("add_node", Setup) -> + couch_log:notice("add_node: ~p~n", [Setup]), + + Options = get_options([ + {username, <<"username">>}, + {password, <<"password">>}, + {host, <<"host">>}, + {port, <<"port">>}, + {name, <<"name">>} + ], Setup), + case setup:add_node(Options) of + {error, cluster_not_enabled} -> + {error, <<"Cluster is not enabled.">>}; + {error, {conn_failed, {error, econnrefused}}} -> + {error, <<"Add node failed. Invalid Host and/or Port.">>}; + {error, wrong_credentials} -> + {error, <<"Add node failed. Invalid admin credentials,">>}; + {error, Message} -> + {error, Message}; + _ -> ok + end; + +handle_action("remove_node", Setup) -> + couch_log:notice("remove_node: ~p~n", [Setup]); + +handle_action("receive_cookie", Setup) -> + couch_log:notice("receive_cookie: ~p~n", [Setup]), + Options = get_options([ + {cookie, <<"cookie">>} + ], Setup), + case setup:receive_cookie(Options) of + {error, Error} -> + {error, Error}; + _ -> ok + end; + +handle_action(_, _) -> + couch_log:notice("invalid_action: ~n", []), + {error, <<"Invalid Action'">>}. + + +get_body(Req) -> + case catch couch_httpd:json_body_obj(Req) of + {Body} -> + Body; + Else -> + couch_log:notice("Body Fail: ~p~n", [Else]), + couch_httpd:send_error(Req, 400, <<"bad_request">>, <<"Missing JSON body'">>) + end. diff --git a/src/setup/src/setup_httpd_handlers.erl b/src/setup/src/setup_httpd_handlers.erl new file mode 100644 index 000000000..994c217e8 --- /dev/null +++ b/src/setup/src/setup_httpd_handlers.erl @@ -0,0 +1,22 @@ +% 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_httpd_handlers). + +-export([url_handler/1, db_handler/1, design_handler/1]). + +url_handler(<<"_cluster_setup">>) -> fun setup_httpd:handle_setup_req/1; +url_handler(_) -> no_match. + +db_handler(_) -> no_match. + +design_handler(_) -> no_match. diff --git a/src/setup/src/setup_sup.erl b/src/setup/src/setup_sup.erl new file mode 100644 index 000000000..b81aa3afb --- /dev/null +++ b/src/setup/src/setup_sup.erl @@ -0,0 +1,38 @@ +% 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_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callbacks +-export([init/1]). + +%% Helper macro for declaring children of supervisor +-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). + +%% =================================================================== +%% API functions +%% =================================================================== + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% =================================================================== +%% Supervisor callbacks +%% =================================================================== + +init([]) -> + {ok, {{one_for_one, 5, 10}, couch_epi:register_service(setup_epi, [])}}. diff --git a/src/setup/test/t-frontend-setup.sh b/src/setup/test/t-frontend-setup.sh new file mode 100755 index 000000000..52056a374 --- /dev/null +++ b/src/setup/test/t-frontend-setup.sh @@ -0,0 +1,63 @@ +#!/bin/sh -ex +# 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. + +HEADERS="-HContent-Type:application/json" +# show cluster state: +curl a:b@127.0.0.1:15986/_nodes/_all_docs + +# Enable Cluster on node A +curl a:b@127.0.0.1:15984/_cluster_setup -d '{"action":"enable_cluster","username":"foo","password":"baz","bind_address":"0.0.0.0","node_count":2}' $HEADERS + +# Enable Cluster on node B +curl a:b@127.0.0.1:15984/_cluster_setup -d '{"action":"enable_cluster","remote_node":"127.0.0.1","port":"25984","remote_current_user":"a","remote_current_password":"b","username":"foo","password":"baz","bind_address":"0.0.0.0","node_count":2}' $HEADERS + +# Add node B on node A +curl a:b@127.0.0.1:15984/_cluster_setup -d '{"action":"add_node","username":"foo","password":"baz","host":"127.0.0.1","port":25984,"name":"node2"}' $HEADERS + +# Show cluster state: +curl a:b@127.0.0.1:15986/_nodes/_all_docs + +# Show db doesn’t exist on node A +curl a:b@127.0.0.1:15984/foo + +# Show db doesn’t exist on node B +curl a:b@127.0.0.1:25984/foo + +# Create database (on node A) +curl -X PUT a:b@127.0.0.1:15984/foo + +# Show db does exist on node A +curl a:b@127.0.0.1:15984/foo + +# Show db does exist on node B +curl a:b@127.0.0.1:25984/foo + +# Finish cluster +curl a:b@127.0.0.1:15984/_cluster_setup -d '{"action":"finish_cluster"}' $HEADERS + +# Show system dbs exist on node A +curl a:b@127.0.0.1:15984/_users +curl a:b@127.0.0.1:15984/_replicator +curl a:b@127.0.0.1:15984/_metadata +curl a:b@127.0.0.1:15984/_global_changes + +# Show system dbs exist on node B +curl a:b@127.0.0.1:25984/_users +curl a:b@127.0.0.1:25984/_replicator +curl a:b@127.0.0.1:25984/_metadata +curl a:b@127.0.0.1:25984/_global_changes + +# Number of nodes is set to 2 +curl a:b@127.0.0.1:25984/_node/node2@127.0.0.1/_config/cluster/n + +echo "YAY ALL GOOD" diff --git a/src/setup/test/t-single-node.sh b/src/setup/test/t-single-node.sh new file mode 100755 index 000000000..d49043773 --- /dev/null +++ b/src/setup/test/t-single-node.sh @@ -0,0 +1,46 @@ +#!/bin/sh -ex +# 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. + +HEADERS="-HContent-Type:application/json" +# show cluster state: +curl a:b@127.0.0.1:15986/_nodes/_all_docs +curl a:b@127.0.0.1:15984/_cluster_setup + +# Enable Cluster on single node +curl a:b@127.0.0.1:15984/_cluster_setup -d '{"action":"enable_single_node","username":"foo","password":"baz","bind_address":"127.0.0.1"}' $HEADERS + +# Show cluster state: +curl a:b@127.0.0.1:15986/_nodes/_all_docs +curl a:b@127.0.0.1:15984/_all_dbs +curl a:b@127.0.0.1:15984/_cluster_setup + +# Delete a database +curl -X DELETE a:b@127.0.0.1:15984/_global_changes + +# Should show single_node_disabled +curl a:b@127.0.0.1:15984/_cluster_setup + +# Change the check +curl -g 'a:b@127.0.0.1:15984/_cluster_setup?ensure_dbs_exist=["_replicator","_users"]' + +# delete all the things +curl -X DELETE a:b@127.0.0.1:15984/_replicator +curl -X DELETE a:b@127.0.0.1:15984/_users + +# setup only creating _users +curl -g a:b@127.0.0.1:15984/_cluster_setup -d '{"action":"enable_single_node","username":"foo","password":"baz","bind_address":"127.0.0.1","ensure_dbs_exist":["_users"]}' $HEADERS + +# check it +curl -g 'a:b@127.0.0.1:15984/_cluster_setup?ensure_dbs_exist=["_users"]' + +echo "YAY ALL GOOD" diff --git a/src/setup/test/t.sh b/src/setup/test/t.sh new file mode 100755 index 000000000..6bd74cdd7 --- /dev/null +++ b/src/setup/test/t.sh @@ -0,0 +1,63 @@ +#!/bin/sh -ex +# 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. + +HEADERS="-HContent-Type:application/json" +# show cluster state: +curl a:b@127.0.0.1:15986/_nodes/_all_docs + +# Enable Cluster on node A +curl a:b@127.0.0.1:15984/_cluster_setup -d '{"action":"enable_cluster","username":"foo","password":"baz","bind_address":"0.0.0.0","node_count":2}' $HEADERS + +# Enable Cluster on node B +curl a:b@127.0.0.1:25984/_cluster_setup -d '{"action":"enable_cluster","username":"foo","password":"baz","bind_address":"0.0.0.0","node_count":2}' $HEADERS + +# Add node B on node A +curl a:b@127.0.0.1:15984/_cluster_setup -d '{"action":"add_node","username":"foo","password":"baz","host":"127.0.0.1","port":25984,"name":"node2"}' $HEADERS + +# Show cluster state: +curl a:b@127.0.0.1:15986/_nodes/_all_docs + +# Show db doesn’t exist on node A +curl a:b@127.0.0.1:15984/foo + +# Show db doesn’t exist on node B +curl a:b@127.0.0.1:25984/foo + +# Create database (on node A) +curl -X PUT a:b@127.0.0.1:15984/foo + +# Show db does exist on node A +curl a:b@127.0.0.1:15984/foo + +# Show db does exist on node B +curl a:b@127.0.0.1:25984/foo + +# Finish cluster +curl a:b@127.0.0.1:15984/_cluster_setup -d '{"action":"finish_cluster"}' $HEADERS + +# Show system dbs exist on node A +curl a:b@127.0.0.1:15984/_users +curl a:b@127.0.0.1:15984/_replicator +curl a:b@127.0.0.1:15984/_metadata +curl a:b@127.0.0.1:15984/_global_changes + +# Show system dbs exist on node B +curl a:b@127.0.0.1:25984/_users +curl a:b@127.0.0.1:25984/_replicator +curl a:b@127.0.0.1:25984/_metadata +curl a:b@127.0.0.1:25984/_global_changes + +# Number of nodes is set to 2 +curl a:b@127.0.0.1:25984/_node/node2@127.0.0.1/_config/cluster/n + +echo "YAY ALL GOOD" |