浏览代码

AMBARI-763. Implement Installer Step 6 (Assign Slaves). (yusaku)

git-svn-id: https://svn.apache.org/repos/asf/incubator/ambari/branches/AMBARI-666@1387859 13f79535-47bb-0310-9956-ffa450edef68
Yusaku Sako 12 年之前
父节点
当前提交
a9c32798c8

+ 2 - 0
AMBARI-666-CHANGES.txt

@@ -12,6 +12,8 @@ AMBARI-666 branch (unreleased changes)
 
   NEW FEATURES
 
+  AMBARI-763. Implement Installer Step 6 (Assign Slaves). (yusaku)
+
   AMBARI-760. Fix injection in data access objects to use guice provider.
   (mahadev)
 

+ 1 - 0
ambari-web/app/controllers.js

@@ -26,6 +26,7 @@ require('controllers/installer/step1_controller');
 require('controllers/installer/step2_controller');
 require('controllers/installer/step3_controller');
 require('controllers/installer/step4_controller');
+require('controllers/installer/step6_controller');
 require('controllers/installer/step7_controller');
 require('controllers/main');
 require('controllers/main/service');

+ 163 - 0
ambari-web/app/controllers/installer/step6_controller.js

@@ -0,0 +1,163 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * 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.
+ */
+
+var App = require('app');
+
+/**
+ * By Step 6, we have the following information stored in App.db and set on this
+ * controller by the router:
+ *
+ *   hosts: App.db.hosts (list of all hosts the user selected in Step 3)
+ *   selectedServiceNames: App.db.selectedServiceNames (the services that the user selected in Step 4)
+ *   masterComponentHosts: App.db.masterComponentHosts (master-components-to-hosts mapping the user selected in Step 5)
+ *
+ * Step 6 will set the following information in App.db:
+ *   hostSlaveComponents: App.db.hostSlaveComponents (hosts-to-slave-components mapping the user selected in Steo 6)
+ *   slaveComponentHosts: App.db.slaveComponentHosts (slave-components-to-hosts mapping the user selected in Step 6)
+ *
+ */
+App.InstallerStep6Controller = Em.Controller.extend({
+
+  hosts: [],
+  // TODO: hook up with user host selection
+  rawHosts: require('data/mock/hosts'),
+  selectedServiceNames: null,
+  masterComponentHosts: require('data/mock/master_component_hosts'),
+
+  hasMasterComponents: function(hostname) {
+    var hasMaster = false;
+    this.get('masterComponentHosts').forEach(function(masterComponent) {
+      if (masterComponent.hosts.contains(hostname)) {
+        hasMaster = true;
+      }
+    });
+    return hasMaster;
+  },
+
+  isAllDataNodes: function() {
+    return this.get('hosts').everyProperty('isDataNode', true);
+  }.property('hosts.@each.isDataNode'),
+
+  isAllTaskTrackers: function() {
+    return this.get('hosts').everyProperty('isTaskTracker', true);
+  }.property('hosts.@each.isTaskTracker'),
+
+  isAllRegionServers: function() {
+    return this.get('hosts').everyProperty('isRegionServer', true);
+  }.property('hosts.@each.isRegionServer'),
+
+  isNoDataNodes: function() {
+    return this.get('hosts').everyProperty('isDataNode', false);
+  }.property('hosts.@each.isDataNode'),
+
+  isNoTaskTrackers: function() {
+    return this.get('hosts').everyProperty('isTaskTracker', false);
+  }.property('hosts.@each.isTaskTracker'),
+
+  isNoRegionServers: function() {
+    return this.get('hosts').everyProperty('isRegionServer', false);
+  }.property('hosts.@each.isRegionServer'),
+
+  selectAllDataNodes: function() {
+    this.get('hosts').setEach('isDataNode', true);
+  },
+
+  selectAllTaskTrackers: function() {
+    this.get('hosts').setEach('isTaskTracker', true);
+  },
+
+  selectAllRegionServers: function() {
+    this.get('hosts').setEach('isRegionServer', true);
+  },
+
+  deselectAllDataNodes: function() {
+    this.get('hosts').setEach('isDataNode', false);
+  },
+
+  deselectAllTaskTrackers: function() {
+    this.get('hosts').setEach('isTaskTracker', false);
+  },
+
+  deselectAllRegionServers: function() {
+    this.get('hosts').setEach('isRegionServer', false);
+  },
+
+  init: function() {
+    this._super();
+    this.get('rawHosts').forEach(function(host) {
+      host.isDataNode = host.isTaskTracker = host.isRegionServer = !this.hasMasterComponents(host.hostname);
+      this.get('hosts').pushObject(Ember.Object.create(host));
+    }, this);
+  },
+
+  validate: function() {
+    return !(this.get('isNoDataNodes') || this.get('isNoTaskTrackers') || this.get('isNoRegionServers'));
+  },
+
+  submit: function() {
+    if (!this.validate()) {
+      this.set('errorMessage', Ember.I18n.t('installer.step6.error.mustSelectOne'));
+      return;
+    }
+    App.db.setHostSlaveComponents(this.get('host'));
+
+    var dataNodeHosts = [];
+    var taskTrackerHosts = [];
+    var regionServerHosts = [];
+
+    this.get('hosts').forEach(function (host) {
+      if (host.get('isDataNode')) {
+        dataNodeHosts.push({
+          hostname: host.hostname,
+          group: 'Default'
+        });
+      }
+      if (host.get('isTaskTracker')) {
+        taskTrackerHosts.push({
+          hostname: host.hostname,
+          group: 'Default'
+        });
+      }
+      if (host.get('isRegionServer')) {
+        regionServerHosts.push({
+          hostname: host.hostname,
+          group: 'Default'
+        });
+      }
+    });
+
+    var slaveComponentHosts = [];
+    slaveComponentHosts.push({
+      componentName: 'DataNode',
+      hosts: dataNodeHosts
+    });
+    slaveComponentHosts.push({
+      componentName: 'TaskTracker',
+      hosts: taskTrackerHosts
+    });
+    slaveComponentHosts.push({
+      componentName: 'RegionServer',
+      hosts: regionServerHosts
+    });
+
+    App.db.setSlaveComponentHosts(slaveComponentHosts);
+
+    App.router.transitionTo('step7');
+
+  }
+});

