You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by on...@apache.org on 2014/01/20 18:10:50 UTC

git commit: AMBARI-4349. Slaves API-calls. (onechiporenko)

Updated Branches:
  refs/heads/trunk aae72dfaf -> 6974a6be3


AMBARI-4349. Slaves API-calls. (onechiporenko)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/6974a6be
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/6974a6be
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/6974a6be

Branch: refs/heads/trunk
Commit: 6974a6be3b18e8788c82db088b20f3090a8312d2
Parents: aae72df
Author: Oleg Nechiporenko <on...@apache.org>
Authored: Mon Jan 20 19:10:30 2014 +0200
Committer: Oleg Nechiporenko <on...@apache.org>
Committed: Mon Jan 20 19:10:30 2014 +0200

----------------------------------------------------------------------
 ambari-web/app/assets/test/tests.js             |   1 +
 ambari-web/app/controllers/main/host.js         | 150 ++++++++++++++++++-
 ambari-web/app/messages.js                      |   4 +
 ambari-web/app/utils/ajax.js                    |  47 +++++-
 .../app/utils/batch_scheduled_requests.js       |   6 +-
 ambari-web/app/views/main/host.js               |   2 +-
 .../views/main/host/hosts_table_menu_view.js    |  79 ++++------
 .../test/utils/batch_scheduled_requests_test.js |  89 +++++++++++
 8 files changed, 320 insertions(+), 58 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/6974a6be/ambari-web/app/assets/test/tests.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/test/tests.js b/ambari-web/app/assets/test/tests.js
index 8190608..20ff4b3 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -63,6 +63,7 @@ require('test/mappers/users_mapper_test');
 require('test/utils/configs/defaults_providers/yarn_defaults_provider_test');
 require('test/utils/configs/validators/service_configs_validator_test');
 require('test/utils/ajax_test');
+require('test/utils/batch_scheduled_requests_test');
 require('test/utils/config_test');
 require('test/utils/date_test');
 require('test/utils/config_test');

http://git-wip-us.apache.org/repos/asf/ambari/blob/6974a6be/ambari-web/app/controllers/main/host.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/host.js b/ambari-web/app/controllers/main/host.js
index bca67a4..5536742 100644
--- a/ambari-web/app/controllers/main/host.js
+++ b/ambari-web/app/controllers/main/host.js
@@ -19,6 +19,7 @@
 var App = require('app');
 var validator = require('utils/validator');
 var componentHelper = require('utils/component');
