Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
...
Checking mergeability… Don't worry, you can still create the pull request.
  • 4 commits
  • 12 files changed
  • 0 commit comments
  • 1 contributor
Commits on Aug 04, 2011
@russelldb russelldb Temp commit of vnode data 7e6fb24
Commits on Aug 05, 2011
@russelldb russelldb Add vnodes to display
This is a temp commit as I'm going away, this is not ready for commit
af9edca
@russelldb russelldb Temp vacaction commit of handoff stuff e94f7ab
Commits on Aug 22, 2011
@russelldb russelldb Add vnode handoff status to GUI 873dcb0
View
BIN  priv/admin_ui/blue-rectangle-16.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
47 priv/admin_ui/control.js
@@ -1,15 +1,48 @@
// node model
function node(name, cluster) {
+ var self = this;
this.name = name;
+ this.cluster = cluster;
+ this.services = ko.observableArray([]);
this.remove = function() { cluster.removeNode(name) };
this.select = function() { cluster.selectNode(name) };
this.deselect = function() { cluster.selectNode("") };
+
+ this.getServices = function() {
+ $.ajax({url: "/admin/nodes/" + self.name + "/services",
+ type: "GET",
+ success: function(data) {
+ $.each(data.services, function(i, s) {
+ self.services.push(new service(s.name, s.initial));
+ });
+ self.services.sort(function(l, r) { return l.initial == r.initial ? 0 : (l.initial < r.initial ? -1 : 1 ) });
+ },
+ error: function(x, s, e) {
+ //
+ }
+ });
+ }
+}
+
+// partition model
+function partition(idx, node, vnodes, cluster) {
+ this.idx = idx;
+ this.node = node;
+ this.vnodes = vnodes;
+ this.cluster = cluster;
+}
+
+// service model
+function service(name, initial) {
+ this.name = name;
+ this.initial = initial;
}
// cluster model
function cluster() {
var self = this;
this.nodes = ko.observableArray([]);
+ this.partitions = ko.observableArray([]);
this.newNodeName = ko.observable();
this.message = ko.observable();
this.selectedNodeName = ko.observable();
@@ -27,6 +60,8 @@ function cluster() {
$.ajax({url: '/admin/nodes/' + node,
type: 'DELETE',
success: function() {
+ self.selectNode("");
+ self.getNodes();
self.message("Sent remove request for " + node);
},
error: function(x, s, e) {
@@ -55,18 +90,25 @@ function cluster() {
this.getNodes = function() {
$.getJSON("/admin/nodes", function(data) {
currentNodes = [];
+ self.partitions([]);
+
$.each(data.nodes, function(i, name) {
currentNodes.push(new node(name, self));
});
+
+ $.each(data.partitions, function(i, p) {
+ self.partitions.push(new partition(i, p.owner, p.vnodes, self));
+ });
+
self.mergeNodes(currentNodes);
});
- setTimeout(self.getNodes, 5000);
+ //setTimeout(self.getNodes, 5000); //take this out until I find a better way than mergeNodes below
}
// sync the model view with the last gotten view
// gives a smoother updating display since the model isn't
// fully rebuilt per get (less flicker)
- // pretty ineffecient, but I doubt we are dealing with 10,000 node clusters
+ // pretty ineffecient, but I doubt we are dealing with 10,000 node clusters (yet) TODO look at knockout's auto model
this.mergeNodes = function(nodeList) {
// remove any node in the model that is not in the latest get result
_.each(self.nodes(), function(inModel) {
@@ -80,6 +122,7 @@ function cluster() {
// add any node not in the model that is in the latest get result
_.each(nodeList, function(inNodeList) {
if(!_.detect(self.nodes(), function(inModel) { return inModel.name == inNodeList.name;})) {
+ inNodeList.getServices();
self.nodes.push(inNodeList);
}
});
View
BIN  priv/admin_ui/green-circle-filled-16.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
57 priv/admin_ui/index.html
@@ -31,13 +31,54 @@
<button type="submit">Add</button>
</form>
</div>
- <ul class="nodes" data-bind="template: { name: 'nodesTemplate', foreach: nodes }, visible: nodes().length > 0"></ul>
- <script type="text/html" id="nodesTemplate">
- <li data-bind="click: select">
- ${name}
- </li>
- </script>
- </div>
+ <table id="cluster">
+ <thead>
+ <tr data-bind="template: {name: 'nodeHeaderTemplate'}">
+ <script type="text/html" id="nodeHeaderTemplate">
+ <th class="header">Partition</th>
+ {{each(i, node) nodes()}}
+ <th class="node header" data-bind="click: select">
+ ${name}
+ </th>
+ {{/each}}
+ </script>
+ </tr>
+ <tr data-bind="template: {name: 'serviceHeaderTemplate'}">
+ <script type="text/html" id="serviceHeaderTemplate">
+ <th>&nbsp;</th>
+ {{each(i, node) nodes()}}
+ <th class="services">
+ <ul class="services">
+ {{each(k, service) node.services()}}
+ <li>${service.initial}</li>
+ {{/each}}
+ </ul>
+ </th>
+ {{/each}}
+ </script>
+ </tr>
+ </thead>
+ <tbody data-bind="template: {name: 'partitionsTemplate'}">
+ <script type="text/html" id="partitionsTemplate">
+ {{each(i, p) partitions()}}
+ <tr>
+ <td>${i}</td>
+ {{each(k, node) nodes()}}
+ <td class="{{if node.name == p.node}}owner{{/if}}">
+ <ul class="vnodes">
+ {{each(l, vnode) p.vnodes}}
+ <li data-bind="visible: vnode.location == node.name" class="${vnode.status}"></li>
+ {{/each}}
+ </ul>
+ </td>
+ {{/each}}
+ </tr>
+ {{/each}}
+ </script>
+ </tbody>
+ </table>
+ </div>
+
<!-- node page -->
<div class="viewNode" data-bind="template: { name: 'nodeTemplate', data: selectedNode }"></div>
<script type="text/html" id="nodeTemplate">
@@ -45,7 +86,7 @@
<a href="#" data-bind="click: deselect">back</a>
<h1>${name}</h1>
<a href="#" data-bind="click: remove">remove</a>
- </div>
+ </div>
</script>
</div>
</div>
View
BIN  priv/admin_ui/orange-arrow-left-16.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  priv/admin_ui/orange-arrow-right-16.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
54 priv/admin_ui/screen.css
@@ -1,18 +1,58 @@
-.nodes {
- list-style-type: none; border-radius: 0.5em;
- padding: 0;
+.vnodes {
+ list-style-type: none;
+ padding: 0;
margin: 0;
}
-.nodes li:hover {
- background-color: #6F0;
+.vnodes li.away {
+ background: url(blue-rectangle-16.png) no-repeat bottom left;
+}
+
+.vnodes li.home {
+ background: url(green-circle-filled-16.png) no-repeat bottom left;
+}
+
+.vnodes li.handingoff {
+ background: url(orange-arrow-right-16.png) no-repeat bottom left;
+}
+
+.vnodes li.receiving_handoff {
+ background: url(orange-arrow-left-16.png) no-repeat bottom left;
}
-.nodes li {
+.vnodes li {
display: inline-block;
- padding: 0.5em 1.5em;
+ padding: 0 0 1.2em 1em;
cursor: pointer;
}
.message {
cursor: pointer;
}
+
+.services {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.services li {
+ display: inline-block;
+ padding: 0 0 0 0.5em;
+ margin: 0;
+}
+
+table thead tr th {
+ padding: 0.5em 1.5em;
+}
+
+table thead tr th.node {
+ cursor: pointer;
+}
+
+table thead tr th.node:hover {
+ background-color: #6F0;
+}
+
+table tbody tr td.owner {
+ background-color: #ddd;
+}
View
2  rebar.config
@@ -1,5 +1,5 @@
%% -*- mode: erlang -*-
-{erl_opts, [warnings_as_errors, debug_info]}.
+{erl_opts, [warnings_as_errors, {parse_transform, lager_transform}]}.
{edoc_opts, [{preprocess, true}]}.
{cover_enabled, true}.
{deps, [
View
53 src/admin_node_resource.erl
@@ -29,8 +29,10 @@
service_available/2,
is_authorized/2,
allowed_methods/2,
+ content_types_provided/2,
content_types_accepted/2,
resource_exists/2,
+ produce_json/2,
accept_content/2,
delete_resource/2,
delete_completed/2
@@ -40,8 +42,6 @@
-include("riak_control.hrl").
-record(ctx, {
- base_url,
- ring_members,
nodename
}).
@@ -55,19 +55,18 @@
routes() ->
[{?ADMIN_BASE_ROUTE++["nodes", 'nodename'],
?MODULE,
- [{base_url, ?ADMIN_BASE_PATH++"nodes/"}]}].
+ []}].
%%% Webmachine API
-spec init(list()) -> {ok, context()}.
-init(Props) ->
- {base_url, Url} = lists:keyfind(base_url, 1, Props),
- {ok, #ctx{base_url=Url}}.
+init(_Props) ->
+ {ok, #ctx{}}.
-spec allowed_methods(wrq:reqdata(), context()) ->
{[method()], wrq:request(), context()}.
allowed_methods(RD, Ctx) ->
- {['PUT', 'DELETE'], RD, Ctx}.
+ {['PUT', 'DELETE', 'GET'], RD, Ctx}.
-spec service_available(wrq:reqdata(), context()) ->
{boolean() | {halt, non_neg_integer()}, wrq:reqdata(), context()}.
@@ -79,6 +78,12 @@ service_available(RD, Ctx) ->
is_authorized(RD, Ctx) ->
riak_control_security:enforce_auth(RD, Ctx).
+-spec content_types_provided(wrq:reqdata(), context()) ->
+ {[{ContentType::string(), HandlerFunction::atom()}],
+ wrq:reqdata(), context()}.
+content_types_provided(RD, Ctx) ->
+ {[{"application/json", produce_json}], RD, Ctx}.
+
-spec content_types_accepted(wrq:reqdata(), context()) ->
{[{ContentType::string(), HandlerFunction::atom()}],
wrq:reqdata(), context()}.
@@ -92,14 +97,23 @@ content_types_accepted(RD, Ctx) ->
-spec resource_exists(wrq:reqdata(), context()) ->
{boolean(), wrq:reqdata(), context()}.
resource_exists(RD, Ctx) ->
- NewNode = list_to_atom(wrq:path_info(nodename, RD)),
- RingCtx = Ctx#ctx{nodename=NewNode},
- {is_node_in_cluster(NewNode), RD, RingCtx}.
+ Node = list_to_atom(wrq:path_info(nodename, RD)),
+ NodeCtx = Ctx#ctx{nodename=Node},
+ {is_node_in_cluster(Node), RD, NodeCtx}.
+
+-spec produce_json(wrq:reqdata(), context()) ->
+ {binary(), wrq:reqdata(), context()}.
+produce_json(RD, #ctx{nodename=Node}=Ctx) ->
+ {ok, Ring} = riak_core_ring_manager:get_my_ring(),
+ Partitions = [I || {I,Owner} <- riak_core_ring:all_owners(Ring), Owner =:= Node],
+ JSON = mochijson2:encode([{partitions, Partitions}]),
+ {JSON, RD, Ctx}.
-spec accept_content(wrq:reqdata(), context()) ->
{boolean(), wrq:reqdata(), context()}.
accept_content(RD, #ctx{nodename=NewNode}=Ctx) ->
- {ok, OurRingSize} = application:get_env(riak_core, ring_creation_size),
+ {ok, Ring} = riak_core_ring_manager:get_my_ring(),
+ OurRingSize = riak_core_ring:num_partitions(Ring),
case net_adm:ping(NewNode) of
pong ->
case rpc:call(NewNode,
@@ -107,6 +121,10 @@ accept_content(RD, #ctx{nodename=NewNode}=Ctx) ->
get_env,
[riak_core, ring_creation_size]) of
{ok, OurRingSize} ->
+ Ring2 = riak_core_ring:add_member(NewNode, Ring,
+ NewNode),
+ Ring3 = riak_core_ring:set_owner(Ring2, NewNode),
+ ok = rpc:call(NewNode, riak_core_ring_manager, set_my_ring, [Ring3]),
riak_core_gossip:send_ring(node(), NewNode),
{true, RD, Ctx};
_ ->
@@ -120,17 +138,12 @@ accept_content(RD, #ctx{nodename=NewNode}=Ctx) ->
-spec delete_resource(wrq:reqdata(), context()) ->
{boolean(), wrq:reqdata(), context()}.
delete_resource(RD, #ctx{nodename=Node}=Ctx) ->
- try
- case catch(riak_core:remove_from_cluster(Node)) of
- {'EXIT', {badarg, [{erlang, hd, [[]]}|_]}} ->
- {{error, <<"single node">>}, wrq:set_resp_body(<<"Can't remove a single node from 'cluster'">>, RD), Ctx};
+ case riak_core:remove_from_cluster(Node) of
+ {error, Reason} ->
+ {{error, list_to_binary(atom_to_list(Reason))}, wrq:set_resp_body(list_to_binary(atom_to_list(Reason)), RD), Ctx};
ok ->
{true, RD, Ctx}
- end
- catch
- Exception:Reason ->
- {{error, Exception}, wrq:set_resp_body(Reason, RD), Ctx}
- end.
+ end.
-spec delete_completed(wrq:reqdata(), context()) ->
View
84 src/admin_nodes_resource.erl
@@ -89,16 +89,87 @@ resource_exists(RD, Ctx) ->
-spec produce_json(wrq:reqdata(), context()) ->
{binary(), wrq:reqdata(), context()}.
produce_json(RD, #ctx{ring=Ring}=Ctx) ->
- %% TODO: this is not really the list of nodes we'll want (nodes
- %% running, and connected, but not yet claiming partitions are
- %% interesting to the UI as well), but it's a good example of
- %% serving data over this interface
- Members = riak_core_ring:all_members(Ring),
- JSON = mochijson2:encode([{nodes, Members}]),
+ Nodes = lists:filter(fun(X) ->
+ case rpc:call(X, erlang, whereis, [riak_core_sup]) of
+ undefined ->
+ false;
+ _ ->
+ true
+ end
+ end,
+ [node() | nodes()]),
+ HandOffs = handoffs_by_partition(Nodes),
+ VNodeMods = lists:sort(riak_core:vnode_modules()),
+ VNodes = all_vnodes(riak_core_ring:all_owners(Ring), Nodes, VNodeMods, HandOffs, []),
+ JSON = mochijson2:encode([{nodes, Nodes}, {partitions, lists:sort(VNodes)}]),
{JSON, RD, Ctx}.
%%% Internal
+%%% HANDOFF
+handoffs_by_partition(Nodes) ->
+ HandOffs = get_all_handoffs(Nodes),
+ handoffs_by_partition(HandOffs, dict:new()).
+
+%% merge the handoffs from all nodes into a single dict
+%% of {Mod, Idx, Location} -> handingoff | receiving_handoff
+handoffs_by_partition([], HandOffs) ->
+ HandOffs;
+handoffs_by_partition([{Host, Hoffs} | Rest], HandOffs) ->
+ HandOffs2 = lists:foldl(fun({{VNodeMod, Idx}, Targets}, Dict) ->
+ append_handoff(Idx, VNodeMod, Host, Targets, Dict);
+ ([], Dict) -> Dict end, HandOffs, Hoffs),
+ handoffs_by_partition(Rest, HandOffs2).
+
+%% Add the handoff data to the dict of handoffs
+append_handoff(Idx, VNodeMod, Host, [Target], HandOffs) ->
+ D1 = dict:append({Idx, VNodeMod, Host}, handingoff, HandOffs),
+ dict:append({Idx, VNodeMod, Target}, receiving_handoff, D1).
+
+%% interogate handoff manager on each Nodes
+get_all_handoffs(Nodes) ->
+ multicall(Nodes, riak_core_handoff_manager, all_handoffs, []).
+
+%%% VNODES
+%% Evil inefficient, find a better way
+all_vnodes([], _Nodes, _VnodeMods, _HandOffs, Partitions) ->
+ lists:reverse(Partitions);
+all_vnodes([{Idx, Owner}|Rest], Nodes, VNodeMods, HandOffs, Partitions) ->
+ VNodes = vnode_data(Idx, Owner, Nodes, VNodeMods, HandOffs, []),
+ all_vnodes(Rest, Nodes, VNodeMods, HandOffs, [{Idx, [{owner, Owner}, {vnodes, VNodes}]} | Partitions]).
+
+vnode_data(_Idx, _Owner, _Nodes, [], _HandOffs, Acc) ->
+ lists:flatten(lists:reverse(Acc));
+vnode_data(Idx, Owner, Nodes, [{_Service, VNodeMod}|Rest], HandOffs, Acc) ->
+ Res = multicall(Nodes, riak_core_vnode_master, is_vnode_pid, [Idx, VNodeMod]),
+ ServiceData = lists:foldl(fun(Elem, L) ->
+ is_home_active(Idx, Owner, Elem, VNodeMod, HandOffs, L) end, [], Res),
+ vnode_data(Idx, Owner, Nodes, Rest, HandOffs, [ServiceData | Acc]).
+
+%% Given the index, owner, location, vnodemod and dict of active handoffs
+%% is the vnode running at home or away? is it giving or receiving handoff?
+is_home_active(Idx, Owner, {Node, {true, _Pid}}, VNodeMod, HandOffs, Acc) ->
+ case dict:find({Idx, VNodeMod, Node}, HandOffs) of
+ error ->
+ [{struct, [{mod, VNodeMod}, {location, Node}, {status, home_or_away(Node, Owner)}]} | Acc];
+ {ok, Status} ->
+ [{struct, [{mod, VNodeMod}, {location, Node}, {status, Status}]} | Acc]
+ end;
+is_home_active(_, _, _, _, _, Acc) ->
+ Acc.
+
+%% Partition owned by 1, running on 2, is it home or away?
+home_or_away(_Node, _Node) ->
+ home;
+home_or_away(_, _) ->
+ away.
+
+%% call MFA on Nodes, only return up responses annotated with responding node
+multicall(Nodes, Mod, Fun, Args) ->
+ {Results, Down} = rpc:multicall(Nodes, Mod, Fun, Args, infinity),
+ Up = Nodes -- Down,
+ lists:zip(Up, Results).
+
add_node_links(RD, #ctx{base_url=BaseUrl, ring=Ring}) ->
Headers = [{"Link", node_link_header(BaseUrl, Node)}
|| Node <- riak_core_ring:all_members(Ring)],
@@ -107,3 +178,4 @@ add_node_links(RD, #ctx{base_url=BaseUrl, ring=Ring}) ->
node_link_header(BaseUrl, Node) ->
io_lib:format("<~s~s>; rel=\"node\"",
[BaseUrl, mochiweb_util:quote_plus(Node)]).
+
View
120 src/admin_services_resource.erl
@@ -0,0 +1,120 @@
+%% -------------------------------------------------------------------
+%%
+%% Copyright (c) 2011 Basho Technologies, Inc.
+%%
+%% This file is provided to you 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.
+%%
+%% -------------------------------------------------------------------
+
+%% @doc Exposes services information for a node
+%%
+%% The resource exposes itself at `/admin/nodes/nodename'.
+-module(admin_services_resource).
+
+-export([
+ routes/0,
+ init/1,
+ service_available/2,
+ is_authorized/2,
+ allowed_methods/2,
+ content_types_provided/2,
+ resource_exists/2,
+ produce_json/2
+ ]).
+
+-include_lib("webmachine/include/webmachine.hrl").
+-include("riak_control.hrl").
+
+-record(ctx, {
+ nodename
+ }).
+
+-type context() :: #ctx{}.
+-type method() :: 'PUT' | 'POST' | 'GET' | 'HEAD' | 'DELETE'.
+
+%%% riak_control_sup API
+
+%% @doc Get the webmachine dispatcher config for this resource.
+-spec routes() -> [webmachine_dispatcher:matchterm()].
+routes() ->
+ [{?ADMIN_BASE_ROUTE++["nodes", 'nodename', "services"],
+ ?MODULE,
+ []}].
+
+%%% Webmachine API
+
+-spec init(list()) -> {ok, context()}.
+init(_Props) ->
+ {ok, #ctx{}}.
+
+-spec allowed_methods(wrq:reqdata(), context()) ->
+ {[method()], wrq:request(), context()}.
+allowed_methods(RD, Ctx) ->
+ {['GET'], RD, Ctx}.
+
+-spec service_available(wrq:reqdata(), context()) ->
+ {boolean() | {halt, non_neg_integer()}, wrq:reqdata(), context()}.
+service_available(RD, Ctx) ->
+ riak_control_security:scheme_is_available(RD, Ctx).
+
+-spec is_authorized(wrq:reqdata(), context()) ->
+ {true | string(), wrq:reqdata(), context()}.
+is_authorized(RD, Ctx) ->
+ riak_control_security:enforce_auth(RD, Ctx).
+
+-spec content_types_provided(wrq:reqdata(), context()) ->
+ {[{ContentType::string(), HandlerFunction::atom()}],
+ wrq:reqdata(), context()}.
+content_types_provided(RD, Ctx) ->
+ {[{"application/json", produce_json}], RD, Ctx}.
+
+-spec resource_exists(wrq:reqdata(), context()) ->
+ {boolean(), wrq:reqdata(), context()}.
+resource_exists(RD, Ctx) ->
+ Node = list_to_atom(wrq:path_info(nodename, RD)),
+ NodeCtx = Ctx#ctx{nodename=Node},
+ {is_node_in_cluster(Node), RD, NodeCtx}.
+
+-spec produce_json(wrq:reqdata(), context()) ->
+ {binary(), wrq:reqdata(), context()}.
+produce_json(RD, #ctx{nodename=Node}=Ctx) ->
+ Services = service_initials(riak_core_node_watcher:services(Node), []),
+ JSON = mochijson2:encode([{services, Services}]),
+ {JSON, RD, Ctx}.
+
+%% ===================================================================
+%% Internal functions
+%% ===================================================================
+is_node_in_cluster(Node) when is_atom(Node) ->
+ {ok, Ring} = riak_core_ring_manager:get_my_ring(),
+ Members = riak_core_ring:all_members(Ring),
+ lists:member(Node, Members).
+
+service_initials([], Acc) ->
+ lists:reverse(Acc);
+service_initials([H|T], Acc) ->
+ service_initials(T, [{struct,[{name, H}, {initial, service_initial(H)}]}|Acc]).
+
+%% Get the display initial for the service
+%% TODO maybe something more sophisticated
+%% Like capitalise first letter after riak_
+service_initial(riak_kv) ->
+ 'K';
+service_initial(riak_pipe) ->
+ 'P';
+service_initial(riak_search) ->
+ 'S';
+service_initial(Service) ->
+ Service.
View
3  src/riak_control_sup.erl
@@ -46,7 +46,8 @@ init([]) ->
Resources = [{rekon, rekon_resource},
{admin, admin_nodes_resource},
{admin, admin_node_resource},
- {admin, admin_ui_resource}],
+ {admin, admin_ui_resource},
+ {admin, admin_services_resource}],
Routes = lists:append([routes(E, M) || {E, M} <- Resources]),
[ webmachine_router:add_route(R) || R <- Routes ],

No commit comments for this range

Something went wrong with that request. Please try again.