+ 29 - 14
ambari-web/app/controllers/installer/step7_controller.js

@@ -19,8 +19,7 @@
 var App = require('app');
 
 /**
- * Controller for Step 7 of the Installer Wizard.
- * By Step 7, we have the following information stored in App.db (localStorage) and set on this
+ * By Step 7, we have the following information stored in App.db and set on this
  * controller by the router.
  *
  *   selectedServices: App.db.selectedServices (the services that the user selected in Step 4)
@@ -43,19 +42,28 @@ App.InstallerStep7Controller = Em.ArrayController.extend({
   }.property('@each.errorCount'),
 
   // TODO: set attributes from localStorage in router
-  // var selectedServiceNames = App.db.getSelectedServiceNames();
   selectedServiceNames: [ 'HDFS', 'MAPREDUCE', 'GANGLIA', 'NAGIOS', 'HBASE', 'PIG', 'SQOOP', 'OOZIE', 'HIVE', 'ZOOKEEPER'],
-  masterComponentHosts: '',
-  slaveComponentHosts: '',
+  masterComponentHosts: require('data/mock/master_component_hosts'),
+  slaveComponentHosts: require('data/mock/slave_component_hosts'),
 
   doInit: true,
 
   loadConfigs: function() {
 
+    // load dependent data from the database
     var selectedServiceNamesInDB = App.db.getSelectedServiceNames();
     if (selectedServiceNamesInDB !== undefined) {
       this.set('selectedServiceNames', selectedServiceNamesInDB);
     }
+    var masterComponentHostsInDB = App.db.getMasterComponentHosts();
+    if (masterComponentHostsInDB != undefined) {
+      this.set('masterComponentHosts', masterComponentHostsInDB);
+    }
+    var slaveComponentHostsInDB = App.db.getSlaveComponentHosts();
+    if (slaveComponentHostsInDB != undefined) {
+      this.set('slaveComponentHosts', slaveComponentHostsInDB);
+    }
+
     // TODO: check App.db to see if configs have been saved already
     if (this.doInit) {
       var serviceConfigs = require('data/service_configs');
@@ -128,15 +136,22 @@ App.SlaveComponentGroupsController = Ember.ArrayController.extend({
 
   name: 'slaveComponentGroupsController',
 
-  // TODO: Set up binding to use actual data
-  //contentBinding: 'App.router.installerStep7Controller.slaveComponentHosts',
-  content: require('data/mock/slave_component_hosts'),
+  contentBinding: 'App.router.installerStep7Controller.slaveComponentHosts',
+
+  selectedComponentName: function() {
+    switch (App.router.get('installerStep7Controller.selectedService.serviceName')) {
+      case 'HDFS':
+        return 'DataNode';
+      case 'MAPREDUCE':
+        return 'TaskTracker';
+      case 'HBASE':
+        return 'RegionServer';
+    }
 
-  selectedComponentName: 'DataNode',
+  }.property('App.router.installerStep7Controller.selectedService'),
 
   showAddSlaveComponentGroup: function (event) {
     var componentName = event.context;
-    this.set('selectedComponentName', componentName);
     App.ModalPopup.show({
       header: componentName + ' Groups',
       bodyClass: Ember.View.extend({
@@ -153,11 +168,11 @@ App.SlaveComponentGroupsController = Ember.ArrayController.extend({
   },
 
   hosts: function() {
-    return this.filterProperty('componentName', this.get('selectedComponentName'))[0].hosts;
-  }.property('@each.hosts'),
+    return this.findProperty('componentName', this.get('selectedComponentName')).hosts;
+  }.property('@each.hosts', 'selectedComponentName'),
 
   groups: function() {
-    return this.filterProperty('componentName', this.get('selectedComponentName'))[0].hosts.mapProperty('group').uniq();
-  }.property('@each.hosts')
+    return this.findProperty('componentName', this.get('selectedComponentName')).hosts.mapProperty('group').uniq();
+  }.property('@each.hosts', 'selectedComponentName')
 
 });

+ 35 - 0
ambari-web/app/data/mock/hosts.js

@@ -0,0 +1,35 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * 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.
+ */
+
+module.exports = [
+  {
+    hostname: 'host0001.company.com'
+  },
+  {
+    hostname: 'host0002.company.com'
+  },
+  {
+    hostname: 'host0003.company.com'
+  },
+  {
+    hostname: 'host0004.company.com'
+  },
+  {
+    hostname: 'host0005.company.com'
+  }
+];