+var batchUtils = require('utils/batch_scheduled_requests');
 
 App.MainHostController = Em.ArrayController.extend({
   name:'mainHostController',
@@ -137,12 +138,155 @@ App.MainHostController = Em.ArrayController.extend({
   },
 
   /**
-   * Do bulk operation for selected hosts or hostComponents
+   * Bulk operation wrapper
    * @param {Object} operationData - data about bulk operation (action, hosts or hostComponents etc)
-   * @param {Array} hostNames - list of affected hostNames
+   * @param {Array} hosts - list of affected hosts
    */
-  bulkOperation: function(operationData, hostNames) {
+  bulkOperation: function(operationData, hosts) {
+    if (operationData.componentNameFormatted) {
+      if (operationData.action === 'RESTART') {
+        this.bulkOperationForHostComponentsRestart(operationData, hosts);
+      }
+      else {
+        if (operationData.action.indexOf('DECOMMISSION') != -1) {
+          this.bulkOperationForHostComponentsDecommission(operationData, hosts);
+        }
+        else {
+          this.bulkOperationForHostComponents(operationData, hosts);
+        }
+      }
+    }
+  },
+
+  /**
+   * Do bulk operation for selected hostComponents
+   * @param {Object} operationData - data about bulk operation (action, hostComponents etc)
+   * @param {Array} hosts - list of affected hosts
+   */
+  bulkOperationForHostComponents: function(operationData, hosts) {
+    var hostsWithSelectedComponent = [];
+    hosts.forEach(function(host) {
+      var components = host.get('hostComponents');
+      if (components.findProperty('componentName', operationData.componentName)) {
+        hostsWithSelectedComponent.push(host);
+      }
+    });
+    var service = App.Service.find(operationData.serviceName);
+    var components = service.get('hostComponents').filter(function(hc) {
+      if (hc.get('componentName') != operationData.componentName) {
+        return false;
+      }
+      if(hc.get('workStatus') == operationData.action) {
+        return false;
+      }
+      return hosts.contains(hc.get('host'));
+    });
 
+    if (components.length) {
+      var hostsWithComponentInProperState = components.mapProperty('host.hostName');
+      App.ajax.send({
+        name: 'bulk_request.host_components',
+        sender: this,
+        data: {
+          hostNames: hostsWithComponentInProperState.join(','),
+          state: operationData.action,
+          requestInfo: operationData.message + ' ' + operationData.componentNameFormatted,
+          componentName: operationData.componentName
+        },
+        success: 'bulkOperationForHostComponentsSuccessCallback'
+      });
+    }
+    else {
+      App.ModalPopup.show({
+        header: Em.I18n.t('rolling.nothingToDo.header'),
+        body: Em.I18n.t('rolling.nothingToDo.body').format(operationData.componentNameFormatted),
+        secondary: false
+      });
+    }
+  },
+
+  /**
+   * Bulk decommission for selected hostComponents
+   * @param {Object} operationData
+   * @param {Array} hosts
+   */
+  bulkOperationForHostComponentsDecommission: function(operationData, hosts) {
+    var service = App.Service.find(operationData.serviceName);
+    var components = service.get('hostComponents').filter(function(hc) {
+      if (hc.get('componentName') != operationData.realComponentName) {
+        return false;
+      }
+      return hosts.contains(hc.get('host'));
+    });
+
+    if (components.length) {
+      var hostsWithComponentInProperState = components.mapProperty('host.hostName');
+      var turn_off = operationData.action.indexOf('OFF') !== -1;
+      var parameters = {
+        "slave_type": operationData.realComponentName
+      };
+      if (turn_off) {
+        parameters['included_hosts'] = hostsWithComponentInProperState.join(',')
+      }
+      else {
+        parameters['excluded_hosts'] = hostsWithComponentInProperState.join(',');
+      }
+      App.ajax.send({
+        name: 'bulk_request.decommission',
+        sender: this,
+        data: {
+          context: turn_off ? Em.I18n.t('hosts.host.datanode.recommission') : Em.I18n.t('hosts.host.datanode.decommission'),
+          serviceName: service.get('serviceName'),
+          componentName: operationData.componentName,
+          parameters: parameters
+        },
+        success: 'bulkOperationForHostComponentsSuccessCallback'
+      });
+    }
+    else {
+      App.ModalPopup.show({
+        header: Em.I18n.t('rolling.nothingToDo.header'),
+        body: Em.I18n.t('rolling.nothingToDo.body').format(operationData.componentNameFormatted),
+        secondary: false
+      });
+    }
+  },
+
+  /**
+   * Bulk restart for selected hostComponents
+   * @param {Object} operationData
+   * @param {Array} hosts
+   */
+  bulkOperationForHostComponentsRestart: function(operationData, hosts) {
+    var service = App.Service.find(operationData.serviceName);
+    var components = service.get('hostComponents').filter(function(hc) {
+      if (hc.get('componentName') != operationData.componentName) {
+        return false;
+      }
+      return hosts.contains(hc.get('host'));
+    });
+
+    if (components.length) {
+      batchUtils._doPostBatchRollingRestartRequest(components, components.length, 1, 1);
+    }
+    else {
+      App.ModalPopup.show({
+        header: Em.I18n.t('rolling.nothingToDo.header'),
+        body: Em.I18n.t('rolling.nothingToDo.body').format(operationData.componentNameFormatted),
+        secondary: false
+      });
+    }
+  },
+
+  /**
+   * Show BO popup after bulk request
+   */
+  bulkOperationForHostComponentsSuccessCallback: function() {
+    App.router.get('applicationController').dataLoading().done(function (initValue) {
+      if (initValue) {
+        App.router.get('backgroundOperationsController').showPopup();
+      }
+    });
   }
 
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/6974a6be/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index 2f69fb0..1b11a06 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -1794,6 +1794,10 @@ Em.I18n.translations = {
   'rollingrestart.dialog.msg.staleConfigsOnly': 'Only restart {0}s with stale configs',
   'rollingrestart.rest.context': 'Rolling Restart of {0}s - batch {1} of {2}',
 
+  'rolling.command.context': 'Rolling set {0} to state "{1}" - batch {2} of {3}',
+  'rolling.nothingToDo.header': 'Nothing to do',
+  'rolling.nothingToDo.body': '{0} on selected hosts are already in selected state.',
+
   'restart.service.all': 'Restart All',
   'restart.service.rest.context': 'Restart {1}s ({0})',
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/6974a6be/ambari-web/app/utils/ajax.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/ajax.js b/ambari-web/app/utils/ajax.js
index 4421d31..867b78a 100644
--- a/ambari-web/app/utils/ajax.js
+++ b/ambari-web/app/utils/ajax.js
@@ -658,12 +658,7 @@ var urls = {
   },
   'settings.get.user_pref': {
     'real': '/persist/{key}',
-    'mock': '/data/user_settings/user_pref.json',
-    'type': 'GET',
-    'format': function (data, opt) {
-      return {
-      };
-    }
+    'mock': '/data/user_settings/user_pref.json'
   },
   'settings.post.user_pref': {
     'real': '/persist',
@@ -1340,6 +1335,46 @@ var urls = {
     'mock': '/data/mirroring/{dataset}_instances.json'
   },
 
+  'bulk_request.host_components': {
+    'real': '/clusters/{clusterName}/host_components',
+    'mock': '',
+    'format': function(data) {
+      return {
+        type: 'PUT',
+        data: JSON.stringify({
+          RequestInfo: {
+            context: data.requestInfo,
+            query: 'HostRoles/component_name=' + data.componentName + '&HostRoles/host_name.in(' + data.hostNames + ')'
+          },
+          Body: {
+            HostRoles: {
+              state: data.state
+            }
+          }
+        })
+      }
+    }
+  },
+
+  'bulk_request.decommission': {
+    'real' : '/clusters/{clusterName}/requests',
+    'mock' : '',
+    'format': function(data) {
+      return {
+        type: 'POST',
+        data: JSON.stringify({
+          'RequestInfo': {
+            'context': data.context,
+            'command': 'DECOMMISSION',
+            'service_name': data.serviceName,
+            'component_name': data.componentName,
+            'parameters': data.parameters
+          }
+        })
+      }
+    }
+  },
+
   'mirroring.create_new_dataset': {
     'real': '/falcon/entities/submitAndSchedule/feed',
     'mock': '/data/mirroring/succeeded.json',

http://git-wip-us.apache.org/repos/asf/ambari/blob/6974a6be/ambari-web/app/utils/batch_scheduled_requests.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/batch_scheduled_requests.js b/ambari-web/app/utils/batch_scheduled_requests.js
index ce3bf24..bf9164f 100644
--- a/ambari-web/app/utils/batch_scheduled_requests.js
+++ b/ambari-web/app/utils/batch_scheduled_requests.js
@@ -70,7 +70,7 @@ module.exports = {
   restartAllServiceHostComponents: function(serviceName, staleConfigsOnly) {
     var service = App.Service.find(serviceName);
     if (service) {
-      var hostComponents = service.get('hostComponents').filterProperty('isClient', false);
+      var hostComponents = service.get('hostComponents');
       if (staleConfigsOnly) {
         hostComponents = hostComponents.filterProperty('staleConfigs', true);
       }
@@ -129,9 +129,10 @@ module.exports = {
    * @param {Function} errorCallback
    */
   _doPostBatchRollingRestartRequest: function(restartHostComponents, batchSize, intervalTimeSeconds, tolerateSize, successCallback, errorCallback) {
+    successCallback = successCallback ? successCallback : defaultSuccessCallback;
     errorCallback = errorCallback ? errorCallback : defaultErrorCallback;
     if (!restartHostComponents.length) {
-      console.log('No batch rolling restart if restartHostComponents is empty!');
+      console.log('No batch rolling restart if no restartHostComponents provided!');
       return;
     }
     App.ajax.send({
@@ -149,6 +150,7 @@ module.exports = {
       error: 'errorCallback'
     });
   },
+
   /**
    * Create list of batches for rolling restart request
    * @param {Array} restartHostComponents list host components should be restarted

http://git-wip-us.apache.org/repos/asf/ambari/blob/6974a6be/ambari-web/app/views/main/host.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/host.js b/ambari-web/app/views/main/host.js
index 93ce21c..340e71a 100644
--- a/ambari-web/app/views/main/host.js
+++ b/ambari-web/app/views/main/host.js
@@ -122,7 +122,7 @@ App.MainHostView = App.TableView.extend({
         hosts = this.get('filteredContent');
         break;
       case 'a':
-        hosts = this.get('content');
+        hosts = this.get('content').toArray();
         break;
     }
     // no hosts - no actions

http://git-wip-us.apache.org/repos/asf/ambari/blob/6974a6be/ambari-web/app/views/main/host/hosts_table_menu_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/host/hosts_table_menu_view.js b/ambari-web/app/views/main/host/hosts_table_menu_view.js
index a3cac2a..b6bd742 100644
--- a/ambari-web/app/views/main/host/hosts_table_menu_view.js
+++ b/ambari-web/app/views/main/host/hosts_table_menu_view.js
@@ -16,80 +16,64 @@
  * limitations under the License.
  */
 
+var App = require('app');
+
 App.HostTableMenuView = Em.View.extend({
 
   templateName: require('templates/main/host/bulk_operation_menu'),
 
   /**
-   * Get third-level menu items for slave components (but not for DataNode!)
+   * Get third-level menu items for slave components
    * @returns {Array}
    */
-  getSlaveItemsTemplate: function() {
+  getSlaveItemsTemplate: function(componentNameForDecommission, componentNameForOtherActions) {
     return Em.A([
       Em.Object.create({
         label: Em.I18n.t('common.start'),
         operationData: Em.Object.create({
-          action: 'start',
-          message: Em.I18n.t('common.start')
+          action: App.HostComponentStatus.started,
+          message: Em.I18n.t('common.start'),
+          componentName: componentNameForOtherActions
         })
       }),
       Em.Object.create({
         label: Em.I18n.t('common.stop'),
         operationData: Em.Object.create({
-          action: 'stop',
-          message: Em.I18n.t('common.stop')
+          action: App.HostComponentStatus.stopped,
+          message: Em.I18n.t('common.stop'),
+          componentName: componentNameForOtherActions
         })
       }),
       Em.Object.create({
         label: Em.I18n.t('common.restart'),
         operationData: Em.Object.create({
-          action: 'restart',
-          message: Em.I18n.t('common.restart')
+          action: 'RESTART',
+          message: Em.I18n.t('common.restart'),
+          componentName: componentNameForOtherActions
         })
       }),
       Em.Object.create({
-        label: Em.I18n.t('maintenance.turnOn'),
+        label: Em.I18n.t('common.decommission'),
         operationData: Em.Object.create({
-          action: 'turn_on_maintenance',
-          message: Em.I18n.t('maintenance.turnOnFor')
+          action: 'DECOMMISSION',
+          message: Em.I18n.t('common.decommission'),
+          componentName: componentNameForDecommission,
+          realComponentName: componentNameForOtherActions
         })
       }),
       Em.Object.create({
-        label: Em.I18n.t('maintenance.turnOff'),
+        label: Em.I18n.t('common.recommission'),
         operationData: Em.Object.create({
-          action: 'turn_off_maintenance',
-          message: Em.I18n.t('maintenance.turnOffFor')
+          action: 'DECOMMISSION_OFF',
+          message: Em.I18n.t('common.recommission'),
+          componentName: componentNameForDecommission,
+          realComponentName: componentNameForOtherActions
         })
       })
     ]);
   },
 
   /**
-   * Get third-level menu items for DataNode
-   * @returns {Array}
-   */
-  getDataNodeItemsTemplate: function() {
-    var dataNodesItems = this.getSlaveItemsTemplate();
-    dataNodesItems.push(Em.Object.create({
-      label: Em.I18n.t('common.decommission'),
-      operationData: Em.Object.create({
-        action: 'decommission',
-        message: Em.I18n.t('common.decommission')
-      })
-    }));
-    dataNodesItems.push(Em.Object.create({
-      label: Em.I18n.t('common.recommission'),
-      operationData: Em.Object.create({
-        action: 'recommission',
-        message: Em.I18n.t('common.recommission')
-      })
-    }));
-    dataNodesItems.setEach('operationData.componentNameFormatted', Em.I18n.t('dashboard.services.hdfs.datanodes'));
-    dataNodesItems.setEach('operationData.componentName', 'DATANODE');
-    return dataNodesItems;
-  },
-
-  /**
    * Get third-level menu items for Hosts
    * @returns {Array}
    */
@@ -142,26 +126,29 @@ App.HostTableMenuView = Em.View.extend({
     var submenu = [{label: Em.I18n.t('common.hosts'), submenu: this.getHostItemsTemplate()}];
 
     if (!!App.HDFSService.find().content.length) {
-      submenu.push({label: Em.I18n.t('dashboard.services.hdfs.datanodes'), submenu: this.getDataNodeItemsTemplate()});
+      var slaveItemsForHdfs = this.getSlaveItemsTemplate('NAMENODE', 'DATANODE');
+      slaveItemsForHdfs.setEach('operationData.serviceName', 'HDFS');
+      slaveItemsForHdfs.setEach('operationData.componentNameFormatted', Em.I18n.t('dashboard.services.hdfs.datanodes'));
+      submenu.push({label: Em.I18n.t('dashboard.services.hdfs.datanodes'), submenu: slaveItemsForHdfs});
     }
 
     if (!!App.YARNService.find().content.length) {
-      var slaveItemsForYarn = this.getSlaveItemsTemplate();
-      slaveItemsForYarn.setEach('operationData.componentName', 'NODEMANAGER');
+      var slaveItemsForYarn = this.getSlaveItemsTemplate('RESOURCEMANAGER', 'NODEMANAGER');
+      slaveItemsForYarn.setEach('operationData.serviceName', 'YARN');
       slaveItemsForYarn.setEach('operationData.componentNameFormatted', Em.I18n.t('dashboard.services.yarn.nodeManagers'));
       submenu.push({label: Em.I18n.t('dashboard.services.yarn.nodeManagers'), submenu: slaveItemsForYarn});
     }
 
     if (!!App.HBaseService.find().content.length) {
-      var slaveItemsForHBase = this.getSlaveItemsTemplate();
-      slaveItemsForHBase.setEach('operationData.componentName', 'HBASE_REGIONSERVER');
+      var slaveItemsForHBase = this.getSlaveItemsTemplate('HBASE_REGIONSERVER', 'HBASE_REGIONSERVER'); // @todo provide proper component for decommission
+      slaveItemsForHBase.setEach('operationData.serviceName', 'HBASE');
       slaveItemsForHBase.setEach('operationData.componentNameFormatted', Em.I18n.t('dashboard.services.hbase.regionServers'));
       submenu.push({label: Em.I18n.t('dashboard.services.hbase.regionServers'), submenu: slaveItemsForHBase});
     }
 
     if (!!App.MapReduceService.find().content.length) {
-      var slaveItemsForMapReduce = this.getSlaveItemsTemplate();
-      slaveItemsForMapReduce.setEach('operationData.componentName', 'TASKTRACKER');
+      var slaveItemsForMapReduce = this.getSlaveItemsTemplate('JOBTRACKER', 'TASKTRACKER');
+      slaveItemsForMapReduce.setEach('operationData.serviceName', 'MAPREDUCE');
       slaveItemsForMapReduce.setEach('operationData.componentNameFormatted', Em.I18n.t('dashboard.services.mapreduce.taskTrackers'));
       submenu.push({label: Em.I18n.t('dashboard.services.mapreduce.taskTrackers'), submenu: slaveItemsForMapReduce});
     }

http://git-wip-us.apache.org/repos/asf/ambari/blob/6974a6be/ambari-web/test/utils/batch_scheduled_requests_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/utils/batch_scheduled_requests_test.js b/ambari-web/test/utils/batch_scheduled_requests_test.js
new file mode 100644
index 0000000..3c4508e
--- /dev/null
+++ b/ambari-web/test/utils/batch_scheduled_requests_test.js
@@ -0,0 +1,89 @@
+/**
+ * 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');
+require('utils/helper');
+var batchUtils = require('utils/batch_scheduled_requests');
+
+describe('batch_scheduled_requests', function() {
+
+  describe('#getRollingRestartComponentName', function() {
+    var tests = [
+      {serviceName: 'HDFS', componentName: 'DATANODE'},
+      {serviceName: 'YARN', componentName: 'NODEMANAGER'},
+      {serviceName: 'MAPREDUCE', componentName: 'TASKTRACKER'},
+      {serviceName: 'HBASE', componentName: 'HBASE_REGIONSERVER'},
+      {serviceName: 'STORM', componentName: 'SUPERVISOR'},
+      {serviceName: 'SOME_INVALID_SERVICE', componentName: null}
+    ];
+
+    tests.forEach(function(test) {
+      it(test.serviceName + ' - ' + test.componentName, function() {
+        expect(batchUtils.getRollingRestartComponentName(test.serviceName)).to.equal(test.componentName);
+      });
+    });
+
+  });
+
+  describe('#getBatchesForRollingRestartRequest', function() {
+    var tests = [
+      {
+        hostComponents: Em.A([
+          Em.Object.create({componentName:'DATANODE', service:{serviceName:'HDFS'}, host:{hostName:'host1'}}),
+          Em.Object.create({componentName:'DATANODE', service:{serviceName:'HDFS'}, host:{hostName:'host2'}}),
+          Em.Object.create({componentName:'DATANODE', service:{serviceName:'HDFS'}, host:{hostName:'host3'}})
+        ]),
+        batchSize: 2,
+        m: 'DATANODES on three hosts, batchSize = 2',
+        e: {
+          batchCount: 2
+        }
+      },
+      {
+        hostComponents: Em.A([
+          Em.Object.create({componentName:'DATANODE', service:{serviceName:'HDFS'}, host:{hostName:'host1'}}),
+          Em.Object.create({componentName:'DATANODE', service:{serviceName:'HDFS'}, host:{hostName:'host2'}}),
+          Em.Object.create({componentName:'DATANODE', service:{serviceName:'HDFS'}, host:{hostName:'host3'}})
+        ]),
+        batchSize: 3,
+        m: 'DATANODES on 3 hosts, batchSize = 3',
+        e: {
+          batchCount: 1
+        }
+      },
+      {
+        hostComponents: Em.A([
+          Em.Object.create({componentName:'DATANODE', service:{serviceName:'HDFS'}, host:{hostName:'host1'}}),
+          Em.Object.create({componentName:'DATANODE', service:{serviceName:'HDFS'}, host:{hostName:'host2'}}),
+          Em.Object.create({componentName:'DATANODE', service:{serviceName:'HDFS'}, host:{hostName:'host3'}})
+        ]),
+        batchSize: 1,
+        m: 'DATANODES on 3 hosts, batchSize = 1',
+        e: {
+          batchCount: 3
+        }
+      }
+    ];
+
+    tests.forEach(function(test) {
+      it(test.m, function() {
+        expect(batchUtils.getBatchesForRollingRestartRequest(test.hostComponents, test.batchSize).length).to.equal(test.e.batchCount);
+      });
+    });
+  });
+
+});