Repository: ambari
Updated Branches:
  refs/heads/trunk b847a0c49 -> cda44fe2f


AMBARI-18848. Create common extensible mixin for host component and service 
configs validation
 (alexantonenko)


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

Branch: refs/heads/trunk
Commit: cda44fe2fac7c16bf333ffcad405a856a630d7e2
Parents: b847a0c
Author: Alex Antonenko <[email protected]>
Authored: Thu Nov 10 17:28:14 2016 +0200
Committer: Alex Antonenko <[email protected]>
Committed: Thu Nov 10 18:33:13 2016 +0200

----------------------------------------------------------------------
 ambari-web/app/assets/test/tests.js             |   2 +
 .../resourceManager/step2_controller.js         |   2 +-
 .../main/service/reassign/step2_controller.js   |   2 +-
 .../app/controllers/wizard/step6_controller.js  | 169 ++++++-----------
 ambari-web/app/mixins.js                        |   2 +
 ambari-web/app/mixins/common/blueprint.js       |  24 ++-
 .../host_component_recommendation_mixin.js      | 127 +++++++++++++
 .../hosts/host_component_validation_mixin.js    | 106 +++++++++++
 ambari-web/app/mixins/common/serverValidator.js |  97 +++++++---
 .../mixins/wizard/assign_master_components.js   | 176 +++++++----------
 ambari-web/app/utils/blueprint.js               |  36 ++++
 .../test/controllers/wizard/step5_test.js       |  55 ------
 .../test/controllers/wizard/step6_test.js       |   6 +-
 .../host_component_recommendation_mixin_test.js | 189 +++++++++++++++++++
 .../host_component_validation_mixin_test.js     | 188 ++++++++++++++++++
 15 files changed, 876 insertions(+), 305 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/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 5c794ea..6a71db6 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -161,6 +161,8 @@ var files = [
   'test/mixins/common/configs/configs_loader_test',
   'test/mixins/common/configs/configs_saver_test',
   'test/mixins/common/configs/toggle_isrequired_test',
+  'test/mixins/common/hosts/host_component_recommendation_mixin_test',
+  'test/mixins/common/hosts/host_component_validation_mixin_test',
   'test/mixins/common/widgets/export_metrics_mixin_test',
   'test/mixins/common/widgets/time_range_mixin_test',
   'test/mixins/common/widgets/widget_section_test',

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/app/controllers/main/admin/highAvailability/resourceManager/step2_controller.js
----------------------------------------------------------------------
diff --git 
a/ambari-web/app/controllers/main/admin/highAvailability/resourceManager/step2_controller.js
 
b/ambari-web/app/controllers/main/admin/highAvailability/resourceManager/step2_controller.js
index 8ad2a51..ec7ddd5 100644
--- 
a/ambari-web/app/controllers/main/admin/highAvailability/resourceManager/step2_controller.js
+++ 
b/ambari-web/app/controllers/main/admin/highAvailability/resourceManager/step2_controller.js
@@ -18,7 +18,7 @@
 
 var App = require('app');
 
-App.RMHighAvailabilityWizardStep2Controller = 
Em.Controller.extend(App.BlueprintMixin, App.AssignMasterComponents, {
+App.RMHighAvailabilityWizardStep2Controller = 
Em.Controller.extend(App.AssignMasterComponents, {
 
   name: "rMHighAvailabilityWizardStep2Controller",
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/app/controllers/main/service/reassign/step2_controller.js
----------------------------------------------------------------------
diff --git 
a/ambari-web/app/controllers/main/service/reassign/step2_controller.js 
b/ambari-web/app/controllers/main/service/reassign/step2_controller.js
index c730771..f40508c 100644
--- a/ambari-web/app/controllers/main/service/reassign/step2_controller.js
+++ b/ambari-web/app/controllers/main/service/reassign/step2_controller.js
@@ -18,7 +18,7 @@
 
 var App = require('app');
 
-App.ReassignMasterWizardStep2Controller = 
Em.Controller.extend(App.BlueprintMixin, App.AssignMasterComponents, {
+App.ReassignMasterWizardStep2Controller = 
Em.Controller.extend(App.AssignMasterComponents, {
 
   name: "reassignMasterWizardStep2Controller",
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/app/controllers/wizard/step6_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/wizard/step6_controller.js 
b/ambari-web/app/controllers/wizard/step6_controller.js
index f8b708c..87b0ee5 100644
--- a/ambari-web/app/controllers/wizard/step6_controller.js
+++ b/ambari-web/app/controllers/wizard/step6_controller.js
@@ -32,7 +32,7 @@ var validationUtils = require('utils/validator');
  *   slaveComponentHosts: App.db.slaveComponentHosts 
(slave-components-to-hosts mapping the user selected in Step 6)
  *
  */
-App.WizardStep6Controller = Em.Controller.extend(App.BlueprintMixin, {
+App.WizardStep6Controller = 
Em.Controller.extend(App.HostComponentValidationMixin, 
App.HostComponentRecommendationMixin, {
 
   name: 'wizardStep6Controller',
 
@@ -571,13 +571,21 @@ App.WizardStep6Controller = 
Em.Controller.extend(App.BlueprintMixin, {
 
   callValidation: function (successCallback) {
     var self = this;
+    var validationCallback = function() {
+      self.set('validationInProgress', false);
+      if (App.get('router.btnClickInProgress') && successCallback) {
+        successCallback();
+      }
+    };
     clearTimeout(this.get('timer'));
     if (this.get('validationInProgress')) {
       this.set('timer', setTimeout(function () {
-        self.callValidation(successCallback);
+        self.callValidation()
+          .then(validationCallback);
       }, 700));
     } else {
-      this.callServerSideValidation(successCallback);
+      this.callServerSideValidation()
+        .then(validationCallback);
     }
   },
 
@@ -586,18 +594,37 @@ App.WizardStep6Controller = 
Em.Controller.extend(App.BlueprintMixin, {
    * @method callServerSideValidation
    */
   callServerSideValidation: function (successCallback) {
-    var self = this;
     this.set('validationInProgress', true);
 
     var selectedServices = 
App.StackService.find().filterProperty('isSelected').mapProperty('serviceName');
     var installedServices = 
App.StackService.find().filterProperty('isInstalled').mapProperty('serviceName');
     var services = installedServices.concat(selectedServices).uniq();
+    var hostNames = this.get('hosts').mapProperty('hostName');
+
+    var bluePrintsForValidation = this.getValidationBlueprint();
+    this.set('content.recommendationsHostGroups', bluePrintsForValidation);
+
+    return this.validateSelectedHostComponents({
+      services: services,
+      hosts: hostNames,
+      blueprint: bluePrintsForValidation
+    });
+  },
 
-    var hostNames = self.get('hosts').mapProperty('hostName');
-    var slaveBlueprint = self.getCurrentBlueprint();
+  /**
+   * Returns blueprint passed to validation request
+   * @method getValidationBlueprint
+   */
+  getValidationBlueprint: function() {
+    var slaveBlueprint = this.getCurrentBlueprint();
     var masterBlueprint = null;
-    //Existing Installed but invisible masters on `Assign Masters page` should 
be included in host component layout for recommnedation/validation call
     var invisibleInstalledMasters = [];
+    var hostNames = this.get('hosts').mapProperty('hostName');
+    var invisibleSlavesAndClients = 
App.StackServiceComponent.find().filter(function (component) {
+      return component.get("isSlave") && 
component.get("isShownOnInstallerSlaveClientPage") === false ||
+        component.get("isClient") && component.get("isRequiredOnAllHosts");
+    }).mapProperty("componentName");
+    //Existing Installed but invisible masters on `Assign Masters page` should 
be included in host component layout for recommnedation/validation call
     if (this.get('isAddServiceWizard')) {
       var invisibleMasters = 
App.StackServiceComponent.find().filterProperty("isMaster").filterProperty("isShownOnAddServiceAssignMasterPage",
 false);
       invisibleInstalledMasters = invisibleMasters.filter(function(item){
@@ -605,56 +632,28 @@ App.WizardStep6Controller = 
Em.Controller.extend(App.BlueprintMixin, {
         return masterComponent && !!masterComponent.get('totalCount');
       }).mapProperty("componentName");
     }
-    var invisibleSlavesAndClients = 
App.StackServiceComponent.find().filter(function (component) {
-      return component.get("isSlave") && 
component.get("isShownOnInstallerSlaveClientPage") === false ||
-        component.get("isClient") && component.get("isRequiredOnAllHosts");
-    }).mapProperty("componentName");
     if (this.get('isInstallerWizard') || this.get('isAddServiceWizard')) {
-      masterBlueprint = self.getCurrentMastersBlueprint();
-      var selectedClientComponents = 
self.get('content.clients').mapProperty('component_name');
+      masterBlueprint = this.getCurrentMastersBlueprint();
+      var selectedClientComponents = 
this.get('content.clients').mapProperty('component_name');
       var alreadyInstalledClients = 
App.get('components.clients').reject(function (c) {
         return selectedClientComponents.contains(c);
       });
-
       var invisibleComponents = 
invisibleInstalledMasters.concat(invisibleSlavesAndClients).concat(alreadyInstalledClients);
-
       var invisibleBlueprint = 
blueprintUtils.filterByComponents(this.get('content.recommendations'), 
invisibleComponents);
       masterBlueprint = blueprintUtils.mergeBlueprints(masterBlueprint, 
invisibleBlueprint);
     } else if (this.get('isAddHostWizard')) {
-      masterBlueprint = self.getCurrentMasterSlaveBlueprint();
+      masterBlueprint = this.getCurrentMasterSlaveBlueprint();
       hostNames = 
hostNames.concat(App.Host.find().mapProperty("hostName")).uniq();
       slaveBlueprint = blueprintUtils.addComponentsToBlueprint(slaveBlueprint, 
invisibleSlavesAndClients);
     }
-
-    var bluePrintsForValidation = 
blueprintUtils.mergeBlueprints(masterBlueprint, slaveBlueprint);
-    this.set('content.recommendationsHostGroups', bluePrintsForValidation);
-
-    return App.ajax.send({
-      name: 'config.validations',
-      sender: self,
-      data: {
-        stackVersionUrl: App.get('stackVersionURL'),
-        hosts: hostNames,
-        services: services,
-        validate: 'host_groups',
-        recommendations: bluePrintsForValidation
-      },
-      success: 'updateValidationsSuccessCallback',
-      error: 'updateValidationsErrorCallback'
-    }).
-      then(function () {
-        self.set('validationInProgress', false);
-        if (App.get('router.btnClickInProgress') && successCallback) {
-          successCallback();
-        }
-      }
-    );
+    return blueprintUtils.mergeBlueprints(masterBlueprint, slaveBlueprint);
   },
 
   /**
    * Success-callback for validations request
    * @param {object} data
    * @method updateValidationsSuccessCallback
+   * @override App.HostComponentRecommendationMixin
    */
   updateValidationsSuccessCallback: function (data) {
     var self = this;
@@ -738,6 +737,7 @@ App.WizardStep6Controller = 
Em.Controller.extend(App.BlueprintMixin, {
    * @param {string} error
    * @param {object} opt
    * @method updateValidationsErrorCallback
+   * @override App.HostComponentRecommendationMixin
    */
   updateValidationsErrorCallback: function (jqXHR, ajaxOptions, error, opt) {
   },
@@ -746,46 +746,18 @@ App.WizardStep6Controller = 
Em.Controller.extend(App.BlueprintMixin, {
    * Composes selected values of comboboxes into blueprint format
    */
   getCurrentBlueprint: function () {
-    var self = this;
-
-    var res = {
-      blueprint: { host_groups: [] },
-      blueprint_cluster_binding: { host_groups: [] }
-    };
-
-    var clientComponents = 
self.get('content.clients').mapProperty('component_name');
-    var mapping = self.get('hosts');
-
-    mapping.forEach(function (item, i) {
-      var groupName = 'host-group-' + (i+1);
-
-      var hostGroup = {
-        name: groupName,
-        components: item.checkboxes.filterProperty('checked', 
true).map(function (checkbox) {
-          if (checkbox.component === "CLIENT") {
-            return clientComponents.map(function (client) {
-              return { name: client };
-            });
-          } else {
-            return { name: checkbox.component };
-          }
-        })
-      };
-
-      hostGroup.components = [].concat.apply([], hostGroup.components);
-
-      var binding = {
-        name: groupName,
-        hosts: [
-          { fqdn: item.hostName }
-        ]
-      };
-
-      res.blueprint.host_groups.push(hostGroup);
-      res.blueprint_cluster_binding.host_groups.push(binding);
-    });
-
-    return res;
+    var clientComponents = 
this.get('content.clients').mapProperty('component_name');
+    var components = this.get('hosts').reduce(function(acc, host) {
+      var checked = host.checkboxes.filterProperty('checked', 
true).mapProperty('component');
+      var componentNames = checked.indexOf('CLIENT') > -1 ? 
checked.without('CLIENT').concat(clientComponents) : checked;
+      return acc.concat(componentNames.map(function(componentName) {
+        return Em.Object.create({
+          componentName: componentName,
+          hostName: Em.get(host, 'hostName')
+        });
+      }));
+    }, []);
+    return this.getComponentsBlueprint(components);
   },
 
   /**
@@ -794,38 +766,13 @@ App.WizardStep6Controller = 
Em.Controller.extend(App.BlueprintMixin, {
    * @method getCurrentMastersBlueprint
    */
   getCurrentMastersBlueprint: function () {
-    var res = {
-      blueprint: { host_groups: [] },
-      blueprint_cluster_binding: { host_groups: [] }
-    };
-
     var masters = this.get('content.masterComponentHosts');
-    var hosts = this.get('content.hosts');
-
-    Em.keys(hosts).forEach(function (host, i) {
-      var groupName = 'host-group-' + (i + 1);
-      var components = [];
-      masters.forEach(function (master) {
-        if (master.hostName === host) {
-          components.push({
-            name: master.component
-          });
-        }
-      });
-      res.blueprint.host_groups.push({
-        name: groupName,
-        components: components
-      });
-      res.blueprint_cluster_binding.host_groups.push({
-        name: groupName,
-        hosts: [
-          {
-            fqdn: host
-          }
-        ]
-      });
-    }, this);
-    return blueprintUtils.mergeBlueprints(res, 
this.getCurrentSlaveBlueprint());
+    var mastersBlueprint = 
this.getComponentsBlueprint(masters.map(function(component) {
+      var c = Em.Object.create(component);
+      c.set('componentName', c.get('component'));
+      return c;
+    }), this.get('hosts').mapProperty('hostName'));
+    return blueprintUtils.mergeBlueprints(mastersBlueprint, 
this.getCurrentSlaveBlueprint());
   },
 
   /**

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/app/mixins.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins.js b/ambari-web/app/mixins.js
index 6deec0a..20d5a93 100644
--- a/ambari-web/app/mixins.js
+++ b/ambari-web/app/mixins.js
@@ -60,6 +60,8 @@ require('mixins/common/configs/configs_saver');
 require('mixins/common/configs/configs_loader');
 require('mixins/common/configs/configs_comparator');
 require('mixins/common/configs/toggle_isrequired');
+require('mixins/common/hosts/host_component_recommendation_mixin');
+require('mixins/common/hosts/host_component_validation_mixin');
 require('mixins/common/widgets/export_metrics_mixin');
 require('mixins/common/widgets/time_range_mixin');
 require('mixins/common/widgets/widget_mixin');

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/app/mixins/common/blueprint.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins/common/blueprint.js 
b/ambari-web/app/mixins/common/blueprint.js
index a006b9c..3df3b1a 100644
--- a/ambari-web/app/mixins/common/blueprint.js
+++ b/ambari-web/app/mixins/common/blueprint.js
@@ -23,14 +23,16 @@ var dataManipulation = require('utils/data_manipulation');
 App.BlueprintMixin = Em.Mixin.create({
 
   /**
-   * returns blueprint for all currently installed master, slave and client 
components
-   * @method getCurrentMasterSlaveBlueprint
+   * returns blueprint for selected components
+   * @method getComponentsBlueprint
+   * @param  {Em.Object[]} components components objects or model @see 
App.HostComponent
+   *  required properties are <code>hostName, componentName</code>
+   * @param {string[]} [allHostNames=[]] host names, provide when host groups 
without components should be created
+   * @return {Object} blueprint object
    */
-  getCurrentMasterSlaveBlueprint: function () {
-    var components = App.HostComponent.find();
-    var hosts = components.mapProperty("hostName").uniq();
+  getComponentsBlueprint: function(components, allHostNames) {
+    var hosts = components.mapProperty('hostName').concat(allHostNames || 
[]).uniq();
     var mappedComponents = dataManipulation.groupPropertyValues(components, 
'hostName');
-
     var res = {
       blueprint: { host_groups: [] },
       blueprint_cluster_binding: { host_groups: [] }
@@ -57,6 +59,14 @@ App.BlueprintMixin = Em.Mixin.create({
   },
 
   /**
+   * returns blueprint for all currently installed master, slave and client 
components
+   * @method getCurrentMasterSlaveBlueprint
+   */
+  getCurrentMasterSlaveBlueprint: function () {
+    return this.getComponentsBlueprint(App.HostComponent.find());
+  },
+
+  /**
    * Returns blueprint for all currently installed slave and client components
    */
   getCurrentSlaveBlueprint: function () {
@@ -69,4 +79,4 @@ App.BlueprintMixin = Em.Mixin.create({
 
     return blueprintUtils.filterByComponents(fullBlueprint, components);
   }
-});
\ No newline at end of file
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/app/mixins/common/hosts/host_component_recommendation_mixin.js
----------------------------------------------------------------------
diff --git 
a/ambari-web/app/mixins/common/hosts/host_component_recommendation_mixin.js 
b/ambari-web/app/mixins/common/hosts/host_component_recommendation_mixin.js
new file mode 100644
index 0000000..b0d58e7
--- /dev/null
+++ b/ambari-web/app/mixins/common/hosts/host_component_recommendation_mixin.js
@@ -0,0 +1,127 @@
+/**
+ * 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.
+ */
+
+require('mixins/common/blueprint');
+
+var App = require('app');
+
+/**
+ * @typedef {object} RecommendComponentObject
+ * @property {string} componentName name of the component
+ * @property {number} [size=0] desired components size
+ * @property {string[]} [hosts=[]] hosts assigned to component
+ */
+
+/**
+ * @typedef {object} HostComponentRecommendationOptions
+ * @property {string[]} hosts list of host names, in most cases all available 
host names
+ * @property {RecommendComponentObject[]} components list of components
+ * @property {string[]} services list of service names
+ * @property {object} [blueprint=null] when null blueprint will be created by 
<code>HostComponentRecommendationOptions.components</code> attribute
+ */
+
+/**
+ * @typedef {object} HostRecommendationRequestData
+ * @property {string} stackVersionUrl stack version url
+ * @property {string[]} hosts host names
+ * @property {string[]} services service names
+ * @property {string} recommend recommendation type e.g. 'host_groups'
+ * @property {object} recommendations blueprint object
+ */
+
+/**
+ * Contains methods to get server recommendation and validation for host 
components
+ * @type {Em.Mixin}
+ */
+App.HostComponentRecommendationMixin = Em.Mixin.create(App.BlueprintMixin, {
+
+  /**
+   * Get recommendations for selected components
+   * @method getRecommendedHosts
+   * @param {HostComponentRecommendationOptions} recommend list of the 
components
+   * @return {$.Deferred}
+   */
+  getRecommendedHosts: function(options) {
+    var opts = $.extend({
+      services: [],
+      hosts: [],
+      components: [],
+      blueprint: null
+    }, options || {});
+
+    opts.components = this.formatRecommendComponents(opts.components);
+    return 
this.loadComponentsRecommendationsFromServer(this.getRecommendationRequestData(opts));
+  },
+
+  /**
+   * @method formatRecommendComponents
+   * @param {RecommendComponentsObject[]} components
+   * @returns {Em.Object[]}
+   */
+  formatRecommendComponents: function(components) {
+    var res = [];
+    if (!components) return [];
+    components.forEach(function(component) {
+      var componentName = Em.get(component, 'componentName');
+      if (Em.get(component, 'hosts.length')) {
+        Em.get(component, 'hosts').forEach(function(hostName) {
+          res.push(Em.Object.create({
+            componentName: componentName,
+            hostName: hostName
+          }));
+        });
+      }
+    });
+    return res;
+  },
+
+  /**
+   * Returns request data for recommendation request
+   * @param {HostComponentRecommendationOptions} options
+   * @return {HostRecommendationRequestData}
+   * @method getRecommendationRequestData
+   */
+  getRecommendationRequestData: function(options) {
+    return {
+      recommend: 'host_groups',
+      stackVersionUrl: App.get('stackVersionURL'),
+      hosts: options.hosts,
+      services: options.services,
+      recommendations: options.blueprint || 
this.getComponentsBlueprint(options.components)
+    };
+  },
+
+  /**
+   * Get recommendations info from API
+   * @method loadComponentsRecommendationsFromServer
+   * @param {object} recommendationData
+   * @returns {$.Deferred}
+   */
+  loadComponentsRecommendationsFromServer: function(recommendationData) {
+    return App.ajax.send({
+      name: 'wizard.loadrecommendations',
+      sender: this,
+      data: recommendationData,
+      success: 'loadRecommendationsSuccessCallback',
+      error: 'loadRecommendationsErrorCallback'
+    });
+  },
+
+  loadRecommendationsSuccessCallback: function() {},
+  loadRecommendationsErrorCallback: function() {}
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/app/mixins/common/hosts/host_component_validation_mixin.js
----------------------------------------------------------------------
diff --git 
a/ambari-web/app/mixins/common/hosts/host_component_validation_mixin.js 
b/ambari-web/app/mixins/common/hosts/host_component_validation_mixin.js
new file mode 100644
index 0000000..538f83c
--- /dev/null
+++ b/ambari-web/app/mixins/common/hosts/host_component_validation_mixin.js
@@ -0,0 +1,106 @@
+/**
+ * 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.
+ */
+
+require('mixins/common/blueprint');
+
+var App = require('app');
+
+/**
+ * @typedef {object} HostValidationRequestData
+ * @property {string} stackVersionUrl stack version url
+ * @property {string[]} hosts host names
+ * @property {string[]} services service names
+ * @property {string} validate validation type e.g. 'host_groups'
+ * @property {object} recommendations blueprint object
+ */
+
+App.HostComponentValidationMixin = Em.Mixin.create(App.BlueprintMixin, {
+  /**
+   * Validate host components
+   * @method validateSelectedHostComponents
+   * @param {HostComponentRecommendationOptions} options
+   * @return {$.Deferred}
+   */
+  validateSelectedHostComponents: function(options) {
+    var opts = $.extend({
+      services: [],
+      blueprint: null,
+      hosts: [],
+      components: []
+    }, options || {});
+
+    opts.components = this.formatValidateComponents(opts.components);
+    return 
this.getHostComponentValidationRequest(this.getHostComponentValidationParams(opts));
+  },
+
+  /**
+   * @method formatValidateComponents
+   * @param {RecommendComponentObject[]} components
+   * @returns {Em.Object[]}
+   */
+  formatValidateComponents: function(components) {
+    var res = [];
+    if (!components) return [];
+    components.forEach(function(component) {
+      var componentName = Em.get(component, 'componentName');
+      if (Em.get(component, 'hosts.length')) {
+        Em.get(component, 'hosts').forEach(function(hostName) {
+          res.push(Em.Object.create({
+            componentName: componentName,
+            hostName: hostName
+          }));
+        });
+      }
+    });
+    return res;
+  },
+
+  /**
+   * Returns request data for validation request
+   * @method getHostComponentValidationParams
+   * @return {HostValidationRequestData}
+   */
+  getHostComponentValidationParams: function(options) {
+    return {
+      stackVersionUrl: App.get('stackVersionURL'),
+      hosts: options.hosts,
+      services: options.services,
+      validate: 'host_groups',
+      recommendations: options.blueprint || 
this.getComponentsBlueprint(options.components)
+    };
+  },
+
+  /**
+   * Performs request to validate components location
+   * @method getHostComponentValidationRequest
+   * @param {object} validationData
+   * @returns {$.Deferred}
+   */
+  getHostComponentValidationRequest: function(validationData) {
+    return App.ajax.send({
+      name: 'config.validations',
+      sender: this,
+      data: validationData,
+      success: 'updateValidationsSuccessCallback',
+      error: 'updateValidationsErrorCallback'
+    });
+  },
+
+  updateValidationsSuccessCallback: function() {},
+  updateValidationsErrorCallback: function() {}
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/app/mixins/common/serverValidator.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins/common/serverValidator.js 
b/ambari-web/app/mixins/common/serverValidator.js
index b251d84..319c281 100644
--- a/ambari-web/app/mixins/common/serverValidator.js
+++ b/ambari-web/app/mixins/common/serverValidator.js
@@ -19,6 +19,21 @@
 var App = require('app');
 var blueprintUtils = require('utils/blueprint');
 
+/**
+ * @typedef {object} ConfigsValidationRequestData
+ * @property {string} stackVersionUrl stack version url
+ * @property {string[]} hosts host names
+ * @property {string[]} services service names
+ * @property {string} validate validation type e.g. 'configurations'
+ * @property {object} recommendations blueprint object
+ */
+
+/**
+ * @typedef {object} ConfigsValidationOptions
+ * @property {string[]} hosts host names
+ * @property {string[]} services service names
+ * @property {object} blueprint service configurations blueprint
+ */
 App.ServerValidatorMixin = Em.Mixin.create({
 
   /**
@@ -124,41 +139,79 @@ App.ServerValidatorMixin = Em.Mixin.create({
     var stepConfigs = this.get('stepConfigs');
     var dfd = $.Deferred();
 
-    this.getBlueprintConfigurations().done(function(blueprintConfigurations) {
-      recommendations.blueprint.configurations = blueprintConfigurations;
-      App.ajax.send({
-        name: 'config.validations',
-        sender: self,
-        data: {
-          stackVersionUrl: App.get('stackVersionURL'),
+    this.getBlueprintConfigurations(this.get('stepConfigs'))
+      .done(function(blueprintConfigurations) {
+        recommendations.blueprint.configurations = blueprintConfigurations;
+        self.validateSelectedConfigs({
           hosts: self.get('hostNames'),
           services: self.get('serviceNames'),
-          validate: 'configurations',
-          recommendations: recommendations
-        },
-        success: 'validationSuccess',
-        error: 'validationError'
-      }).done(dfd.resolve).fail(dfd.reject);
-    });
+          blueprint: recommendations
+        }).done(dfd.resolve)
+          .fail(dfd.reject);
+      });
     return dfd.promise();
   },
 
   /**
+   * Perform service config validation
+   * @param validateSelectedConfigs
+   * @param {ConfigsValidationOptions} options
+   * @returns {$.Deferred}
+   */
+  validateSelectedConfigs: function(options) {
+    var opts = $.extend({
+      services: [],
+      hosts: [],
+      blueprint: null
+    }, options || {});
+
+    return 
this.getServiceConfigsValidationRequest(this.getServiceConfigsValidationParams(opts));
+  },
+
+  /**
+   * @method getServiceConfigsValidationRequest
+   * @param {ConfigsValidationRequestData} validationData
+   * @returns {$.Deferred}
+   */
+  getServiceConfigsValidationRequest: function(validationData) {
+    return App.ajax.send({
+      name: 'config.validations',
+      sender: this,
+      data: validationData,
+      success: 'validationSuccess',
+      error: 'validationError'
+    });
+  },
+
+  /**
+   * @method getServiceConfigsValidationParams
+   * @param {ConfigsValidationOptions} options
+   * @returns {ConfigsValidationRequestData}
+   */
+  getServiceConfigsValidationParams: function(options) {
+    return {
+      stackVersionUrl: App.get('stackVersionURL'),
+      hosts: options.hosts,
+      services: options.services,
+      validate: 'configurations',
+      recommendations: options.blueprint
+    };
+  },
+
+  /**
    * Return JSON for blueprint configurations
+   * @param {App.ServiceConfigs[]} serviceConfigs
    * @returns {*}
    */
-  getBlueprintConfigurations: function () {
+  getBlueprintConfigurations: function (serviceConfigs) {
     var dfd = $.Deferred();
-    var stepConfigs = this.get('stepConfigs');
-
     // check if we have configs from 'cluster-env', if not, then load them, as 
they are mandatory for validation request
-    if (!stepConfigs.findProperty('serviceName', 'MISC')) {
-      App.config.getConfigsByTypes([{site: 'cluster-env', serviceName: 
'MISC'}]).done(function (configs) {
-        stepConfigs = stepConfigs.concat(configs);
-        dfd.resolve(blueprintUtils.buildConfigsJSON(stepConfigs));
+    if (!serviceConfigs.findProperty('serviceName', 'MISC')) {
+      App.config.getConfigsByTypes([{site: 'cluster-env', serviceName: 
'MISC'}]).done(function(configs) {
+        
dfd.resolve(blueprintUtils.buildConfigsJSON(serviceConfigs.concat(configs)));
       });
     } else {
-      dfd.resolve(blueprintUtils.buildConfigsJSON(stepConfigs));
+      dfd.resolve(blueprintUtils.buildConfigsJSON(serviceConfigs));
     }
     return dfd.promise();
   },

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/app/mixins/wizard/assign_master_components.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins/wizard/assign_master_components.js 
b/ambari-web/app/mixins/wizard/assign_master_components.js
index 46409b2..70f1638 100644
--- a/ambari-web/app/mixins/wizard/assign_master_components.js
+++ b/ambari-web/app/mixins/wizard/assign_master_components.js
@@ -16,6 +16,9 @@
  * limitations under the License.
  */
 
+require('mixins/common/hosts/host_component_recommendation_mixin');
+require('mixins/common/hosts/host_component_validation_mixin');
+
 var App = require('app');
 var blueprintUtils = require('utils/blueprint');
 var numberUtils = require('utils/number_utils');
@@ -27,7 +30,7 @@ var validationUtils = require('utils/validator');
  * Should be used with controller linked with App.AssignMasterComponentsView
  * @type {Ember.Mixin}
  */
-App.AssignMasterComponents = Em.Mixin.create({
+App.AssignMasterComponents = Em.Mixin.create(App.HostComponentValidationMixin, 
App.HostComponentRecommendationMixin, {
 
   /**
    * Array of master component names to show on the page
@@ -390,39 +393,6 @@ App.AssignMasterComponents = Em.Mixin.create({
   },
 
   /**
-   * Send AJAX request to validate current host layout
-   * @param blueprint - blueprint for validation (can be with/withour 
slave/client components)
-   */
-  validate: function(blueprint, callback) {
-    var self = this;
-
-    var selectedServices = 
App.StackService.find().filterProperty('isSelected').mapProperty('serviceName');
-    var installedServices = 
App.StackService.find().filterProperty('isInstalled').mapProperty('serviceName');
-    var services = installedServices.concat(selectedServices).uniq();
-
-    var hostNames = self.get('hosts').mapProperty('host_name');
-
-    App.ajax.send({
-      name: 'config.validations',
-      sender: self,
-      data: {
-        stackVersionUrl: App.get('stackVersionURL'),
-        hosts: hostNames,
-        services: services,
-        validate: 'host_groups',
-        recommendations: blueprint
-      },
-      success: 'updateValidationsSuccessCallback',
-      error: 'updateValidationsErrorCallback'
-    }).then(function() {
-          if (callback) {
-            callback();
-          }
-        }
-    );
-  },
-
-  /**
    * Success-callback for validations request
    * @param {object} data
    * @method updateValidationsSuccessCallback
@@ -471,38 +441,34 @@ App.AssignMasterComponents = Em.Mixin.create({
   },
 
   /**
-   * Composes selected values of comboboxes into master blueprint + merge it 
with currently installed slave blueprint
+   * @override App.HostComponentRecommendationMixin
    */
-  getCurrentBlueprint: function() {
-    var self = this;
-
-    var res = {
-      blueprint: { host_groups: [] },
-      blueprint_cluster_binding: { host_groups: [] }
-    };
-
-    var mapping = self.get('masterHostMapping');
-
-    mapping.forEach(function(item, i) {
-      var group_name = 'host-group-' + (i+1);
-
-      var host_group = {
-        name: group_name,
-        components: item.masterServices.map(function(master) {
-          return { name: master.component_name };
-        })
-      };
-
-      var binding = {
-        name: group_name,
-        hosts: [ { fqdn: item.host_name } ]
-      };
+  getRecommendationRequestData: function(options) {
+    var res = this._super(options);
+    res.services = this.getCurrentServiceNames();
+    if (!this.get('isInstallerWizard')) {
+      res.recommendations = this.getCurrentMasterSlaveBlueprint();
+    }
+    return res;
+  },
 
-      res.blueprint.host_groups.push(host_group);
-      res.blueprint_cluster_binding.host_groups.push(binding);
-    });
+  /**
+   * @override App.HostComponentRecommendationMixin
+   */
+  getHostComponentValidationParams: function(options) {
+    var res = this._super(options);
+    res.services = this.getCurrentServiceNames();
+    return res;
+  },
 
-    return blueprintUtils.mergeBlueprints(res, 
self.getCurrentSlaveBlueprint());
+  /**
+   * Returns selected and installed service names
+   * @return {string[]}
+   */
+  getCurrentServiceNames: function() {
+    return App.StackService.find().filter(function(i) {
+      return i.get('isSelected') || i.get('isInstalled');
+    }).mapProperty('serviceName').uniq();
   },
 
   /**
@@ -519,7 +485,6 @@ App.AssignMasterComponents = Em.Mixin.create({
     App.StackServiceComponent.find().forEach(function (stackComponent) {
       stackComponent.set('serviceComponentId', 1);
     }, this);
-
   },
 
   /**
@@ -527,12 +492,17 @@ App.AssignMasterComponents = Em.Mixin.create({
    * @method loadStep
    */
   loadStep: function () {
+    var self = this;
     this.clearStep();
     if (this._additionalClearSteps) {
       this._additionalClearSteps();
     }
     this.renderHostInfo();
-    this.loadComponentsRecommendationsFromServer(this.loadStepCallback);
+    this.getRecommendedHosts({
+      hosts: this.get('hosts').mapProperty('host_name')
+    }).then(function() {
+      self.loadStepCallback(self.createComponentInstallationObjects(), self);
+    });
   },
 
   /**
@@ -636,50 +606,19 @@ App.AssignMasterComponents = Em.Mixin.create({
 
   /**
    * Get recommendations info from API
-   * @param {function}callback
-   * @param {boolean} includeMasters
+   * @param {object} recommendationBlueprint
    * @method loadComponentsRecommendationsFromServer
+   * @override App.HostComponentRecommendationMixin
    */
-  loadComponentsRecommendationsFromServer: function(callback, includeMasters) {
+  loadComponentsRecommendationsFromServer: function(recommendationBlueprint) {
     var self = this;
 
     if (this.get('recommendations')) {
       // Don't do AJAX call if recommendations has been already received
       // But if user returns to previous step (selecting services), stored 
recommendations will be cleared in routers' next handler and AJAX call will be 
made again
-      callback(self.createComponentInstallationObjects(), self);
-    }
-    else {
-      var selectedServices = 
App.StackService.find().filterProperty('isSelected').mapProperty('serviceName');
-      var installedServices = 
App.StackService.find().filterProperty('isInstalled').mapProperty('serviceName');
-      var services = installedServices.concat(selectedServices).uniq();
-
-      var hostNames = self.get('hosts').mapProperty('host_name');
-
-      var data = {
-        stackVersionUrl: App.get('stackVersionURL'),
-        hosts: hostNames,
-        services: services,
-        recommend: 'host_groups'
-      };
-
-      if (includeMasters) {
-        // Made partial recommendation request for reflect in blueprint 
host-layout changes which were made by user in UI
-        data.recommendations = self.getCurrentBlueprint();
-      }
-      else
-        if (!self.get('isInstallerWizard')) {
-          data.recommendations = self.getCurrentMasterSlaveBlueprint();
-        }
-
-      return App.ajax.send({
-        name: 'wizard.loadrecommendations',
-        sender: self,
-        data: data,
-        success: 'loadRecommendationsSuccessCallback',
-        error: 'loadRecommendationsErrorCallback'
-      }).then(function () {
-          callback(self.createComponentInstallationObjects(), self);
-        });
+      return $.Deferred().resolve().promise();
+    } else {
+      return this._super(recommendationBlueprint);
     }
   },
 
@@ -1159,9 +1098,14 @@ App.AssignMasterComponents = Em.Mixin.create({
     this.set('validationInProgress', true);
 
     // load recommendations with partial request
-    self.loadComponentsRecommendationsFromServer(function() {
-      // For validation use latest received recommendations because it 
contains current master layout and recommended slave/client layout
-      self.validate(self.get('recommendations'), function() {
+    this.getRecommendedHosts({
+      hosts: this.get('hosts').mapProperty('host_name'),
+      components: this.getCurrentComponentHostMap()
+    }).then(function() {
+      self.validateSelectedHostComponents({
+        hosts: self.get('hosts').mapProperty('host_name'),
+        blueprint: self.get('recommendations')
+      }).then(function() {
         if (callback) {
           callback();
         }
@@ -1174,6 +1118,28 @@ App.AssignMasterComponents = Em.Mixin.create({
     }, true);
   },
 
+  getCurrentComponentHostMap: function() {
+    return this.get('masterHostMapping').reduce(function(acc, i) {
+      var components = Em.getWithDefault(i, 'masterServices', []);
+      var hostName = Em.get(i, 'host_name');
+      components.forEach(function(i) {
+        var componentName = Em.get(i, 'component_name');
+        var component = acc.findProperty('componentName', componentName);
+        if (component) {
+          Em.set(component, 'hosts', Em.getWithDefault(component, 'hosts', 
[]).concat(hostName).uniq());
+          Em.set(component, 'size', Em.getWithDefault(component, 
'hosts.length', 0));
+        } else {
+          acc.push({
+            componentName: componentName,
+            hosts: [hostName],
+            size: 1
+          });
+        }
+      });
+      return acc;
+    }, []);
+  },
+
   _goNextStepIfValid: function () {
     if (!this.get('submitDisabled')) {
       App.router.send('next');

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/app/utils/blueprint.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/blueprint.js 
b/ambari-web/app/utils/blueprint.js
index 1148d3d..1216c3f 100644
--- a/ambari-web/app/utils/blueprint.js
+++ b/ambari-web/app/utils/blueprint.js
@@ -16,8 +16,44 @@
  * limitations under the License.
  */
 
+/**
+ * @typedef {object} BlueprintObject
+ * @property {BlueprintMappings} blueprint
+ * @property {BlueprintClusterBindings} blueprint_cluster_bindings
+ */
+/**
+ * @typedef {object} BlueprintMappings
+ * @property {BlueprintMappingsHostGroup[]} host_groups
+ */
+/**
+ * @typedef {object[]} BlueprintMappingsHostGroup
+ * @property {BlueprintHostGroupComponent[]} components
+ * @property {string} name host group name
+ */
+/**
+ * @typedef {object} BlueprintHostGroupComponent
+ * @property {string} name component name
+ */
+/**
+ * @typedef {object} BlueprintClusterBindings
+ * @property {BlueprintClusterBindingsHostGroup[]} host_groups
+ */
+/**
+ * @typedef {object} BlueprintClusterBindingsHostGroup
+ * @property {BlueprintClusterBindingsHostGroupHosts[]} hosts
+ * @property {string} name host group name
+ */
+/**
+ * @typedef {object} BlueprintClusterBindingsHostGroupHosts
+ * @property {string} fqdn host fqdn
+ */
 module.exports = {
 
+  /**
+   * @param {BlueprintObject} masterBlueprint
+   * @param {BlueprintObject} slaveBlueprint
+   * @return {BlueprintObject}
+   */
   mergeBlueprints: function(masterBlueprint, slaveBlueprint) {
     console.time('mergeBlueprints');
     var self = this;

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/test/controllers/wizard/step5_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/wizard/step5_test.js 
b/ambari-web/test/controllers/wizard/step5_test.js
index 821a709..3c9048d 100644
--- a/ambari-web/test/controllers/wizard/step5_test.js
+++ b/ambari-web/test/controllers/wizard/step5_test.js
@@ -1149,61 +1149,6 @@ describe('App.WizardStep5Controller', function () {
 
   });
 
-  describe('#getCurrentBlueprint', function () {
-
-    beforeEach(function() {
-      sinon.stub(c, 'getCurrentSlaveBlueprint', function() {
-        return {
-          blueprint_cluster_binding: {
-            host_groups: []
-          },
-          blueprint: {
-            host_groups: []
-          }
-        };
-      });
-    });
-
-    afterEach(function() {
-      c.getCurrentSlaveBlueprint.restore();
-    });
-
-    it('should map masterHostMapping', function () {
-
-      c.reopen({masterHostMapping: [
-        {host_name: 'h1', hostInfo:{}, masterServices: [
-          {serviceId: 's1', component_name: 'c1'},
-          {serviceId: 's2', component_name: 'c2'}
-        ]},
-        {host_name: 'h2', hostInfo:{}, masterServices: [
-          {serviceId: 's1', component_name: 'c1'},
-          {serviceId: 's3', component_name: 'c3'}
-        ]}
-      ]});
-
-      var r = c.getCurrentBlueprint();
-      expect(r).to.eql({"blueprint": {"host_groups": [
-          {"name": "host-group-1", "components": [
-            {"name": "c1"},
-            {"name": "c2"}
-          ]},
-          {"name": "host-group-2", "components": [
-            {"name": "c1"},
-            {"name": "c3"}
-          ]}
-        ]}, "blueprint_cluster_binding": {"host_groups": [
-          {"name": "host-group-1", "hosts": [
-            {"fqdn": "h1"}
-          ]},
-          {"name": "host-group-2", "hosts": [
-            {"fqdn": "h2"}
-          ]}
-        ]}}
-      );
-    });
-
-  });
-
   describe('#updateValidationsSuccessCallback', function() {
 
     beforeEach(function() {

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/test/controllers/wizard/step6_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/wizard/step6_test.js 
b/ambari-web/test/controllers/wizard/step6_test.js
index ed8f16a..4cf155c 100644
--- a/ambari-web/test/controllers/wizard/step6_test.js
+++ b/ambari-web/test/controllers/wizard/step6_test.js
@@ -1123,7 +1123,7 @@ describe('App.WizardStep6Controller', function () {
         masterComponentHosts: Em.A([
           {hostName: 'h1', component: 'c1'}
         ]),
-        hosts: {'h1': {}},
+        hosts: [{hostName: 'h1'}],
         m: 'one host and one component',
         e:{
           blueprint: {
@@ -1154,7 +1154,7 @@ describe('App.WizardStep6Controller', function () {
           {hostName: 'h2', component: 'c2'},
           {hostName: 'h2', component: 'c3'}
         ]),
-        hosts: {'h1': {}, 'h2': {}, 'h3': {}},
+        hosts: [{hostName: 'h1'}, {hostName: 'h2'}, {hostName: 'h3'}],
         m: 'multiple hosts and multiple components',
         e: {
           blueprint: {
@@ -1206,7 +1206,7 @@ describe('App.WizardStep6Controller', function () {
     tests.forEach(function (test) {
       it(test.m, function () {
         controller.set('content.masterComponentHosts', 
test.masterComponentHosts);
-        controller.set('content.hosts', test.hosts);
+        controller.set('hosts', test.hosts);
         var r = controller.getCurrentMastersBlueprint();
         expect(r).to.eql(test.e);
       });

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/test/mixins/common/hosts/host_component_recommendation_mixin_test.js
----------------------------------------------------------------------
diff --git 
a/ambari-web/test/mixins/common/hosts/host_component_recommendation_mixin_test.js
 
b/ambari-web/test/mixins/common/hosts/host_component_recommendation_mixin_test.js
new file mode 100644
index 0000000..2e95a06
--- /dev/null
+++ 
b/ambari-web/test/mixins/common/hosts/host_component_recommendation_mixin_test.js
@@ -0,0 +1,189 @@
+/**
+ * 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');
+
+describe('App.HostComponentRecommendationMixin', function() {
+  var mixedObject;
+  beforeEach(function() {
+    mixedObject = Em.Object.create(App.HostComponentRecommendationMixin, {});
+  });
+
+  describe('#formatRecommendComponents', function() {
+    var cases = [
+      {
+        components: undefined,
+        hosts: [],
+        m: 'when undefined components passed should return empty array',
+        e: []
+      },
+      {
+        components: [],
+        hosts: [],
+        m: 'when empty array passed should return empty array',
+        e: []
+      },
+      {
+        components: [
+          { componentName: 'C1', hosts: ['h1', 'h2']},
+          { componentName: 'C2', hosts: ['h3']},
+        ],
+        hosts: [],
+        m: 'when components passed along with hosts location should return 
correct objects',
+        e: [
+          Em.Object.create({ componentName: 'C1', hostName: 'h1'}),
+          Em.Object.create({ componentName: 'C1', hostName: 'h2'}),
+          Em.Object.create({ componentName: 'C2', hostName: 'h3'}),
+        ]
+      }
+    ];
+
+    cases.forEach(function(test) {
+      it(test.m, function() {
+        expect(mixedObject.formatRecommendComponents(test.components, 
test.hosts)).to.be.eql(test.e);
+      });
+    });
+  });
+
+  describe('#getRecommendedHosts', function() {
+    var cases;
+    beforeEach(function() {
+      sinon.stub(mixedObject, 'loadComponentsRecommendationsFromServer');
+      sinon.stub(mixedObject, 'getRecommendationRequestData');
+      sinon.stub(mixedObject, 'formatRecommendComponents', function(c) { 
return c;});
+    });
+
+    afterEach(function() {
+      mixedObject.loadComponentsRecommendationsFromServer.restore();
+      mixedObject.getRecommendationRequestData.restore();
+      mixedObject.formatRecommendComponents.restore();
+    });
+
+    cases = [
+      {
+        opts: null,
+        e: {
+          services: [],
+          hosts: [],
+          components: [],
+          blueprint: null
+        },
+        m: 'when no args passed defaults should be used and no error thrown'
+      },
+      {
+        opts: {
+          services: ['s1'],
+          hosts: ['h1'],
+          components: [{componentName: 'c1'}],
+          blueprint: {recommend:{}}
+        },
+        e: {
+          services: ['s1'],
+          hosts: ['h1'],
+          components: [{componentName: 'c1'}],
+          blueprint: {recommend:{}}
+        },
+        m: 'should normaly merge defaults with passed options'
+      }
+    ];
+
+    cases.forEach(function(test) {
+      it(test.m, function() {
+        mixedObject.getRecommendedHosts(test.opts);
+        
expect(mixedObject.getRecommendationRequestData.args[0][0]).to.be.eql(test.e);
+      });
+    });
+  });
+
+  describe('#loadComponentsRecommedationsFromServer', function() {
+    it('default request options checking', function() {
+      mixedObject.loadComponentsRecommendationsFromServer('someData');
+      expect(App.ajax.send.args[0][0]).to.be.eql({
+        name: 'wizard.loadrecommendations',
+        sender: mixedObject,
+        data: 'someData',
+        success: 'loadRecommendationsSuccessCallback',
+        error: 'loadRecommendationsErrorCallback'
+      });
+    });
+  });
+
+  describe('#getRecommendationRequestData', function() {
+    var cases;
+    beforeEach(function() {
+      sinon.stub(App, 'get').withArgs('stackVersionURL').returns('/stack/url');
+    });
+    afterEach(function() {
+      App.get.restore();
+    });
+    cases = [
+      {
+        options: {
+          hosts: ['h1'],
+          services: ['s1'],
+          components: [Em.Object.create({componentName: 'c1', hostName: 
'h1'})],
+          blueprint: null
+        },
+        e: {
+          recommend: 'host_groups',
+          stackVersionUrl: '/stack/url',
+          hosts: ['h1'],
+          services: ['s1'],
+          recommendations: {
+            blueprint: {
+              host_groups: [
+                {name: 'host-group-1',components: [{name: 'c1'}]}
+              ]
+            },
+            blueprint_cluster_binding: {
+              host_groups: [
+                {name: 'host-group-1', hosts: [{fqdn: 'h1'}]}
+              ]
+            }
+          }
+        },
+        m: 'when blueprint not passed it should be generated from components 
list'
+      },
+      {
+        options: {
+          hosts: ['h1'],
+          services: ['s1'],
+          components: [Em.Object.create({componentName: 'c1', hostName: 
'h1'})],
+          blueprint: { blueprint: {}}
+        },
+        e: {
+          recommend: 'host_groups',
+          stackVersionUrl: '/stack/url',
+          hosts: ['h1'],
+          services: ['s1'],
+          recommendations: {
+            blueprint: {}
+          }
+        },
+        m: 'when blueprint passed it should be used instead of generated 
blueprint'
+      }
+    ];
+
+    cases.forEach(function(test) {
+      it(test.m, function() {
+        
expect(mixedObject.getRecommendationRequestData(test.options)).to.be.eql(test.e);
+      });
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/cda44fe2/ambari-web/test/mixins/common/hosts/host_component_validation_mixin_test.js
----------------------------------------------------------------------
diff --git 
a/ambari-web/test/mixins/common/hosts/host_component_validation_mixin_test.js 
b/ambari-web/test/mixins/common/hosts/host_component_validation_mixin_test.js
new file mode 100644
index 0000000..7cbe232
--- /dev/null
+++ 
b/ambari-web/test/mixins/common/hosts/host_component_validation_mixin_test.js
@@ -0,0 +1,188 @@
+/**
+ * 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');
+
+describe('App.HostComponentValidationMixin', function() {
+  var mixedObject;
+  beforeEach(function() {
+    mixedObject = Em.Object.create(App.HostComponentValidationMixin, {});
+  });
+
+  describe('#formatValidateComponents', function() {
+    var cases = [
+      {
+        components: undefined,
+        hosts: [],
+        m: 'when undefined components passed should return empty array',
+        e: []
+      },
+      {
+        components: [],
+        hosts: [],
+        m: 'when empty array passed should return empty array',
+        e: []
+      },
+      {
+        components: [
+          { componentName: 'C1', hosts: ['h1', 'h2']},
+          { componentName: 'C2', hosts: ['h3']},
+        ],
+        hosts: [],
+        m: 'when components passed along with hosts location should return 
correct objects',
+        e: [
+          Em.Object.create({ componentName: 'C1', hostName: 'h1'}),
+          Em.Object.create({ componentName: 'C1', hostName: 'h2'}),
+          Em.Object.create({ componentName: 'C2', hostName: 'h3'}),
+        ]
+      }
+    ];
+
+    cases.forEach(function(test) {
+      it(test.m, function() {
+        expect(mixedObject.formatValidateComponents(test.components, 
test.hosts)).to.be.eql(test.e);
+      });
+    });
+  });
+
+  describe('#validateSelectedHostComponents', function() {
+    var cases;
+    beforeEach(function() {
+      sinon.stub(mixedObject, 'getHostComponentValidationRequest');
+      sinon.stub(mixedObject, 'getHostComponentValidationParams');
+      sinon.stub(mixedObject, 'formatValidateComponents', function(c) { return 
c;});
+    });
+
+    afterEach(function() {
+      mixedObject.getHostComponentValidationRequest.restore();
+      mixedObject.getHostComponentValidationParams.restore();
+      mixedObject.formatValidateComponents.restore();
+    });
+
+    cases = [
+      {
+        opts: null,
+        e: {
+          services: [],
+          hosts: [],
+          components: [],
+          blueprint: null
+        },
+        m: 'when no args passed defaults should be used and no error thrown'
+      },
+      {
+        opts: {
+          services: ['s1'],
+          hosts: ['h1'],
+          components: [{componentName: 'c1'}],
+          blueprint: {recommend:{}}
+        },
+        e: {
+          services: ['s1'],
+          hosts: ['h1'],
+          components: [{componentName: 'c1'}],
+          blueprint: {recommend:{}}
+        },
+        m: 'should normaly merge defaults with passed options'
+      }
+    ];
+
+    cases.forEach(function(test) {
+      it(test.m, function() {
+        mixedObject.validateSelectedHostComponents(test.opts);
+        
expect(mixedObject.getHostComponentValidationParams.args[0][0]).to.be.eql(test.e);
+      });
+    });
+  });
+
+  describe('#getHostComponentValidationRequest', function() {
+    it('default request options checking', function() {
+      mixedObject.getHostComponentValidationRequest('someData');
+      expect(App.ajax.send.args[0][0]).to.be.eql({
+        name: 'config.validations',
+        sender: mixedObject,
+        data: 'someData',
+        success: 'updateValidationsSuccessCallback',
+        error: 'updateValidationsErrorCallback'
+      });
+    });
+  });
+
+  describe('#getHostComponentValidationParams', function() {
+    var cases;
+    beforeEach(function() {
+      sinon.stub(App, 'get').withArgs('stackVersionURL').returns('/stack/url');
+    });
+    afterEach(function() {
+      App.get.restore();
+    });
+    cases = [
+      {
+        options: {
+          hosts: ['h1'],
+          services: ['s1'],
+          components: [Em.Object.create({componentName: 'c1', hostName: 
'h1'})],
+          blueprint: null
+        },
+        e: {
+          validate: 'host_groups',
+          stackVersionUrl: '/stack/url',
+          hosts: ['h1'],
+          services: ['s1'],
+          recommendations: {
+            blueprint: {
+              host_groups: [
+                {name: 'host-group-1',components: [{name: 'c1'}]}
+              ]
+            },
+            blueprint_cluster_binding: {
+              host_groups: [
+                {name: 'host-group-1', hosts: [{fqdn: 'h1'}]}
+              ]
+            }
+          }
+        },
+        m: 'when blueprint not passed it should be generated from components 
list'
+      },
+      {
+        options: {
+          hosts: ['h1'],
+          services: ['s1'],
+          components: [Em.Object.create({componentName: 'c1', hostName: 
'h1'})],
+          blueprint: { blueprint: {}}
+        },
+        e: {
+          validate: 'host_groups',
+          stackVersionUrl: '/stack/url',
+          hosts: ['h1'],
+          services: ['s1'],
+          recommendations: {
+            blueprint: {}
+          }
+        },
+        m: 'when blueprint passed it should be used instead of generated 
blueprint'
+      }
+    ];
+
+    cases.forEach(function(test) {
+      it(test.m, function() {
+        
expect(mixedObject.getHostComponentValidationParams(test.options)).to.be.eql(test.e);
+      });
+    });
+  });
+});

Reply via email to