+ 76 - 0
ambari-web/app/data/mock/master_component_hosts.js

@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * 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.
+ */
+
+module.exports = [
+  {
+    componentName: 'NameNode',
+    hosts: [
+      'host0001.company.com'
+    ]
+  },
+  {
+    componentName: 'SNameNode',
+    hosts: [
+      'host0002.company.com'
+    ]
+  },
+  {
+    componentName: 'JobTracker',
+    hosts: [
+      'host0003.company.com'
+    ]
+  },
+  {
+    componentName: 'Hive Metastore',
+    hosts: [
+      'host0003.company.com'
+    ]
+  },
+  {
+    componentName: 'HBase Master',
+    hosts: [
+      'host0003.company.com'
+    ]
+  },
+  {
+    componentName: 'Oozie Server',
+    hosts: [
+      'host0002.company.com'
+    ]
+  },
+  {
+    componentName: 'Nagios Server',
+    hosts: [
+      'host0002.company.com'
+    ]
+  },
+  {
+    componentName: 'Ganglia Collector',
+    hosts: [
+      'host0002.company.com'
+    ]
+  },
+  {
+    componentName: 'ZooKeeper Server',
+    hosts: [
+      'host0001.company.com',
+      'host0002.company.com',
+      'host0003.company.com'
+    ]
+  }
+]

+ 1 - 0
ambari-web/app/ember.js

@@ -0,0 +1 @@
+module.exports = Ember;

+ 3 - 0
ambari-web/app/messages.js

