Add Cluster Menu 08/1108/3
authorAndrew Kim <andrekim@cisco.com>
Thu, 5 Sep 2013 20:15:18 +0000 (15:15 -0500)
committerAndrew Kim <andrekim@cisco.com>
Fri, 6 Sep 2013 18:09:29 +0000 (13:09 -0500)
Change-Id: Ie795953393493a41dd05a0a8a7de03ddb5278729
Signed-off-by: Andrew Kim <andrekim@cisco.com>
opendaylight/web/root/pom.xml
opendaylight/web/root/src/main/java/org/opendaylight/controller/web/ClusterNodeBean.java [new file with mode: 0644]
opendaylight/web/root/src/main/java/org/opendaylight/controller/web/DaylightWebAdmin.java
opendaylight/web/root/src/main/java/org/opendaylight/controller/web/NodeBean.java [new file with mode: 0644]
opendaylight/web/root/src/main/resources/WEB-INF/jsp/main.jsp
opendaylight/web/root/src/main/resources/css/one.less
opendaylight/web/root/src/main/resources/img/topology_view_1033_16.png [new file with mode: 0644]
opendaylight/web/root/src/main/resources/js/lib.js
opendaylight/web/root/src/main/resources/js/open.js

index 6000efa..bc5c73a 100644 (file)
@@ -34,6 +34,9 @@
               org.opendaylight.controller.sal.utils,
               org.opendaylight.controller.usermanager,
               org.opendaylight.controller.containermanager,
+              org.opendaylight.controller.clustering.services,
+              org.opendaylight.controller.connectionmanager,
+              org.opendaylight.controller.switchmanager,
               com.google.gson,
               javax.annotation,
               javax.naming,
     </plugins>
   </build>
   <dependencies>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>clustering.services</artifactId>
+      <version>0.4.0-SNAPSHOT</version>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>connectionmanager</artifactId>
+      <version>0.1.0-SNAPSHOT</version>
+    </dependency>
     <dependency>
       <groupId>org.opendaylight.controller</groupId>
       <artifactId>configuration</artifactId>
       <artifactId>containermanager</artifactId>
       <version>0.4.0-SNAPSHOT</version>
     </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>switchmanager</artifactId>
+      <version>0.5.0-SNAPSHOT</version>
+    </dependency>
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
diff --git a/opendaylight/web/root/src/main/java/org/opendaylight/controller/web/ClusterNodeBean.java b/opendaylight/web/root/src/main/java/org/opendaylight/controller/web/ClusterNodeBean.java
new file mode 100644 (file)
index 0000000..5e4f22a
--- /dev/null
@@ -0,0 +1,50 @@
+package org.opendaylight.controller.web;
+
+import java.net.InetAddress;
+
+/**
+ * Information about a clustered controller to send to the UI frontend
+ * @author andrekim
+ */
+public class ClusterNodeBean {
+    private final byte[] address;
+    private final String name;
+    private final Boolean me;
+    private final Boolean coordinator;
+
+    public static class Builder {
+        // required params
+        private final byte[] address;
+        private final String name;
+
+        // optional params
+        private Boolean me = null;
+        private Boolean coordinator = null;
+
+        public Builder(InetAddress address) {
+            this.address = address.getAddress();
+            this.name = address.getHostAddress();
+        }
+
+        public Builder highlightMe() {
+            this.me = true;
+            return this;
+        }
+
+        public Builder iAmCoordinator() {
+            this.coordinator = true;
+            return this;
+        }
+
+        public ClusterNodeBean build() {
+            return new ClusterNodeBean(this);
+        }
+    }
+
+    private ClusterNodeBean(Builder builder) {
+        this.address = builder.address;
+        this.name = builder.name;
+        this.me = builder.me;
+        this.coordinator = builder.coordinator;
+    }
+}
\ No newline at end of file
index 524cb62..d9aa03e 100644 (file)
@@ -8,14 +8,24 @@
 
 package org.opendaylight.controller.web;
 
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 import javax.servlet.http.HttpServletRequest;
 
