ambari-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From srima...@apache.org
Subject git commit: AMBARI-6869. FE: Ambari installer wizard should use the /validations API to validate host-component layout
Date Fri, 15 Aug 2014 00:31:38 GMT
Repository: ambari
Updated Branches:
  refs/heads/trunk 58ae58d44 -> 2489e7711


AMBARI-6869. FE: Ambari installer wizard should use the /validations API to validate host-component layout


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

Branch: refs/heads/trunk
Commit: 2489e7711505f91ea5b3c3a663120ede2151046b
Parents: 58ae58d
Author: Srimanth Gunturi <sgunturi@hortonworks.com>
Authored: Thu Aug 14 17:29:02 2014 -0700
Committer: Srimanth Gunturi <sgunturi@hortonworks.com>
Committed: Thu Aug 14 17:29:02 2014 -0700

----------------------------------------------------------------------
 .../assets/data/stacks/HDP-2.1/validations.json |  70 +++++
 ambari-web/app/assets/test/tests.js             |   1 +
 .../app/controllers/wizard/step5_controller.js  | 228 +++++++++++++--
 .../app/controllers/wizard/step6_controller.js  | 267 +++++++++++++++++-
 ambari-web/app/routes/add_host_routes.js        |   4 +-
 ambari-web/app/routes/add_service_routes.js     |   4 +-
 ambari-web/app/routes/installer.js              |   2 +-
 ambari-web/app/styles/application.less          |  43 ++-
 ambari-web/app/styles/apps.less                 |   1 +
 ambari-web/app/templates/wizard/step5.hbs       |  11 +-
 ambari-web/app/templates/wizard/step6.hbs       |  22 +-
 ambari-web/app/utils/ajax/ajax.js               |  40 ++-
 ambari-web/app/utils/blueprint.js               | 189 +++++++++++++
 ambari-web/app/utils/validator.js               |  11 +
 ambari-web/app/views/wizard/step6_view.js       |  12 +-
 .../test/controllers/wizard/step5_test.js       |   4 +-
 ambari-web/test/utils/blueprint_test.js         | 279 +++++++++++++++++++
 17 files changed, 1120 insertions(+), 68 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/assets/data/stacks/HDP-2.1/validations.json
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/data/stacks/HDP-2.1/validations.json b/ambari-web/app/assets/data/stacks/HDP-2.1/validations.json
new file mode 100644
index 0000000..77ae656
--- /dev/null
+++ b/ambari-web/app/assets/data/stacks/HDP-2.1/validations.json
@@ -0,0 +1,70 @@
+{ "resources": [
+  {
+    "items": [
+      {
+        "type": "configuration",
+        "level": "ERROR",
+        "message": "Value should be integer",
+        "config-type": "mapred-site",
+        "config-name": "mapreduce.map.memory.mb"
+      },
+      {
+        "type": "configuration",
+        "level": "WARN",
+        "message": "Maximum memory exceeds map memory size",
+        "config-type": "mapred-site",
+        "config-name": "mapreduce.map.java.opt"
+      },
+      {
+        "type": "configuration",
+        "level": "ERROR",
+        "message": "yarn.new-config should be defined for HDP-2.1 Yarn service",
+        "config-type": "yarn-site",
+        "config-name": "yarn.new-config"
+      },
+      {
+        "type": "configuration",
+        "level": "WARN",
+        "message": "yarn.old-config has been deprecated in HDP-2.1",
+        "config-type": "yarn-site",
+        "config-name": "yarn.old-config"
+      },
+      {
+        "type": "host-component",
+        "level": "ERROR",
+        "message": "NameNode and Secondary NameNode cannot be hosted on same machine",
+        "component-name": "NAMENODE",
+        "host": "c6401.ambari.apache.org"
+      },
+      {
+        "type": "host-component",
+        "level": "ERROR",
+        "message": "NameNode and Secondary NameNode cannot be hosted on same machine",
+        "component-name": "SNAMENODE",
+        "host": "c6401.ambari.apache.org"
+      },
+      {
+        "type": "host-component",
+        "level": "WARN",
+        "message": "DataNode should not be places on c6401.ambari.apache.org DataNode should not be places on c6401.ambari.apache.org DataNode should not be places on c6401.ambari.apache.org  DataNode should not be places on c6401.ambari.apache.orgDataNode should not be places on c6401.ambari.apache.org",
+        "component-name": "DATANODE",
+        "host": "c6402.ambari.apache.org"
+      },
+      {
+        "type": "host-component",
+        "level": "WARN",
+        "message": "Another small message",
+        "component-name": "DATANODE",
+        "host": "c6402.ambari.apache.org"
+      },
+      {
+        "type": "host-component",
+        "level": "WARN",
+        "message": "It's better to not place HIVE_CLIENT on c6403.ambari.apache.org",
+        "component-name": "HIVE_CLIENT",
+        "host": "c6403.ambari.apache.org"
+      }
+    ]
+  }
+]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/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 dc19b75..9c7d111 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -113,6 +113,7 @@ var files = ['test/init_model_test',
   'test/utils/ajax/ajax_test',
   'test/utils/ajax/ajax_queue_test',
   'test/utils/batch_scheduled_requests_test',
+  'test/utils/blueprint_test',
   'test/utils/config_test',
   'test/utils/date_test',
   'test/utils/config_test',

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/controllers/wizard/step5_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/wizard/step5_controller.js b/ambari-web/app/controllers/wizard/step5_controller.js
index c44835b..b5c1b76 100644
--- a/ambari-web/app/controllers/wizard/step5_controller.js
+++ b/ambari-web/app/controllers/wizard/step5_controller.js
@@ -18,6 +18,7 @@
 
 var App = require('app');
 var numberUtils = require('utils/number_utils');
+var validationUtils = require('utils/validator');
 
 App.WizardStep5Controller = Em.Controller.extend({
 
@@ -130,6 +131,16 @@ App.WizardStep5Controller = Em.Controller.extend({
   isLoaded: false,
 
   /**
+   * Validation error messages which don't related with any master
+   */
+  generalErrorMessages: [],
+
+  /**
+   * Validation warning messages which don't related with any master
+   */
+  generalWarningMessages: [],
+
+  /**
    * List of host with assigned masters
    * Format:
    * <code>
@@ -185,15 +196,154 @@ App.WizardStep5Controller = Em.Controller.extend({
 
   /**
    * Update submit button status
-   * @metohd getIsSubmitDisabled
+   * @metohd updateIsSubmitDisabled
    */
-  getIsSubmitDisabled: function () {
-    var isSubmitDisabled = this.get('servicesMasters').someProperty('isHostNameValid', false);
-    this.set('submitDisabled', isSubmitDisabled);
-    return isSubmitDisabled;
+  updateIsSubmitDisabled: function () {
+    var self = this;
+
+    if (self.thereIsNoMasters()) {
+      return false;
+    }
+
+    if (App.supports.serverRecommendValidate) {
+      self.set('submitDisabled', true);
+
+      // reset previous recommendations
+      App.router.set('installerController.recommendations', null);
+
+      if (self.get('servicesMasters').length === 0) {
+        return;
+      }
+
+      var isSubmitDisabled = this.get('servicesMasters').someProperty('isHostNameValid', false);
+      if (!isSubmitDisabled) {
+        self.recommendAndValidate();
+      }
+    } else {
+      var isSubmitDisabled = this.get('servicesMasters').someProperty('isHostNameValid', false);
+      self.set('submitDisabled', isSubmitDisabled);
+      return isSubmitDisabled;
+    }
   }.observes('servicesMasters.@each.selectedHost', 'servicesMasters.@each.isHostNameValid'),
 
   /**
+   * 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'
+    }).
+      retry({
+        times: App.maxRetries,
+        timeout: App.timeout
+      }).
+      then(function() {
+        if (callback) {
+          callback();
+        }
+      }, function () {
+        App.showReloadPopup();
+        console.log('Load validations failed');
+      }
+    );
+  },
+
+/**
+  * Success-callback for validations request
+  * @param {object} data
+  * @method updateValidationsSuccessCallback
+  */
+  updateValidationsSuccessCallback: function (data) {
+    var self = this;
+
+    this.set('generalErrorMessages', []);
+    this.set('generalWarningMessages', []);
+    this.get('servicesMasters').setEach('warnMessage', null);
+    this.get('servicesMasters').setEach('errorMessage', null);
+    var anyErrors = false;
+
+    var validationData = validationUtils.filterNotInstalledComponents(data);
+    validationData.filterProperty('type', 'host-component').forEach(function(item) {
+      var master = self.get('servicesMasters').find(function(m) {
+        return m.component_name === item['component-name'] && m.selectedHost === item.host;
+      });
+      if (master) {
+        if (item.level === 'ERROR') {
+          anyErrors = true;
+          master.set('errorMessage', item.message);
+        } else if (item.level === 'WARN') {
+          master.set('warnMessage', item.message);
+        }
+      } else {
+        var details = " (" + item['component-name'] + " on " + item.host + ")";
+        if (item.level === 'ERROR') {
+          anyErrors = true;
+          self.get('generalErrorMessages').push(item.message + details);
+        } else if (item.level === 'WARN') {
+          self.get('generalWarningMessages').push(item.message + details);
+        }
+      }
+    });
+
+    this.set('submitDisabled', anyErrors);
+  },
+
+  /**
+   * Composes selected values of comboboxes into blueprint format
+   */
+  getCurrentBlueprint: function() {
+    var self = this;
+
+    var res = {
+      blueprint: { host_groups: [] },
+      blueprint_cluster_binding: { host_groups: [] }
+    };
+
+    var mapping = self.get('masterHostMapping');
+
+    var i = 0;
+    mapping.forEach(function(item) {
+      i += 1;
+      var group_name = 'host-group-' + i;
+
+      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 } ]
+      }
+
+      res.blueprint.host_groups.push(host_group);
+      res.blueprint_cluster_binding.host_groups.push(binding);
+    });
+
+    return res;
+  },
+
+/**
    * Clear controller data (hosts, masters etc)
    * @method clearStep
    */
@@ -232,13 +382,20 @@ App.WizardStep5Controller = Em.Controller.extend({
     self.get('addableComponents').forEach(function (componentName) {
       self.updateComponent(componentName);
     }, self);
-    if (!self.get("selectedServicesMasters").filterProperty('isInstalled', false).length) {
+    if (self.thereIsNoMasters()) {
       console.log('no master components to add');
       App.router.send('next');
     }
   },
 
   /**
+  * Returns true if there is no new master components which need assigment to host
+  */
+  thereIsNoMasters: function() {
+    return !this.get("selectedServicesMasters").filterProperty('isInstalled', false).length;
+  },
+
+  /**
    * Used to set showAddControl flag for installer wizard
    * @method updateComponent
    */
@@ -305,11 +462,13 @@ App.WizardStep5Controller = Em.Controller.extend({
   /**
    * Get recommendations info from API
    * @return {undefined}
+   * @param function(componentInstallationobjects, this) callback
+   * @param bool includeMasters
    */
-  loadComponentsRecommendationsFromServer: function(callback) {
+  loadComponentsRecommendationsFromServer: function(callback, includeMasters) {
     var self = this;
 
-    if (App.router.get('installerController.recommendations') !== undefined) {
+    if (App.router.get('installerController.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);
@@ -320,14 +479,21 @@ App.WizardStep5Controller = Em.Controller.extend({
 
       var hostNames = self.get('hosts').mapProperty('host_name');
 
+      var data = {
+        stackVersionUrl: App.get('stackVersionURL'),
+        hosts: hostNames,
+        services: services,
+        recommend: 'host_groups'
+      };
+
+      if (includeMasters) {
+        data.recommendations = self.getCurrentBlueprint();
+      }
+
       return App.ajax.send({
-        name: 'wizard.step5.recommendations',
+        name: 'wizard.loadrecommendations',
         sender: self,
-        data: {
-          stackVersionUrl: App.get('stackVersionURL'),
-          hosts: hostNames,
-          services: services
-        },
+        data: data,
         success: 'loadRecommendationsSuccessCallback'
       }).
         retry({
@@ -554,6 +720,7 @@ App.WizardStep5Controller = Em.Controller.extend({
         componentObj.set("showRemoveControl", showRemoveControl);
       }
       componentObj.set('isHostNameValid', true);
+
       result.push(componentObj);
     }, this);
     result = this.sortComponentsByServiceName(result);
@@ -842,15 +1009,40 @@ App.WizardStep5Controller = Em.Controller.extend({
     return true;
   },
 
+  recommendAndValidate: function(callback) {
+    var self = this;
+
+    // load recommendations with partial request
+    self.loadComponentsRecommendationsFromServer(function() {
+      // For validation use latest received recommendations because ir contains current master layout and recommended slave/client layout
+      self.validate(App.router.get('installerController.recommendations'), function() {
+        if (callback) {
+          callback();
+        }
+      });
+    }, true);
+  },
+
   /**
    * Submit button click handler
    * @metohd submit
    */
   submit: function () {
-    this.getIsSubmitDisabled();
-    if (!this.get('submitDisabled')) {
-      App.router.send('next');
+    var self = this;
+
+    var goNextStepIfValid = function() {
+      if (!self.get('submitDisabled')) {
+        App.router.send('next');
+      }
+    };
+
+    if (App.supports.serverRecommendValidate ) {
+      self.recommendAndValidate(function() {
+        goNextStepIfValid();
+      });
+    } else {
+      self.updateIsSubmitDisabled();
+      goNextStepIfValid();
     }
   }
-
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/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 882bf95..ce4e496 100644
--- a/ambari-web/app/controllers/wizard/step6_controller.js
+++ b/ambari-web/app/controllers/wizard/step6_controller.js
@@ -19,6 +19,8 @@
 var App = require('app');
 var db = require('utils/db');
 var stringUtils = require('utils/string_utils');
+var blueprintUtils = require('utils/blueprint');
+var validationUtils = require('utils/validator');
 
 /**
  * By Step 6, we have the following information stored in App.db and set on this
@@ -58,6 +60,12 @@ App.WizardStep6Controller = Em.Controller.extend({
   isLoaded: false,
 
   /**
+   * Define state for submit button
+   * @type {bool}
+   */
+  submitDisabled: true,
+
+  /**
    * Check if <code>addHostWizard</code> used
    * @type {bool}
    */
@@ -86,6 +94,16 @@ App.WizardStep6Controller = Em.Controller.extend({
   }.property('content.services').cacheable(),
 
   /**
+   * Validation error messages which don't related with any master
+   */
+  generalErrorMessages: [],
+
+  /**
+   * Validation warning messages which don't related with any master
+   */
+  generalWarningMessages: [],
+
+  /**
    * Verify condition that at least one checkbox of each component was checked
    * @method clearError
    */
@@ -149,6 +167,7 @@ App.WizardStep6Controller = Em.Controller.extend({
     var name = Em.get(event, 'context.name');
     if (name) {
       this.setAllNodes(name, true);
+      this.callValidation();
     }
   },
 
@@ -161,6 +180,7 @@ App.WizardStep6Controller = Em.Controller.extend({
     var name = Em.get(event, 'context.name');
     if (name) {
       this.setAllNodes(name, false);
+      this.callValidation();
     }
   },
 
@@ -251,6 +271,8 @@ App.WizardStep6Controller = Em.Controller.extend({
     this.render();
     if (this.get('content.skipSlavesStep')) {
       App.router.send('next');
+    } else {
+      this.callValidation();
     }
   },
 
@@ -365,7 +387,6 @@ App.WizardStep6Controller = Em.Controller.extend({
 
         var clientComponents = App.get('components.clients');
 
-
         hostsObj.forEach(function (host) {
           var checkboxes = host.get('checkboxes');
           checkboxes.forEach(function (checkbox) {
@@ -448,14 +469,249 @@ App.WizardStep6Controller = Em.Controller.extend({
     return this.get('content.masterComponentHosts').filterProperty('hostName', hostName).mapProperty('component');
   },
 
+  callValidation: function(successCallback) {
+    var self = this;
+    if (App.supports.serverRecommendValidate) {
+      self.callServerSideValidation(successCallback);
+    } else {
+      var res = self.callClientSideValidation();
+      self.set('submitDisabled', !res);
+      if (res && successCallback) {
+        successCallback();
+      }
+    }
+  },
 
   /**
-   * Validate form. Return do we have errors or not
-   * @return {bool}
-   * @method validate
+   * Update submit button status
+   * @metohd callServerSideValidation
    */
-  validate: function () {
+  callServerSideValidation: function (successCallback) {
+    var self = this;
+    self.set('submitDisabled', 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 = self.get('hosts').mapProperty('hostName');
+    var slaveBlueprint = self.getCurrentBlueprint();
+    var masterBlueprint = null;
+    var invisibleSlaves = App.StackServiceComponent.find().filterProperty("isSlave").filterProperty("isShownOnInstallerSlaveClientPage", false).mapProperty("componentName");
 
+    if (this.get('isInstallerWizard') || this.get('isAddServiceWizard')) {
+      masterBlueprint = App.router.get('wizardStep5Controller').getCurrentBlueprint();
+
+      var invisibleMasters = [];
+      if (this.get('isInstallerWizard')) {
+        invisibleMasters = App.StackServiceComponent.find().filterProperty("isMaster").filterProperty("isShownOnInstallerAssignMasterPage", false).mapProperty("componentName");
+      } else if (this.get('isAddServiceWizard')) {
+        invisibleMasters = App.StackServiceComponent.find().filterProperty("isMaster").filterProperty("isShownOnAddServiceAssignMasterPage", false).mapProperty("componentName");
+      }
+
+      var selectedClientComponents = self.get('content.clients').mapProperty('component_name');
+      var alreadyInstalledClients = App.get('components.clients').reject(function(c) {
+        return selectedClientComponents.contains(c);
+      });
+
+      var invisibleComponents = invisibleMasters.concat(invisibleSlaves).concat(alreadyInstalledClients);
+
+      var invisibleBlueprint = blueprintUtils.filterByComponents(App.router.get('installerController.recommendations'), invisibleComponents);
+      masterBlueprint = blueprintUtils.mergeBlueprints(masterBlueprint, invisibleBlueprint);
+    } else if (this.get('isAddHostWizard')) {
+      masterBlueprint = self.getMasterSlaveBlueprintForAddHostWizard();
+      hostNames = hostNames.concat(App.Host.find().mapProperty("hostName")).uniq();
+      slaveBlueprint = blueprintUtils.addComponentsToBlueprint(slaveBlueprint, invisibleSlaves);
+    }
+
+    App.ajax.send({
+      name: 'config.validations',
+      sender: self,
+      data: {
+        stackVersionUrl: App.get('stackVersionURL'),
+        hosts: hostNames,
+        services: services,
+        validate: 'host_groups',
+        recommendations: blueprintUtils.mergeBlueprints(masterBlueprint, slaveBlueprint)
+      },
+      success: 'updateValidationsSuccessCallback'
+    }).
+      retry({
+        times: App.maxRetries,
+        timeout: App.timeout
+      }).
+      then(function() {
+        if (!self.get('submitDisabled') && successCallback) {
+          successCallback();
+        }
+      }, function () {
+        App.showReloadPopup();
+        console.log('Load validations failed');
+      }
+    );
+  },
+
+  /**
+   * Success-callback for validations request
+   * @param {object} data
+   * @method updateValidationsSuccessCallback
+   */
+  updateValidationsSuccessCallback: function (data) {
+    var self = this;
+    //data = JSON.parse(data); // temporary fix
+
+    var clientComponents = App.get('components.clients');
+
+    this.set('generalErrorMessages', []);
+    this.set('generalWarningMessages', []);
+    this.get('hosts').setEach('warnMessages', []);
+    this.get('hosts').setEach('errorMessages', []);
+    this.get('hosts').setEach('anyMessage', false);
+    this.get('hosts').forEach(function(host) {
+      host.checkboxes.setEach('hasWarnMessage', false);
+      host.checkboxes.setEach('hasErrorMessage', false);
+    });
+    var anyErrors = false;
+    var anyGeneralClientErrors = false; // any error/warning for any client component (under "CLIENT" alias)
+
+    var validationData = validationUtils.filterNotInstalledComponents(data);
+    validationData.filterProperty('type', 'host-component').forEach(function(item) {
+      var checkboxWithIssue = null;
+      var isGeneralClientValidationItem = clientComponents.contains(item['component-name']); // it is an error/warning for any client component (under "CLIENT" alias)
+      var host = self.get('hosts').find(function(h) {
+        return h.hostName === item.host && h.checkboxes.some(function(checkbox) {
+          var isClientComponent = checkbox.component === "CLIENT" && isGeneralClientValidationItem;
+          if (checkbox.component === item['component-name'] || isClientComponent) {
+            checkboxWithIssue = checkbox;
+            return true;
+          } else {
+            return false;
+          }
+        });
+      });
+      if (host) {
+        host.set('anyMessage', true);
+
+        if (item.level === 'ERROR') {
+          anyErrors = true;
+          host.get('errorMessages').push(item.message);
+          checkboxWithIssue.set('hasErrorMessage', true);
+        } else if (item.level === 'WARN') {
+          host.get('warnMessages').push(item.message);
+          checkboxWithIssue.set('hasWarnMessage', true);
+        }
+      } else {
+        var component;
+        if (isGeneralClientValidationItem) {
+          if (!anyGeneralClientErrors) {
+            anyGeneralClientErrors = true;
+            component = "Client";
+          }
+        } else {
+          component = item['component-name'];
+        }
+
+        if (component || !item['component-name']) {
+          var details = "";
+          if (component) {
+            details += " for " + component + " component";
+          }
+          if (item.host) {
+            details += " " + item.host;
+          }
+
+          if (item.level === 'ERROR') {
+            anyErrors = true;
+            self.get('generalErrorMessages').push(item.message + details);
+          } else if (item.level === 'WARN') {
+            self.get('generalWarningMessages').push(item.message + details);
+          }
+        }
+      }
+    });
+
+    this.set('submitDisabled', anyErrors);
+  },
+
+  /**
+   * 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');
+
+    var i = 0;
+    mapping.forEach(function(item) {
+      i += 1;
+      var group_name = 'host-group-' + i;
+
+      var host_group = {
+        name: group_name,
+        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 };
+          }
+        })
+      };
+
+      host_group.components = [].concat.apply([], host_group.components);
+
+      var binding = {
+        name: group_name,
+        hosts: [ { fqdn: item.hostName } ]
+      }
+
+      res.blueprint.host_groups.push(host_group);
+      res.blueprint_cluster_binding.host_groups.push(binding);
+    });
+
+    return res;
+  },
+
+  getMasterSlaveBlueprintForAddHostWizard: function() {
+    var components = App.HostComponent.find();
+    var hosts = components.mapProperty("hostName").uniq();
+
+    var res = {
+      blueprint: { host_groups: [] },
+      blueprint_cluster_binding: { host_groups: [] }
+    };
+
+    var i = 0;
+    hosts.forEach(function(host) {
+      i += 1;
+      var group_name = 'host-group-' + i;
+
+      res.blueprint.host_groups.push({
+        name: group_name,
+        components: components.filterProperty("hostName", host).mapProperty("componentName").map(function(c) { return { name: c }; })
+      });
+
+      res.blueprint_cluster_binding.host_groups.push({
+        name: group_name,
+        hosts: [ { fqdn: host } ]
+      });
+    });
+    return res;
+  },
+
+  /**
+   * callClientSideValidation form. Return do we have errors or not
+   * @return {bool}
+   * @method callClientSideValidation
+   */
+  callClientSideValidation: function () {
     if (this.get('isAddHostWizard')) {
       return this.validateEachHost(Em.I18n.t('installer.step6.error.mustSelectOneForHost'));
     }
@@ -546,5 +802,4 @@ App.WizardStep6Controller = Em.Controller.extend({
 
     return !isError;
   }
-
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/routes/add_host_routes.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/routes/add_host_routes.js b/ambari-web/app/routes/add_host_routes.js
index bec3867..10e30dd 100644
--- a/ambari-web/app/routes/add_host_routes.js
+++ b/ambari-web/app/routes/add_host_routes.js
@@ -192,14 +192,14 @@ module.exports = App.WizardRoute.extend({
       var addHostController = router.get('addHostController');
       var wizardStep6Controller = router.get('wizardStep6Controller');
 
-      if (wizardStep6Controller.validate()) {
+      wizardStep6Controller.callValidation(function() {
         addHostController.saveSlaveComponentHosts(wizardStep6Controller);
         if(App.supports.hostOverrides){
           router.transitionTo('step4');
         }else{
           router.transitionTo('step5');
         }
-      }
+      });
     }
   }),
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/routes/add_service_routes.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/routes/add_service_routes.js b/ambari-web/app/routes/add_service_routes.js
index 4900af1..06408b9 100644
--- a/ambari-web/app/routes/add_service_routes.js
+++ b/ambari-web/app/routes/add_service_routes.js
@@ -168,13 +168,13 @@ module.exports = App.WizardRoute.extend({
       var addServiceController = router.get('addServiceController');
       var wizardStep6Controller = router.get('wizardStep6Controller');
 
-      if (wizardStep6Controller.validate()) {
+      wizardStep6Controller.callValidation(function() {
         addServiceController.saveSlaveComponentHosts(wizardStep6Controller);
         addServiceController.get('content').set('serviceConfigProperties', null);
         addServiceController.setDBProperty('serviceConfigProperties', null);
         addServiceController.setDBProperty('groupsToDelete', []);
         router.transitionTo('step4');
-      }
+      });
     }
   }),
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/routes/installer.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/routes/installer.js b/ambari-web/app/routes/installer.js
index 846b554..7abfe96 100644
--- a/ambari-web/app/routes/installer.js
+++ b/ambari-web/app/routes/installer.js
@@ -296,7 +296,7 @@ module.exports = Em.Route.extend({
       var wizardStep6Controller = router.get('wizardStep6Controller');
       var wizardStep7Controller = router.get('wizardStep7Controller');
 
-      if (wizardStep6Controller.validate()) {
+      if (!wizardStep6Controller.get('submitDisabled')) {
         controller.saveSlaveComponentHosts(wizardStep6Controller);
         controller.get('content').set('serviceConfigProperties', null);
         controller.setDBProperty('serviceConfigProperties', null);

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/styles/application.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/application.less b/ambari-web/app/styles/application.less
index 5479c1d..b3ed5cf 100644
--- a/ambari-web/app/styles/application.less
+++ b/ambari-web/app/styles/application.less
@@ -1184,30 +1184,18 @@ h1 {
   .common-config-category {
     .action {
       cursor: pointer;
-    }
-    .btn-final .icon-lock {
-      color: grey;
-    }
-    .btn-final.active .icon-lock {
-      color: blue;
-    }
-    .btn-final.active[disabled] { //copied from Bootstrap .btn.active
-      background-color: #e6e6e6;
-      background-color: #d9d9d9 \9;
-      background-image: none;
-      outline: 0;
-      -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
-      -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
-      box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
-    }
-    .icon-plus-sign {
-      color: #5AB400;
-    }
-    .icon-minus-sign {
-      color: #FF4B4B;
-    }
-    .icon-undo {
-      color: rgb(243, 178, 11);
+      .icon-plus-sign {
+        color: #5AB400;
+        margin-right: 2px;
+      }
+      .icon-minus-sign {
+        color: #FF4B4B;
+        margin-right: 2px;
+      }
+      .icon-undo {
+        color: rgb(243, 178, 11);
+        margin-right: 2px;
+      }
     }
   }
   .capacity-scheduler {
@@ -4047,6 +4035,9 @@ table.graphs {
 .assign-masters {
   .select-hosts {
     white-space: nowrap;
+    .help-block {
+      white-space: normal;
+    }
   }
 
   label.host-name {
@@ -6792,3 +6783,7 @@ i.icon-asterisks {
     width: 95%;
   }
 }
+
+.table td.no-borders { border-top: none; }
+.table td.error { background-color: #f2dede; }
+.table td.warning { background-color: #fcf8e3; }

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/styles/apps.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/apps.less b/ambari-web/app/styles/apps.less
index ad1055b..8426380 100644
--- a/ambari-web/app/styles/apps.less
+++ b/ambari-web/app/styles/apps.less
@@ -260,6 +260,7 @@
   .table-striped tbody .even th {
     background-color: #fff;
   }
+
   .sorting_asc { background: url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAZAAA/+4ADkFkb2JlAGTAAAAAAf/bAIQAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAwMDAwMDAwMDAwEBAQEBAQECAQECAgIBAgIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMD/8AAEQgAEwATAwERAAIRAQMRAf/EAHgAAAMBAQAAAAAAAAAAAAAAAAAFCAYKAQACAQUAAAAAAAAAAAAAAAAABQMCBAYHCBAAAQUAAQMEAwAAAAAAAAAAAwECBAUGABESByExIghBMxQRAAIBAwMDAwUAAAAAAAAAAAECAwAEBRESBiExUUHhB2GBIhMU/9oADAMBAAIRAxEAPwDvA8k+Qc54sxGj32qlNi0ucrjTj/JqGlmROyJXQ2u/bOsZTmBExPd70/HXmQcW41lOX5+145h0L391KEHhR3Z28Ii6sx9AKgubiO1gaeU6Io19h9TUg/S/7eP+wia3NbBIFbuqiyn3VTCjIMArHHTJarEDGGiNU8vOKVsc7/VxBuGR3yV683X86/Cq/GpssrhP2S8emiSKRm1JS5VfyLH0WfQug7KwZR0CilWHy39++ObQTgkgeV9ux+xq9uc6U8pLfZzP6mClZpKWrvq1DilJAt4Mewh/0hRyBOsaUMoVKLvXtVU6t6+nL/HZTJYi4/rxU81tdbSu+N2Rtp7jcpB0OnUa9aoeOOVdsgDL4I1pFS+NPHmcsQ2+fw+UpLWOwwwWNVQ1kCaIcgaiONkmLGEZrDDXtcnXo5PfjC+5VybKWrWWSyF5cWbEEpJNI6kqdQSrMRqD1B9KjS2t423xoqt5AArb8QVPRwoo4
 UUcKK//2Q==) no-repeat right 50%; }
   .sorting_desc { background: url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAZAAA/+4ADkFkb2JlAGTAAAAAAf/bAIQAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAwMDAwMDAwMDAwEBAQEBAQECAQECAgIBAgIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMD/8AAEQgAEwATAwERAAIRAQMRAf/EAIEAAAIDAQAAAAAAAAAAAAAAAAAGBwgJCgEBAAIDAQAAAAAAAAAAAAAAAAMFBAYHCBAAAAUDAwMFAAAAAAAAAAAAAQIDBAUABgcSNTYRFQgTZFUWZhEAAAQEAggGAwAAAAAAAAAAAAECAxEhBAYSMjFBYRMzFDQFUZFSYmMHJFRk/9oADAMBAAIRAxEAPwDv4oAKACgCKc1tMmusb3Eph6cSgsgx7fucEZxGRks2llGIGVWgVm8q1dt0+6ogKaapSgdNbQPXTqAdwsN602bopk3vTnUW24rduwccbU2S5E8Sm1JM92czSZwNOKUYDFrCqTp1corDUFMpEcYap+Ipb4P5O8n81y9xXXlG50yY+thR3AEivqFvRDmduvSUrhuLtrFNXqCFvJm1LAQ5RMuchB6gBy13f7+tP6lsOipuz2jSGdy1ZJeNzmXnEtU+pWFTikmbxyTEjgglKKZpMU3ZanudYtTtSr8dMoYSKKvKMte0aUV5YGxgoASbD2iQ4Tyi6uB7Rvz/AHD9R8r7/wBWr64uta6/pKfq+JwUZP5/1/hwCFjIeTMrLo0np93q2xDtVCJh/9k=) no-repeat right 50%; }
   .sorting { background: url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAZAAA/+4ADkFkb2JlAGTAAAAAAf/bAIQAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAwMDAwMDAwMDAwEBAQEBAQECAQECAgIBAgIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMD/8AAEQgAEwATAwERAAIRAQMRAf/EAGgAAAIDAQAAAAAAAAAAAAAAAAUHAAYICgEBAQAAAAAAAAAAAAAAAAAAAAEQAAEEAQIFAgcAAAAAAAAAAAECAwQFABEGIRI0NQcTFDFBMmNUZRYRAQEBAQAAAAAAAAAAAAAAAAABEUH/2gAMAwEAAhEDEQA/AO93cd/XbXpLC9tHQ1Dr46nljUBby/gzGZB+p+Q6QhA+ZOApfDnllW/ha1tv6Ee7iyH5kRlvlbTIqHndWkNJ0HO7XFQbWeJUkpUeOpySrZh65UUnyFUW1ztaexRmIbaPyzoLE6vg2UWW9GC1e0XHnsSGEqfQohCwApK9OIGuAjfBP9VuG0m39vGqINVUe4r2xF21TVsuXZOI9N9lMmLBYkttQ21auBKhqtSUngCMkW5xqjKiYASh6SR2Tulr2HpOvf6j9p+V9/mwDeB//9k=) no-repeat right 50%; }

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/templates/wizard/step5.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/wizard/step5.hbs b/ambari-web/app/templates/wizard/step5.hbs
index 137e181..c30d89b 100644
--- a/ambari-web/app/templates/wizard/step5.hbs
+++ b/ambari-web/app/templates/wizard/step5.hbs
@@ -23,6 +23,12 @@
     {{{view.coHostedComponentText}}}
   {{/if}}
 </div>
+{{#each msg in controller.generalErrorMessages}}
+  <div class="alert alert-error">{{msg}}</div>
+{{/each}}
+{{#each msg in controller.generalWarningMessages}}
+    <div class="alert alert-warning">{{msg}}</div>
+{{/each}}
 {{#if controller.isLoaded}}
   <div class="assign-masters row-fluid">
     <div class="select-hosts span7">
@@ -56,7 +62,7 @@
                         {{selectedHost}}<i class="icon-asterisks">&#10037;</i>
                       </div>
                     {{else}}
-                      <div class="control-group">
+                      <div {{bindAttr class="errorMessage:error: warnMessage:warning: :control-group"}}>
                         {{#if view.shouldUseInputs}}
                           {{view App.InputHostView
                           componentBinding="this"
@@ -74,6 +80,9 @@
                         {{#if showRemoveControl}}
                           {{view App.RemoveControlView componentNameBinding="component_name" serviceComponentIdBinding="serviceComponentId"}}
                         {{/if}}
+
+                        <span class="help-block">{{warnMessage}}</span>
+                        <span class="help-block">{{errorMessage}}</span>
                       </div>
                     {{/if}}
                   </div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/templates/wizard/step6.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/wizard/step6.hbs b/ambari-web/app/templates/wizard/step6.hbs
index c9d600c..77cca2a 100644
--- a/ambari-web/app/templates/wizard/step6.hbs
+++ b/ambari-web/app/templates/wizard/step6.hbs
@@ -23,9 +23,15 @@
   {{#if errorMessage}}
     <div class="alert alert-error">{{errorMessage}}</div>
   {{/if}}
+  {{#each msg in controller.generalErrorMessages}}
+      <div class="alert alert-error">{{msg}}</div>
+  {{/each}}
+  {{#each msg in controller.generalWarningMessages}}
+      <div class="alert alert-warning">{{msg}}</div>
+  {{/each}}
 
   <div class="pre-scrollable">
-    <table class="table table-striped" id="component_assign_table">
+    <table class="table" id="component_assign_table">
       <thead>
       <tr>
         <th>{{t common.host}}</th>
@@ -52,7 +58,7 @@
               {{/if}}
             {{/view}}
             {{#each checkbox in host.checkboxes}}
-              <td>
+              <td {{bindAttr class="checkbox.hasErrorMessage:error checkbox.hasWarnMessage:warning"}}>
                 <label class="checkbox">
                   <input {{bindAttr checked = "checkbox.checked" disabled="checkbox.isDisabled"}} {{action "checkboxClick" checkbox target="view" }}
                           type="checkbox"/>{{checkbox.title}}
@@ -60,6 +66,16 @@
               </td>
             {{/each}}
           </tr>
+          <tr {{bindAttr class="host.anyMessage::hidden"}}>
+            <td {{bindAttr colspan="view.columnCount"}} class="no-borders">
+              {{#each errorMsg in host.errorMessages}}
+                  <div class="alert alert-error">{{errorMsg}}</div>
+              {{/each}}
+              {{#each warnMsg in host.warnMessages}}
+                <div class="alert alert-warning">{{warnMsg}}</div>
+              {{/each}}
+            </td>
+          </tr>
         {{/each}}
       {{/if}}
       </tbody>
@@ -81,6 +97,6 @@
   </div>
   <div class="btn-area">
     <a class="btn" {{action back}}>&larr; {{t common.back}}</a>
-    <a class="btn btn-success pull-right" {{action next}}>{{t common.next}} &rarr;</a>
+    <a class="btn btn-success pull-right" {{bindAttr disabled="submitDisabled"}} {{action next}}>{{t common.next}} &rarr;</a>
   </div>
 </div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/utils/ajax/ajax.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/ajax/ajax.js b/ambari-web/app/utils/ajax/ajax.js
index 64a3fcc..4ad4572 100644
--- a/ambari-web/app/utils/ajax/ajax.js
+++ b/ambari-web/app/utils/ajax/ajax.js
@@ -338,7 +338,7 @@ var urls = {
       }
     }
   },
-  
+
   'cancel.background.operation' : {
     'real' : '/clusters/{clusterName}/requests/{requestId}',
     'mock' : '',
@@ -1232,21 +1232,30 @@ var urls = {
     }
   },
 
-  'wizard.step5.recommendations': {
+
+  'wizard.loadrecommendations': {
     'real': '{stackVersionUrl}/recommendations',
     'mock': '/data/stacks/HDP-2.1/recommendations.json',
     'type': 'POST',
     'format': function (data) {
+      var q = {
+        hosts: data.hosts,
+        services: data.services,
+        recommend: data.recommend
+      };
+
+      if (data.recommendations) {
+        q.recommendations = data.recommendations;
+      }
+
       return {
-        data: JSON.stringify({
-          hosts: data.hosts,
-          services: data.services,
-          recommend: "host_groups"
-        })
+        data: JSON.stringify(q)
       }
     }
   },
 
+
+  // TODO: merge with wizard.loadrecommendations query
   'wizard.step7.loadrecommendations.configs': {
     'real': '{stackVersionUrl}/recommendations',
     'mock': '/data/stacks/HDP-2.1/recommendations_configs.json',
@@ -1263,6 +1272,23 @@ var urls = {
     }
   },
 
+  'config.validations': {
+    'real': '{stackVersionUrl}/validations',
+    'mock': '/data/stacks/HDP-2.1/validations.json',
+    'type': 'POST',
+    'format': function (data) {
+      return {
+          data: JSON.stringify({
+            hosts: data.hosts,
+            services: data.services,
+            validate: data.validate,
+            recommendations: data.recommendations
+        })
+      }
+    }
+  },
+
+
   'preinstalled.checks': {
     'real':'/requests',
     'mock':'',

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/utils/blueprint.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/blueprint.js b/ambari-web/app/utils/blueprint.js
new file mode 100644
index 0000000..b56ceca
--- /dev/null
+++ b/ambari-web/app/utils/blueprint.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.
+ */
+
+module.exports = {
+  mergeBlueprints: function(masterBlueprint, slaveBlueprint) {
+    var self = this;
+
+    // Check edge cases
+    if (!slaveBlueprint && !masterBlueprint) {
+      throw 'slaveBlueprint or masterBlueprint should not be empty';
+    } else if (slaveBlueprint && !masterBlueprint) {
+      return slaveBlueprint;
+    } else if (!slaveBlueprint && masterBlueprint) {
+      return masterBlueprint;
+    }
+
+    // Work with main case (both blueprint are presented)
+    var matches = self.matchGroups(masterBlueprint, slaveBlueprint);
+
+    var res = {
+      blueprint: { host_groups: [] },
+      blueprint_cluster_binding: { host_groups: [] }
+    };
+
+    var i = 0;
+    matches.forEach(function(match){
+      i += 1;
+      var group_name = 'host-group-' + i;
+
+      var masterComponents = self.getComponentsFromBlueprintByGroupName(masterBlueprint, match.g1);
+      var slaveComponents = self.getComponentsFromBlueprintByGroupName(slaveBlueprint, match.g2);
+
+      res.blueprint.host_groups.push({
+        name: group_name,
+        components: masterComponents.concat(slaveComponents)
+      });
+
+      res.blueprint_cluster_binding.host_groups.push({
+        name: group_name,
+        hosts: self.getHostsFromBlueprintByGroupName(match.g1 ? masterBlueprint : slaveBlueprint, match.g1 ? match.g1 : match.g2)
+      });
+    });
+    return res;
+  },
+
+  getHostsFromBlueprint: function(blueprint) {
+    return blueprint.blueprint_cluster_binding.host_groups.mapProperty("hosts").reduce(function(prev, curr){ return prev.concat(curr); }, []).mapProperty("fqdn");
+  },
+
+  getHostsFromBlueprintByGroupName: function(blueprint, groupName) {
+    if (groupName) {
+      var group = blueprint.blueprint_cluster_binding.host_groups.find(function(g) {
+        return g.name === groupName;
+      });
+
+      if (group) {
+        return group.hosts;
+      }
+    }
+    return [];
+  },
+
+  getComponentsFromBlueprintByGroupName: function(blueprint, groupName) {
+    if (groupName) {
+      var group = blueprint.blueprint.host_groups.find(function(g) {
+        return g.name === groupName;
+      });
+
+      if (group) {
+        return group.components;
+      }
+    }
+    return [];
+  },
+
+  matchGroups: function(masterBlueprint, slaveBlueprint) {
+    var self = this;
+    var res = [];
+
+    var groups1 = masterBlueprint.blueprint_cluster_binding.host_groups;
+    var groups2 = slaveBlueprint.blueprint_cluster_binding.host_groups;
+
+    var groups1_used = groups1.map(function() { return false; });
+    var groups2_used = groups2.map(function() { return false; });
+
+    self.matchGroupsWithLeft(groups1, groups2, groups1_used, groups2_used, res, false);
+    self.matchGroupsWithLeft(groups2, groups1, groups2_used, groups1_used, res, true);
+
+    return res;
+  },
+
+  matchGroupsWithLeft: function(groups1, groups2, groups1_used, groups2_used, res, inverse) {
+    for (var i = 0; i < groups1.length; i++) {
+      if (groups1_used[i]) {
+        continue;
+      }
+
+      var group1 = groups1[i];
+      groups1_used[i] = true;
+
+      var group2 = groups2.find(function(g2, index) {
+        if (group1.hosts.length != g2.hosts.length) {
+          return false;
+        }
+
+        for (var gi = 0; gi < group1.hosts.length; gi++) {
+          if (group1.hosts[gi].fqdn != g2.hosts[gi].fqdn) {
+            return false;
+          }
+        }
+
+        groups2_used[index] = true;
+        return true;
+      });
+
+      var item = {};
+
+      if (inverse) {
+        item.g2 = group1.name;
+        if (group2) {
+          item.g1 = group2.name;
+        }
+      } else {
+        item.g1 = group1.name;
+        if (group2) {
+          item.g2 = group2.name;
+        }
+      }
+      res.push(item);
+    }
+  },
+
+  /**
+   * Remove from blueprint all components expect given components
+   * @param blueprint
+   * @param [string] components
+   */
+  filterByComponents: function(blueprint, components) {
+    var res = JSON.parse(JSON.stringify(blueprint))
+    var emptyGroups = [];
+
+    for (var i = 0; i < res.blueprint.host_groups.length; i++) {
+      res.blueprint.host_groups[i].components = res.blueprint.host_groups[i].components.filter(function(c) {
+        return components.contains(c.name);
+      });
+
+      if (res.blueprint.host_groups[i].components.length == 0) {
+        emptyGroups.push(res.blueprint.host_groups[i].name);
+      }
+    }
+
+    res.blueprint.host_groups = res.blueprint.host_groups.filter(function(g) {
+      return !emptyGroups.contains(g.name);
+    });
+
+    res.blueprint_cluster_binding.host_groups = res.blueprint_cluster_binding.host_groups.filter(function(g) {
+      return !emptyGroups.contains(g.name);
+    });
+
+    return res;
+  },
+
+  addComponentsToBlueprint: function(blueprint, components) {
+    var res = JSON.parse(JSON.stringify(blueprint))
+
+    res.blueprint.host_groups.forEach(function(group) {
+      components.forEach(function(component) {
+        group.components.push({ name: component });
+      });
+    });
+
+    return res;
+  }
+};

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/utils/validator.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/validator.js b/ambari-web/app/utils/validator.js
index 3c41da2..0b8afb2 100644
--- a/ambari-web/app/utils/validator.js
+++ b/ambari-web/app/utils/validator.js
@@ -167,5 +167,16 @@ module.exports = {
     }
     if (/^[\?\|\*\!,]/.test(value)) return false;
     return /^((\.\*?)?([\w\[\]\?\-_,\|\*\!\{\}]*)?)+(\.\*?)?$/g.test(value) && (checkPair(['[',']'])) && (checkPair(['{','}']));
+  },
+
+  /**
+  * Remove validation messages for components which are already installed
+  */
+  filterNotInstalledComponents: function(validationData) {
+    var hostComponents = App.HostComponent.find();
+    return validationData.resources[0].items.filter(function(item) {
+      // true is there is no host with this component
+      return hostComponents.filterProperty("componentName", item["component-name"]).filterProperty("hostName", item.host).length === 0;
+    });
   }
 };

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/app/views/wizard/step6_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/wizard/step6_view.js b/ambari-web/app/views/wizard/step6_view.js
index 1a32bc6..4efea53 100644
--- a/ambari-web/app/views/wizard/step6_view.js
+++ b/ambari-web/app/views/wizard/step6_view.js
@@ -90,7 +90,17 @@ App.WizardStep6View = App.TableView.extend({
     var checkbox = e.context;
     checkbox.toggleProperty('checked');
     this.get('controller').checkCallback(checkbox.component);
-  }
+    this.get('controller').callValidation();
+  },
+
+  columnCount: function() {
+    var hosts = this.get('controller.hosts');
+    if  (hosts && hosts.length > 0) {
+      var checkboxes = hosts[0].get('checkboxes');
+      return checkboxes.length + 1;
+    }
+    return 1;
+  }.property('controller.hosts.@each.checkboxes')
 });
 
 App.WizardStep6HostView = Em.View.extend({

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/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 ab49097..e39c7a8 100644
--- a/ambari-web/test/controllers/wizard/step5_test.js
+++ b/ambari-web/test/controllers/wizard/step5_test.js
@@ -489,15 +489,13 @@ describe('App.WizardStep5Controller', function () {
       App.router.send.restore();
     });
     it('should go next if not isSubmitDisabled', function () {
-      c.reopen({isSubmitDisabled: false});
+      c.reopen({submitDisabled: false});
       c.submit();
       expect(App.router.send.calledWith('next')).to.equal(true);
     });
     it('shouldn\'t go next if submitDisabled true', function () {
-      sinon.stub(c, 'getIsSubmitDisabled', Em.K);
       c.reopen({submitDisabled: true});
       c.submit();
-      c.getIsSubmitDisabled.restore();
       expect(App.router.send.called).to.equal(false);
     });
   });

http://git-wip-us.apache.org/repos/asf/ambari/blob/2489e771/ambari-web/test/utils/blueprint_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/utils/blueprint_test.js b/ambari-web/test/utils/blueprint_test.js
new file mode 100644
index 0000000..e527a09
--- /dev/null
+++ b/ambari-web/test/utils/blueprint_test.js
@@ -0,0 +1,279 @@
+/**
+ * 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 blueprintUtils = require('utils/blueprint');
+
+describe('utils/blueprint', function() {
+  var masterBlueprint = {
+    blueprint: {
+      host_groups: [
+        {
+          name: "host-group-1",
+          components: [
+            { name: "ZOOKEEPER_SERVER" },
+            { name: "NAMENODE" },
+            { name: "HBASE_MASTER" }
+          ]
+        },
+        {
+          name: "host-group-2",
+          components: [
+            { name: "SECONDARY_NAMENODE" }
+          ]
+        }
+      ]
+    },
+    blueprint_cluster_binding: {
+      host_groups: [
+        {
+          name: "host-group-1",
+          hosts: [
+            { fqdn: "host1" },
+            { fqdn: "host2" }
+          ]
+        },
+        {
+          name: "host-group-2",
+          hosts: [
+            { fqdn: "host3" }
+          ]
+        }
+      ]
+    }
+  };
+
+  var slaveBlueprint = {
+    blueprint: {
+      host_groups: [
+        {
+          name: "host-group-1",
+          components: [
+            { name: "DATANODE" }
+          ]
+        },
+        {
+          name: "host-group-2",
+          components: [
+            { name: "DATANODE" },
+            { name: "HDFS_CLIENT" },
+            { name: "ZOOKEEPER_CLIENT" }
+          ]
+        }
+      ]
+    },
+    blueprint_cluster_binding: {
+      host_groups: [
+        {
+          name: "host-group-1",
+          hosts: [
+            { fqdn: "host3" }
+          ]
+        },
+        {
+          name: "host-group-2",
+          hosts: [
+            { fqdn: "host4" },
+            { fqdn: "host5" }
+          ]
+        }
+      ]
+    }
+  };
+
+  describe('#getHostsFromBlueprint', function() {
+    it('should extract all hosts from blueprint', function() {
+      expect(blueprintUtils.getHostsFromBlueprint(masterBlueprint)).to.deep.equal(["host1", "host2", "host3"]);
+    });
+  });
+
+  describe('#getHostsFromBlueprintByGroupName', function() {
+    it('should extract hosts from blueprint by given group name', function() {
+      expect(blueprintUtils.getHostsFromBlueprintByGroupName(masterBlueprint, "host-group-1")).to.deep.equal([
+        { fqdn: "host1" },
+        { fqdn: "host2" }
+      ]);
+    });
+
+    it('should return empty array if group with given name doesn\'t exist', function() {
+      expect(blueprintUtils.getHostsFromBlueprintByGroupName(masterBlueprint, "not an existing group")).to.deep.equal([]);
+    });
+  });
+
+  describe('#getComponentsFromBlueprintByGroupName', function() {
+    it('should extract all components from blueprint for given host', function() {
+      expect(blueprintUtils.getComponentsFromBlueprintByGroupName(masterBlueprint, "host-group-1")).to.deep.equal([
+        { name: "ZOOKEEPER_SERVER" },
+        { name: "NAMENODE" },
+        { name: "HBASE_MASTER" }
+      ]);
+    });
+
+    it('should return empty array if group doesn\'t exists', function() {
+      expect(blueprintUtils.getComponentsFromBlueprintByGroupName(masterBlueprint, "not an existing group")).to.deep.equal([]);
+    });
+
+    it('should return empty array if group name isn\'t valid', function() {
+      expect(blueprintUtils.getComponentsFromBlueprintByGroupName(masterBlueprint, undefined)).to.deep.equal([]);
+    });
+  });
+
+  describe('#matchGroups', function() {
+    it('should compose same host group into pairs', function() {
+      expect(blueprintUtils.matchGroups(masterBlueprint, slaveBlueprint)).to.deep.equal([
+        { g1: "host-group-1" },
+        { g1: "host-group-2", g2: "host-group-1" },
+        { g2: "host-group-2" }
+      ]);
+    });
+  });
+
+  describe('#filterByComponents', function() {
+    it('should remove all components except', function() {
+      expect(blueprintUtils.filterByComponents(masterBlueprint, ["NAMENODE"])).to.deep.equal({
+        blueprint: {
+          host_groups: [
+            {
+              name: "host-group-1",
+              components: [
+                { name: "NAMENODE" }
+              ]
+            }
+          ]
+        },
+        blueprint_cluster_binding: {
+          host_groups: [
+            {
+              name: "host-group-1",
+              hosts: [
+                { fqdn: "host1" },
+                { fqdn: "host2" }
+              ]
+            }
+          ]
+        }
+      });
+    });
+  });
+
+  describe('#addComponentsToBlueprint', function() {
+    it('should add components to blueprint', function() {
+      var components = ["FLUME_HANDLER", "HCAT"];
+      expect(blueprintUtils.addComponentsToBlueprint(masterBlueprint, components)).to.deep.equal({
+        blueprint: {
+          host_groups: [
+            {
+              name: "host-group-1",
+              components: [
+                { name: "ZOOKEEPER_SERVER" },
+                { name: "NAMENODE" },
+                { name: "HBASE_MASTER" },
+                { name: "FLUME_HANDLER" },
+                { name: "HCAT" }
+              ]
+            },
+            {
+              name: "host-group-2",
+              components: [
+                { name: "SECONDARY_NAMENODE" },
+                { name: "FLUME_HANDLER" },
+                { name: "HCAT" }
+              ]
+            }
+          ]
+        },
+        blueprint_cluster_binding: {
+          host_groups: [
+            {
+              name: "host-group-1",
+              hosts: [
+                { fqdn: "host1" },
+                { fqdn: "host2" }
+              ]
+            },
+            {
+              name: "host-group-2",
+              hosts: [
+                { fqdn: "host3" }
+              ]
+            }
+          ]
+        }
+      });
+    });
+  });
+
+  describe('#mergeBlueprints', function() {
+    it('should merge components', function() {
+      expect(blueprintUtils.mergeBlueprints(masterBlueprint, slaveBlueprint)).to.deep.equal(
+        {
+          blueprint: {
+            host_groups: [
+              {
+                name: "host-group-1",
+                components: [
+                  { name: "ZOOKEEPER_SERVER" },
+                  { name: "NAMENODE" },
+                  { name: "HBASE_MASTER" }
+                ]
+              },
+              {
+                name: "host-group-2",
+                components: [
+                  { name: "SECONDARY_NAMENODE" },
+                  { name: "DATANODE" }
+                ]
+              },
+              {
+                name: "host-group-3",
+                components: [
+                  { name: "DATANODE" },
+                  { name: "HDFS_CLIENT" },
+                  { name: "ZOOKEEPER_CLIENT" }
+                ]
+              }
+            ]
+          },
+          blueprint_cluster_binding: {
+            host_groups: [
+              {
+                name: "host-group-1",
+                hosts: [
+                  { fqdn: "host1" },
+                  { fqdn: "host2" }
+                ]
+              },
+              {
+                name: "host-group-2",
+                hosts: [
+                  { fqdn: "host3" }
+                ]
+              },
+              {
+                name: "host-group-3",
+                hosts: [
+                  { fqdn: "host4" },
+                  { fqdn: "host5" }
+                ]
+              }
+            ]
+          }
+        }
+      );
+    });
+  });
+});
\ No newline at end of file


Mime
View raw message