@@ -87,8 +87,11 @@ Em.I18n.translations = {
   'installer.step4.body': 'Choose which services you want to install on your cluster.<br>Note that some services have dependencies (e.g., HBase requires ZooKeeper.)',
 
   'installer.step5.header': 'Assign Masters',
+  'installer.step5.body': 'Assign master components to hosts you want to run them on.',
 
   'installer.step6.header': 'Assign Slaves',
+  'installer.step6.body': 'Assign slave components to hosts you want to run them on.',
+  'installer.step6.error.mustSelectOne': 'You must assign at least one host to each.',
 
   'installer.step7.header': 'Customize Services',
   'installer.step7.body': 'We have come up with recommended configurations for the services you selected.  Customize them as you see fit.',

+ 1 - 1
ambari-web/app/styles/application.less

@@ -82,7 +82,7 @@ h1 {
         background-color: #fff;
 
     }
-    #step4 {
+    #step4, #step6 {
         a.selected {
             color: #333;
         }

+ 1 - 1
ambari-web/app/templates/installer/slave_hosts.hbs

@@ -5,5 +5,5 @@ No host assigned
 {{value}}
 {{/if}}
 {{#if view.hasMultipleHosts}}
-<a href="#" {{action showEditSlaveComponentGroups view.serviceConfig.category target="App.router.slaveComponentGroupsController"}}>{{value.firstObject}} and {{view.otherLength}} others</a>
+<a href="#" {{action showEditSlaveComponentGroups view.serviceConfig.category target="App.router.slaveComponentGroupsController"}}>{{hosts.firstObject.hostname}} and {{view.otherLength}}</a>
 {{/if}}

+ 1 - 1
ambari-web/app/templates/installer/step4.hbs

@@ -17,11 +17,11 @@
 -->
 
 
+<div id="step4">
 <h2>{{t installer.step4.header}}</h2>
 <div class="alert alert-info">
   {{t installer.step4.body}}
 </div>
-<div id="step4">
   <table class="table table-striped">
     <thead>
     <tr>

+ 35 - 3
ambari-web/app/templates/installer/step6.hbs

@@ -16,9 +16,41 @@
 * limitations under the License.
 -->
 
+<div id="step6">
+  <h2>{{t installer.step6.header}}</h2>
 
-<h2>{{t installer.step6.header}}</h2>
-<div class="btn-area">
+  <div class="alert alert-info">{{t installer.step6.body}}</div>
+  {{#if errorMessage}}
+  <div class="alert alert-error">{{errorMessage}}</div>
+  {{/if}}
+  <table class="table table-striped">
+    <thead>
+    <tr>
+      <th>Host</th>
+      <th>
+        <a href="#" {{bindAttr class="isAllDataNodes:selected:deselected"}} {{action selectAllDataNodes target="controller"}}>all</a> | <a href="#" {{bindAttr class="isNoDataNodes:selected:deselected"}} {{action deselectAllDataNodes target="controller"}}>none</a>
+      </th>
+      <th>
+        <a href="#" {{bindAttr class="isAllTaskTrackers:selected:deselected"}} {{action selectAllTaskTrackers target="controller"}}>all</a> | <a href="#" {{bindAttr class="isNoTaskTrackers:selected:deselected"}} {{action deselectAllTaskTrackers target="controller"}}>none</a>
+      </th>
+      <th>
+        <a href="#" {{bindAttr class="isAllRegionServers:selected:deselected"}} {{action selectAllRegionServers target="controller"}}>all</a> | <a href="#" {{bindAttr class="isNoRegionServers:selected:deselected"}} {{action deselectAllRegionServers target="controller"}}>none</a>
+      </th>
+    </tr>
+    </thead>
+    <tbody>
+    {{#each hosts}}
+    <tr>
+      <td>{{hostname}}</td>
+      <td><label class="checkbox">{{view Ember.Checkbox checkedBinding="isDataNode"}}DataNode</label></td>
+      <td><label class="checkbox">{{view Ember.Checkbox checkedBinding="isTaskTracker"}}TaskTracker</label></td>
+      <td><label class="checkbox">{{view Ember.Checkbox checkedBinding="isRegionServer"}}RegionServer</label></td>
+    </tr>
+    {{/each}}
+    </tbody>
+  </table>
+  <div class="btn-area">
     <a class="btn" {{action back}}>Back</a>
-    <a class="btn btn-success" {{action next}}>Next</a>
+    <a class="btn btn-success" style="float:right" {{action submit target="controller"}}>Next</a>
+  </div>
 </div>

+ 39 - 0
ambari-web/app/utils/db.js

@@ -166,6 +166,27 @@ App.db.setSelectedServiceNames = function(serviceNames) {
   localStorage.setObject('ambari', App.db.data);
 }
 
+App.db.setMasterComponentHosts = function(masterComponentHosts) {
+  App.db.data = localStorage.getObject('ambari');
+  var user = App.db.data.app.loginName;
+  App.db.data[user].Installer.masterComponentHosts = masterComponentHosts;
+  localStorage.setObject('ambari', App.db.data);
+}
+
+App.db.setHostSlaveComponents = function(hostSlaveComponents) {
+  App.db.data = localStorage.getObject('ambari');
+  var user = App.db.data.app.loginName;
+  App.db.data[user].Installer.hostSlaveComponents = hostSlaveComponents;
+  localStorage.setObject('ambari', App.db.data);
+}
+
+App.db.setSlaveComponentHosts = function(slaveComponentHosts) {
+  App.db.data = localStorage.getObject('ambari');
+  var user = App.db.data.app.loginName;
+  App.db.data[user].Installer.slaveComponentHosts = slaveComponentHosts;
+  localStorage.setObject('ambari', App.db.data);
+}
+
 /*
  *  getter methods
  */
@@ -234,4 +255,22 @@ App.db.getSelectedServiceNames = function() {
   return App.db.data[user].Installer.selectedServiceNames;
 }
 
+App.db.getMasterComponentHosts = function() {
+  App.db.data = localStorage.getObject('ambari');
+  var user = App.db.data.app.loginName;
+  return App.db.data[user].Installer.masterComponentHosts;
+}
+
+App.db.getHostSlaveComponents = function() {
+  App.db.data = localStorage.getObject('ambari');
+  var user = App.db.data.app.loginName;
+  return App.db.data[user].Installer.hostSlaveComponents;
+}
+
+App.db.getSlaveComponentHosts = function() {
+  App.db.data = localStorage.getObject('ambari');
+  var user = App.db.data.app.loginName;
+  return App.db.data[user].Installer.slaveComponentHosts;
+}
+
 module.exports = App.db;

+ 1 - 1
ambari-web/app/views.js

@@ -38,7 +38,7 @@ require('views/installer/step2_view');
 require('views/installer/step3_view');
 require('views/installer/step4_view');
 require('views/installer/step5');
-require('views/installer/step6');
+require('views/installer/step6_view');
 require('views/installer/step7_view');
 require('views/installer/step8');
 require('views/installer/step9');

+ 1 - 6
ambari-web/app/views/installer/step6.js → ambari-web/app/views/installer/step6_view.js

@@ -21,11 +21,6 @@ var App = require('app');
 
 App.InstallerStep6View = Em.View.extend({
 
-  templateName: require('templates/installer/step6'),
-
-  submit: function(e) {
-    alert(this.get('controller.clusterName'));
-    App.router.transitionTo('step7');
-  }
+  templateName: require('templates/installer/step6')
 
 });

+ 12 - 4
ambari-web/app/views/installer/step7_view.js

@@ -158,8 +158,6 @@ App.ServiceConfigMasterHostView = Ember.View.extend(App.ServiceConfigHostPopover
 
 App.ServiceConfigMultipleHostsDisplay = Ember.Mixin.create(App.ServiceConfigHostPopoverSupport, {
 
-  valueBinding: 'serviceConfig.value',
-
   hasNoHosts: function() {
     return this.get('value').length === 0;
   }.property('value'),
@@ -173,13 +171,20 @@ App.ServiceConfigMultipleHostsDisplay = Ember.Mixin.create(App.ServiceConfigHost
   }.property('value'),
 
   otherLength: function() {
-    return this.get('value').length - 1;
+    var len = this.get('value').length;
+    if (len > 2) {
+      return (len - 1) + ' others';
+    } else {
+      return '1 other';
+    }
   }.property('value')
 
 })
 
 App.ServiceConfigMasterHostsView = Ember.View.extend(App.ServiceConfigMultipleHostsDisplay, {
 
+  valueBinding: 'serviceConfig.value',
+
   classNames: ['master-hosts', 'span6'],
   templateName: require('templates/installer/master_hosts')
 
@@ -188,7 +193,10 @@ App.ServiceConfigMasterHostsView = Ember.View.extend(App.ServiceConfigMultipleHo
 App.ServiceConfigSlaveHostsView = Ember.View.extend(App.ServiceConfigMultipleHostsDisplay, {
 
   classNames: ['slave-hosts', 'span6'],
-  templateName: require('templates/installer/slave_hosts')
+  templateName: require('templates/installer/slave_hosts'),
+
+  controllerBinding: 'App.router.slaveComponentGroupsController',
+  valueBinding: 'App.router.slaveComponentGroupsController.hosts'
 
 });
 

+ 140 - 0
ambari-web/test/installer/step6_test.js

@@ -0,0 +1,140 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * 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.
+ */
+
+var Ember = require('ember');
+var App = require('app');
+require('controllers/installer/step6_controller');
+
+describe('App.InstallerStep6Controller', function () {
+
+  var HOSTS = [ 'host1', 'host2', 'host3', 'host4' ];
+  App.InstallerStep6Controller.rawHosts = HOSTS;
+  var controller = App.InstallerStep6Controller.create();
+
+  describe('#selectAllDataNodes()', function () {
+    it('should set isDataNode to true on all hosts', function() {
+      controller.selectAllDataNodes();
+      expect(controller.get('hosts').everyProperty('isDataNode', true)).to.equal(true);
+    })
+  })
+
+  describe('#selectAllTaskTrackers()', function () {
+    it('should set isTaskTracker to true on all hosts', function() {
+      controller.selectAllTaskTrackers();
+      expect(controller.get('hosts').everyProperty('isTaskTracker', true)).to.equal(true);
+    })
+  })
+
+  describe('#selectAllRegionServers()', function () {
+    it('should set isRegionServer to true on all hosts', function() {
+      controller.selectAllRegionServers();
+      expect(controller.get('hosts').everyProperty('isRegionServer', true)).to.equal(true);
+    })
+  })
+
+  describe('#isAllDataNodes()', function () {
+
+    beforeEach(function() {
+      controller.get('hosts').setEach('isDataNode', true);
+    })
+
+    it('should return true if isDataNode is true for all services', function() {
+      expect(controller.get('isAllDataNodes')).to.equal(true);
+    })
+
+    it('should return false if isDataNode is false for one host', function() {
+      controller.get('hosts')[0].set('isDataNode', false);
+      expect(controller.get('isAllDataNodes')).to.equal(false);
+    })
+  })
+
+  describe('#isAllTaskTrackers()', function () {
+
+    beforeEach(function() {
+      controller.get('hosts').setEach('isTaskTracker', true);
+    })
+
+    it('should return true if isTaskTracker is true for all hosts', function() {
+      expect(controller.get('isAllTaskTrackers')).to.equal(true);
+    })
+
+    it('should return false if isTaskTracker is false for one host', function() {
+      controller.get('hosts')[0].set('isTaskTracker', false);
+      expect(controller.get('isAllTaskTrackers')).to.equal(false);
+    })
+
+  })
+
+  describe('#isAllRegionServers()', function () {
+
+    beforeEach(function() {
+      controller.get('hosts').setEach('isRegionServer', true);
+    });
+
+    it('should return true if isRegionServer is true for all hosts', function() {
+      expect(controller.get('isAllRegionServers')).to.equal(true);
+    })
+
+    it('should return false if isRegionServer is false for one host', function() {
+      controller.get('hosts')[0].set('isRegionServer', false);
+      expect(controller.get('isAllRegionServers')).to.equal(false);
+    })
+
+  })
+
+  describe('#validate()', function () {
+
+    beforeEach(function() {
+      controller.get('hosts').setEach('isDataNode', true);
+      controller.get('hosts').setEach('isTaskTracker', true);
+      controller.get('hosts').setEach('isRegionServer', true);
+    });
+
+    it('should return false if isDataNode is false for all hosts', function() {
+      controller.get('hosts').setEach('isDataNode', false);
+      expect(controller.validate()).to.equal(false);
+    })
+
+    it('should return false if isTaskTracker is false for all hosts', function() {
+      controller.get('hosts').setEach('isTaskTracker', false);
+      expect(controller.validate()).to.equal(false);
+    })
+
+    it('should return false if isRegionServer is false for all hosts', function() {
+      controller.get('hosts').setEach('isRegionServer', false);
+      expect(controller.validate()).to.equal(false);
+    })
+
+    it('should return true if isDataNode, isTaskTracker, and isRegionServer is true for all hosts', function() {
+      expect(controller.validate()).to.equal(true);
+    })
+
+    it('should return true if isDataNode, isTaskTracker, and isRegionServer is true for only one host', function() {
+      controller.get('hosts').setEach('isDataNode', false);
+      controller.get('hosts').setEach('isTaskTracker', false);
+      controller.get('hosts').setEach('isRegionServer', false);
+      var host = controller.get('hosts')[0];
+      host.set('isDataNode', true);
+      host.set('isTaskTracker', true);
+      host.set('isRegionServer', true);
+      expect(controller.validate()).to.equal(true);
+    })
+
+  })
+
+})