+import org.opendaylight.controller.clustering.services.IClusterGlobalServices;
+import org.opendaylight.controller.connectionmanager.IConnectionManager;
 import org.opendaylight.controller.sal.authorization.UserLevel;
+import org.opendaylight.controller.sal.core.Description;
+import org.opendaylight.controller.sal.core.Node;
+import org.opendaylight.controller.sal.utils.GlobalConstants;
 import org.opendaylight.controller.sal.utils.ServiceHelper;
 import org.opendaylight.controller.sal.utils.Status;
 import org.opendaylight.controller.sal.utils.StatusCode;
+import org.opendaylight.controller.switchmanager.ISwitchManager;
 import org.opendaylight.controller.usermanager.IUserManager;
 import org.opendaylight.controller.usermanager.UserConfig;
 import org.springframework.stereotype.Controller;
@@ -30,14 +40,95 @@ import com.google.gson.Gson;
 @Controller
 @RequestMapping("/admin")
 public class DaylightWebAdmin {
+    Gson gson = new Gson();
 
+    /**
+     * Returns list of clustered controllers. Highlights "this" controller and
+     * if controller is coordinator
+     * @return List<ClusterBean>
+     */
+    @RequestMapping("/cluster")
+    @ResponseBody
+    public String getClusteredControllers() {
+        IClusterGlobalServices clusterServices = (IClusterGlobalServices) ServiceHelper.getGlobalInstance(
+                IClusterGlobalServices.class, this);
+        if (clusterServices == null) {
+            return null;
+        }
+
+        List<ClusterNodeBean> clusters = new ArrayList<ClusterNodeBean>();
+
+        List<InetAddress> controllers = clusterServices.getClusteredControllers();
+        for (InetAddress controller : controllers) {
+            ClusterNodeBean.Builder clusterBeanBuilder = new ClusterNodeBean.Builder(controller);
+            if (controller.equals(clusterServices.getMyAddress())) {
+                clusterBeanBuilder.highlightMe();
+            }
+            if (clusterServices.amICoordinator()) {
+                clusterBeanBuilder.iAmCoordinator();
+            }
+
+            clusters.add(clusterBeanBuilder.build());
+        }
+
+        return gson.toJson(clusters);
+    }
+
+    /**
+     * Return nodes connected to controller {controller}
+     * @param cluster
+     *            - byte[] of the address of the controller
+     * @return List<NodeBean>
+     */
+    @RequestMapping("/cluster/controller/{controller}")
+    @ResponseBody
+    public String getNodesConnectedToController(@PathVariable("controller") String cluster) {
+        IClusterGlobalServices clusterServices = (IClusterGlobalServices) ServiceHelper.getGlobalInstance(
+                IClusterGlobalServices.class, this);
+        if (clusterServices == null) {
+            return null;
+        }
+        IConnectionManager connectionManager = (IConnectionManager) ServiceHelper.getGlobalInstance(
+                IConnectionManager.class, this);
+        if (connectionManager == null) {
+            return null;
+        }
+        ISwitchManager switchManager = (ISwitchManager) ServiceHelper.getInstance(ISwitchManager.class,
+                GlobalConstants.DEFAULT.toString(), this);
+        if (switchManager == null) {
+            return null;
+        }
+
+        byte[] address = gson.fromJson(cluster, byte[].class);
+        InetAddress clusterAddress = null;
+        try {
+            clusterAddress = InetAddress.getByAddress(address);
+        } catch (UnknownHostException e) {
+            return null;
+        }
+        InetAddress thisCluster = clusterServices.getMyAddress();
+
+        List<NodeBean> result = new ArrayList<NodeBean>();
+
+        Set<Node> nodes = connectionManager.getNodes(thisCluster);
+        for (Node node : nodes) {
+            Description description = (Description) switchManager.getNodeProp(node, Description.propertyName);
+            NodeBean nodeBean;
+            if (description == null || description.getValue().equals("None")) {
+                nodeBean = new NodeBean(node);
+            } else {
+                nodeBean = new NodeBean(node, description.getValue());
+            }
+            result.add(nodeBean);
+        }
 
+        return gson.toJson(result);
+    }
 
     @RequestMapping("/users")
     @ResponseBody
     public List<UserConfig> getUsers() {
-        IUserManager userManager = (IUserManager) ServiceHelper
-                .getGlobalInstance(IUserManager.class, this);
+        IUserManager userManager = (IUserManager) ServiceHelper.getGlobalInstance(IUserManager.class, this);
         if (userManager == null) {
             return null;
         }
@@ -52,13 +143,10 @@ public class DaylightWebAdmin {
      */
     @RequestMapping(value = "/users", method = RequestMethod.POST)
     @ResponseBody
-    public String saveLocalUserConfig(
-            @RequestParam(required = true) String json,
-            @RequestParam(required = true) String action,
-            HttpServletRequest request) {
+    public String saveLocalUserConfig(@RequestParam(required = true) String json,
+            @RequestParam(required = true) String action, HttpServletRequest request) {
 
-        IUserManager userManager = (IUserManager) ServiceHelper
-                .getGlobalInstance(IUserManager.class, this);
+        IUserManager userManager = (IUserManager) ServiceHelper.getGlobalInstance(IUserManager.class, this);
         if (userManager == null) {
             return "Internal Error";
         }
@@ -70,10 +158,9 @@ public class DaylightWebAdmin {
         Gson gson = new Gson();
         UserConfig config = gson.fromJson(json, UserConfig.class);
 
-        Status result = (action.equals("add")) ? userManager
-                .addLocalUser(config) : userManager.removeLocalUser(config);
-        if(result.getCode().equals(StatusCode.SUCCESS)) {
-            String userAction=(action.equals("add")) ? "added":"removed";
+        Status result = (action.equals("add")) ? userManager.addLocalUser(config) : userManager.removeLocalUser(config);
+        if (result.isSuccess()) {
+            String userAction = (action.equals("add")) ? "added" : "removed";
             DaylightWebUtil.auditlog("User", request.getUserPrincipal().getName(), userAction, config.getUser());
             return "Success";
         }
@@ -82,16 +169,14 @@ public class DaylightWebAdmin {
 
     @RequestMapping(value = "/users/{username}", method = RequestMethod.POST)
     @ResponseBody
-    public String removeLocalUser(@PathVariable("username") String userName,
-            HttpServletRequest request) {
+    public String removeLocalUser(@PathVariable("username") String userName, HttpServletRequest request) {
 
         String username = request.getUserPrincipal().getName();
         if (username.equals(userName)) {
             return "Invalid Request: User cannot delete itself";
         }
 
-        IUserManager userManager = (IUserManager) ServiceHelper
-                .getGlobalInstance(IUserManager.class, this);
+        IUserManager userManager = (IUserManager) ServiceHelper.getGlobalInstance(IUserManager.class, this);
         if (userManager == null) {
             return "Internal Error";
         }
@@ -101,7 +186,7 @@ public class DaylightWebAdmin {
         }
 
         Status result = userManager.removeLocalUser(userName);
-        if(result.getCode().equals(StatusCode.SUCCESS)) {
+        if (result.isSuccess()) {
             DaylightWebUtil.auditlog("User", request.getUserPrincipal().getName(), "removed", userName);
             return "Success";
         }
@@ -112,8 +197,7 @@ public class DaylightWebAdmin {
     @ResponseBody
     public Status changePassword(@PathVariable("username") String username, HttpServletRequest request,
             @RequestParam("currentPassword") String currentPassword, @RequestParam("newPassword") String newPassword) {
-        IUserManager userManager = (IUserManager) ServiceHelper
-                .getGlobalInstance(IUserManager.class, this);
+        IUserManager userManager = (IUserManager) ServiceHelper.getGlobalInstance(IUserManager.class, this);
         if (userManager == null) {
             return new Status(StatusCode.GONE, "User Manager not found");
         }
@@ -127,7 +211,7 @@ public class DaylightWebAdmin {
         }
 
         Status status = userManager.changeLocalUserPassword(username, currentPassword, newPassword);
-        if(status.isSuccess()){
+        if (status.isSuccess()) {
             DaylightWebUtil.auditlog("User", request.getUserPrincipal().getName(), "changed password for", username);
         }
         return status;
@@ -135,11 +219,9 @@ public class DaylightWebAdmin {
 
     /**
      * Is the operation permitted for the given level
-     *
      * @param level
      */
-    private boolean authorize(IUserManager userManager, UserLevel level,
-            HttpServletRequest request) {
+    private boolean authorize(IUserManager userManager, UserLevel level, HttpServletRequest request) {
         String username = request.getUserPrincipal().getName();
         UserLevel userLevel = userManager.getUserLevel(username);
         return userLevel.toNumber() <= level.toNumber();
diff --git a/opendaylight/web/root/src/main/java/org/opendaylight/controller/web/NodeBean.java b/opendaylight/web/root/src/main/java/org/opendaylight/controller/web/NodeBean.java
new file mode 100644 (file)
index 0000000..21d9310
--- /dev/null
@@ -0,0 +1,21 @@
+package org.opendaylight.controller.web;
+
+import org.opendaylight.controller.sal.core.Node;
+
+/**
+ * Information about a node connected to a controller to send to the UI frontend
+ * @author andrekim
+ */
+public class NodeBean {
+    private final String node;
+    private final String description;
+
+    public NodeBean(Node node) {
+        this(node, node.toString());
+    }
+
+    public NodeBean(Node node, String description) {
+        this.node = node.toString();
+        this.description = description;
+    }
+}
index c7c3ef1..c795a5d 100644 (file)
       <div class="icon-user"></div> ${username} <span class="caret"></span>
      </a>
      <ul class="dropdown-menu">
-      <li><a href="#admin" id="admin" data-role="${role}"><div
-         class="icon-users"></div> Users</a></li>
-      <li><a href="#save" id="save"><div class="icon-save"></div>
-        Save</a></li>
-      <li><a href="#logout" id="logout"><div
-         class="icon-logout"></div> Logout</a></li>
+      <li><a href="#admin" id="admin" data-role="${role}"><div class="icon-users"></div> Users</a></li>
+      <li><a href="#cluster" id="cluster"><div class="icon-cluster"></div>Cluster</a></li>
+      <li><a href="#save" id="save"><div class="icon-save"></div>Save</a></li>
+      <li><a href="#logout" id="logout"><div class="icon-logout"></div> Logout</a></li>
      </ul>
     </div>
    </div>
index 6ec818c..db9d763 100644 (file)
                                .icon;
                                background-image: url('../img/user_group_0107_16.png');
                        }
+                       .icon-cluster {
+                               .icon;
+                               background-image: url('../img/topology_view_1033_16.png');
+                       }
                        .icon-save {
                                .icon;
                                background-image: url('../img/save_as_0106_16.png');
diff --git a/opendaylight/web/root/src/main/resources/img/topology_view_1033_16.png b/opendaylight/web/root/src/main/resources/img/topology_view_1033_16.png
new file mode 100644 (file)
index 0000000..1a13254
Binary files /dev/null and b/opendaylight/web/root/src/main/resources/img/topology_view_1033_16.png differ
index 811d35e..64dc098 100644 (file)
@@ -26,6 +26,17 @@ one.lib.dashlet = {
         $h4.text(header);
         return $h4;
     },
+    label : function(name, type) {
+       var $span = $(document.createElement('span'));
+       $span.addClass('label');
+       if (type !== undefined) {
+               $span.addClass(type);
+       } else if (type !== null) {
+               $span.addClass('label-info');
+       }
+       $span.append(name);
+       return $span;
+    },
     list : function(list) {
         var $ul = $(document.createElement('ul'));
         $(list).each(function(index, value) {
index b82a85a..599922d 100644 (file)
@@ -175,12 +175,9 @@ one.main.admin = {
         },
         footer : function() {
             var footer = [];
-
-            var closeButton = one.lib.dashlet.button.single("Close",
-                    one.main.admin.id.modal.close, "", "");
+            var closeButton = one.lib.dashlet.button.single('Close', one.main.admin.id.modal.close, '', '');
             var $closeButton = one.lib.dashlet.button.button(closeButton);
             footer.push($closeButton);
-
             return footer;
         }
     },
@@ -257,76 +254,53 @@ one.main.admin = {
                 var $body = one.main.admin.remove.body();
                 var $modal = one.lib.modal.spawn(one.main.admin.id.modal.user,
                         h3, $body, footer);
-
                 // close binding
-                $('#' + one.main.admin.id.modal.remove.close, $modal).click(
-                        function() {
-                            $modal.modal('hide');
-                        });
-
+                $('#'+one.main.admin.id.modal.remove.close, $modal).click(function() {
+                                       $modal.modal('hide');
+                               });
                 // remove binding
-                $('#' + one.main.admin.id.modal.remove.user, $modal)
-                        .click(
-                                function() {
-                                    one.main.admin.remove.modal
-                                            .ajax(
-                                                    id,
-                                                    function(result) {
-                                                        if (result == 'Success') {
-                                                            $modal
-                                                                    .modal('hide');
-                                                            // body inject
-                                                            var $admin = $('#'
-                                                                    + one.main.admin.id.modal.main);
-                                                            one.main.admin.ajax
-                                                                    .users(function($body) {
-                                                                        one.lib.modal.inject
-                                                                                .body(
-                                                                                        $admin,
-                                                                                        $body);
-                                                                    });
-                                                        } else
-                                                            alert("Failed to remove user: "
-                                                                    + result);
-                                                    });
-                                });
-
+                $('#' + one.main.admin.id.modal.remove.user, $modal).click(function() {
+                                       one.main.admin.remove.modal.ajax(id, function(result) {
+                                               if (result == 'Success') {
+                                                       $modal.modal('hide');
+                                                       // body inject
+                                                       var $admin = $('#'+one.main.admin.id.modal.main);
+                                                       one.main.admin.ajax.users(function($body) {
+                                                               one.lib.modal.inject.body($admin, $body);
+                                                       });
+                                               } else {
+                                                       alert("Failed to remove user: " + result);
+                                               }
+                                       });
+                               });
                                // change password binding
                                $('#' + one.main.admin.id.modal.remove.password, $modal).click(function() {
                                        one.main.admin.password.initialize(id, function() {
                                                $modal.modal('hide');
                                        });
                                });
-
                 $modal.modal();
             },
             ajax : function(id, callback) {
-                $.post(one.main.admin.address.root
-                        + one.main.admin.address.users + '/' + id,
-                        function(data) {
-                            callback(data);
-                        });
+                $.post(one.main.admin.address.root + one.main.admin.address.users + '/' + id, function(data) {
+                       callback(data);
+                });
             },
         },
-
         footer : function() {
             var footer = [];
-
             var removeButton = one.lib.dashlet.button.single("Remove User",
                     one.main.admin.id.modal.remove.user, "btn-danger", "");
             var $removeButton = one.lib.dashlet.button.button(removeButton);
             footer.push($removeButton);
-
                        var change = one.lib.dashlet.button.single('Change Password',
                                        one.main.admin.id.modal.remove.password, 'btn-success', '');
                        var $change = one.lib.dashlet.button.button(change);
                        footer.push($change);
-
             var closeButton = one.lib.dashlet.button.single("Close",
                     one.main.admin.id.modal.remove.close, "", "");
             var $closeButton = one.lib.dashlet.button.button(closeButton);
             footer.push($closeButton);
-
             return footer;
         },
         body : function() {
@@ -343,40 +317,25 @@ one.main.admin = {
                 var $body = one.main.admin.add.body();
                 var $modal = one.lib.modal.spawn(one.main.admin.id.modal.user,
                         h3, $body, footer);
-
                 // close binding
-                $('#' + one.main.admin.id.modal.add.close, $modal).click(
-                        function() {
-                            $modal.modal('hide');
-                        });
-
+                $('#' + one.main.admin.id.modal.add.close, $modal).click(function() {
+                                       $modal.modal('hide');
+                               });
                 // add binding
-                $('#' + one.main.admin.id.modal.add.user, $modal)
-                        .click(
-                                function() {
-                                    one.main.admin.add.modal
-                                            .add(
-                                                    $modal,
-                                                    function(result) {
-                                                        if (result == 'Success') {
-                                                            $modal
-                                                                    .modal('hide');
-                                                            // body inject
-                                                            var $admin = $('#'
-                                                                    + one.main.admin.id.modal.main);
-                                                            one.main.admin.ajax
-                                                                    .users(function($body) {
-                                                                        one.lib.modal.inject
-                                                                                .body(
-                                                                                        $admin,
-                                                                                        $body);
-                                                                    });
-                                                        } else
-                                                            alert("Failed to add user: "
-                                                                    + result);
-                                                    });
-                                });
-
+                $('#' + one.main.admin.id.modal.add.user, $modal).click(function() {
+                                       one.main.admin.add.modal.add($modal, function(result) {
+                                               if (result == 'Success') {
+                                                       $modal.modal('hide');
+                                                       // body inject
+                                                       var $admin = $('#'+one.main.admin.id.modal.main);
+                                                       one.main.admin.ajax.users(function($body) {
+                                                               one.lib.modal.inject.body($admin, $body);
+                                                       });
+                                               } else {
+                                                       alert("Failed to add user: "+result);
+                                               }
+                                       });
+                               });
                 $modal.modal();
             },
             add : function($modal, callback) {
@@ -550,6 +509,153 @@ one.main.admin = {
        }
 }
 
+one.main.cluster = {
+       id : { // one.main.cluster.id
+               modal : 'one-main-cluster-id-modal',
+               close : 'one-main-cluster-id-close',
+               datagrid : 'one-main-cluster-id-datagrid'
+       },
+       registry : { // one.main.cluster.registry
+               cluster : undefined
+       },
+       initialize : function() {
+               var h3 = 'Cluster Management';
+               var footer = one.main.cluster.footer();
+               var $body = '';
+               var $modal = one.lib.modal.spawn(one.main.cluster.id.modal, h3, $body, footer); 
+
+               // close
+               $('#'+one.main.cluster.id.close, $modal).click(function() {
+                       $modal.modal('hide');
+               });
+
+               // body
+               $.getJSON('/admin/cluster', function(data) {
+                       var $gridHTML = one.lib.dashlet.datagrid.init(one.main.cluster.id.datagrid, {
+                               searchable: true,
+                               filterable: false,
+                               pagination: true,
+                               flexibleRowsPerPage: true
+                       }, 'table-striped table-condensed table-cursor');
+                       var source = one.main.cluster.data(data);
+                       $gridHTML.datagrid({dataSource : source}).on('loaded', function() {
+                               $(this).find('tbody tr').click(function() {
+                                       var $tr = $(this);
+                                       if ($tr.find('td:nth-child(1)').attr('colspan') === '1') {
+                                               return false;
+                                       }
+                                       var address = one.main.cluster.registry.cluster[$tr.index()];
+                                       one.main.cluster.nodes.initialize(address);
+                               });
+                       });
+                       one.lib.modal.inject.body($modal, $gridHTML);
+               });
+
+               $modal.modal();
+       },
+       data : function(data) {
+               var tdata = [];
+               var registry = [];
+               $(data).each(function(idx, val) {
+                       var name = val.name;
+                       name = one.lib.dashlet.label(name, null)[0].outerHTML;
+                       if (val.me === true) {
+                               var me = one.lib.dashlet.label('*', 'label-inverse')[0].outerHTML;
+                               name += '&nbsp;'+me;
+                       }
+                       if (val.coordinator === true) {
+                               var coord = one.lib.dashlet.label('C')[0].outerHTML;
+                               name += '&nbsp;'+coord;
+                       }
+                       tdata.push({
+                               'controller' : name
+                       });
+                       registry.push(val.address);
+               });
+               one.main.cluster.registry.cluster = registry;
+               var source = new StaticDataSource({
+                       columns : [
+                               {
+                                       property : 'controller',
+                                       label : 'Controller',
+                                       sortable : true
+                               }
+                       ],
+                       data : tdata,
+                       delay : 0
+               });
+               return source;
+       },
+       footer : function() {
+               var footer = [];
+               var close = one.lib.dashlet.button.single('Close', one.main.cluster.id.close, '', '');
+               var $close = one.lib.dashlet.button.button(close);
+               footer.push($close);
+               return footer;
+       }
+}
+
+one.main.cluster.nodes = {
+       id : { // one.main.cluster.nodes.id
+               modal : 'one-main-cluster-nodes-id-modal',
+               close : 'one-main-cluster-nodes-id-close',
+               datagrid : 'one-main-cluser-nodes-id-datagrid'
+       },
+       initialize : function(address) { // one.main.cluster.nodes.initialize
+               var h3 = 'Connected Nodes';
+               var footer = one.main.cluster.nodes.footer();
+               var $body = '';
+               var $modal = one.lib.modal.spawn(one.main.cluster.nodes.id.modal, h3, $body, footer);
+
+               // close
+               $('#'+one.main.cluster.nodes.id.close, $modal).click(function() {
+                       $modal.modal('hide');
+               });
+               
+               // body
+               $.getJSON('/admin/cluster/controller/'+JSON.stringify(address), function(data) {
+                       var $gridHTML = one.lib.dashlet.datagrid.init(one.main.cluster.nodes.id.datagrid, {
+                               searchable: true,
+                               filterable: false,
+                               pagination: true,
+                               flexibleRowsPerPage: true
+                       }, 'table-striped table-condensed');
+                       var source = one.main.cluster.nodes.data(data);
+                       $gridHTML.datagrid({dataSource : source});
+                       one.lib.modal.inject.body($modal, $gridHTML);
+               });
+
+               $modal.modal();
+       },
+       data : function(data) {
+               var tdata = [];
+               $(data).each(function(idx, val) {
+                       tdata.push({
+                               'node' : val.description
+                       });
+               });
+               var source = new StaticDataSource({
+                       columns : [
+                               {
+                                       property : 'node',
+                                       label : 'Node',
+                                       sortable : true
+                               }
+                       ],
+                       data : tdata,
+                       delay : 0
+               });
+               return source;
+       },
+       footer : function() { // one.main.cluster.nodes.footer
+               var footer = [];
+               var close = one.lib.dashlet.button.single('Close', one.main.cluster.nodes.id.close, '', '');
+               var $close = one.lib.dashlet.button.button(close);
+               footer.push($close);
+               return footer;
+       }
+}
+
 one.main.dashlet = {
     left : {
         top : $("#left-top .dashlet"),
@@ -581,6 +687,11 @@ $("#admin").click(function() {
     });
 });
 
+// cluster
+$('#cluster').click(function() {
+       one.main.cluster.initialize();
+});
+
 // save
 $("#save").click(function() {
     $.post(one.main.constants.address.save, function(data) {