AMBARI-22449. Improved service/component dependency support (amagyar)

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

Branch: refs/heads/branch-feature-AMBARI-22008
Commit: 97cea11f3d378fa4f53de9d2b403bb384b0698b9
Parents: 6fedb9f
Author: Attila Magyar <amag...@hortonworks.com>
Authored: Thu Nov 16 12:27:13 2017 +0100
Committer: Attila Magyar <amag...@hortonworks.com>
Committed: Wed Nov 22 10:55:11 2017 +0100

----------------------------------------------------------------------
 .../app/controllers/wizard/step4_controller.js  | 115 ++++++++++---------
 ambari-web/app/messages.js                      |   3 +-
 ambari-web/app/models/stack_service.js          |  82 +++++++++++++
 .../test/controllers/wizard/step4_test.js       |  16 +--
 4 files changed, 155 insertions(+), 61 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/97cea11f/ambari-web/app/controllers/wizard/step4_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/wizard/step4_controller.js 
b/ambari-web/app/controllers/wizard/step4_controller.js
index d82bf34..d39164b 100644
--- a/ambari-web/app/controllers/wizard/step4_controller.js
+++ b/ambari-web/app/controllers/wizard/step4_controller.js
@@ -386,61 +386,29 @@ App.WizardStep4Controller = Em.ArrayController.extend({
    */
   serviceDependencyValidation: function(callback) {
     var selectedServices = this.filterProperty('isSelected', true);
+    var availableServices = this.get('content');
     var missingDependencies = [];
-    var missingDependenciesDisplayName = [];
     selectedServices.forEach(function(service) {
-      var requiredServices = service.get('requiredServices');
-      if (!!requiredServices && requiredServices.length) {
-        requiredServices.forEach(function(_requiredService){
-          var requiredService = this.findProperty('serviceName', 
_requiredService);
-          if (requiredService) {
-            if(requiredService.get('isSelected') === false) {
-               if(missingDependencies.indexOf(_requiredService) === -1) {
-                 missingDependencies.push(_requiredService);
-                 
missingDependenciesDisplayName.push(requiredService.get('displayNameOnSelectServicePage'));
-               }
-            }
-            else {
-               //required service is selected, remove the service error from 
errorObject array
-               var serviceName = requiredService.get('serviceName');
-               var serviceError = 
this.get('errorStack').filterProperty('id',"serviceCheck_"+serviceName);
-               if(serviceError) {
-                  this.get('errorStack').removeObject(serviceError[0]);
-               }
-            }
-          }
-        },this);
-      }
-    },this);
+      service.collectMissingDependencies(selectedServices, availableServices, 
missingDependencies);
+    });
+    this.cleanExistingServiceCheckErrors();
+    this.addServiceCheckErrors(missingDependencies, callback);
+  },
 
-    //create a copy of the errorStack, reset it
-    //and add the dependencies in the correct order
-    var errorStackCopy = this.get('errorStack');
-    this.set('errorStack', []);
+  cleanExistingServiceCheckErrors() {
+    var existingServiceCheckErrors = this.get('errorStack').filter(function 
(error) {
+      return error.id.startsWith('serviceCheck_');
+    });
+    this.get('errorStack').removeObjects(existingServiceCheckErrors);
+  },
 
-    if (missingDependencies.length > 0) {
-      for(var i = 0; i < missingDependencies.length; i++) {
-        this.addValidationError({
-          id: 'serviceCheck_' + missingDependencies[i],
-          callback: this.needToAddServicePopup,
-          callbackParams: [{serviceName: missingDependencies[i], selected: 
true}, 'serviceCheck', missingDependenciesDisplayName[i], callback]
-        });
-      }
-    }
-
-    //iterate through the errorStackCopy array and add to errorStack array, 
the error objects that have no matching entry in the errorStack 
-    //and that are not related to serviceChecks since serviceCheck errors have 
already been added when iterating through the missing dependencies list
-    //Only add Ranger, Ambari Metrics, Spark and file system service 
validation errors if they exist in the errorStackCopy array
-    var ctr = 0;
-    while(ctr < errorStackCopy.length) {
-      //no matching entry in errorStack array
-      if (!this.get('errorStack').someProperty('id', errorStackCopy[ctr].id)) {
-        //not serviceCheck error
-        if(!errorStackCopy[ctr].id.startsWith('serviceCheck_')) {
-          this.get('errorStack').push(this.createError(errorStackCopy[ctr]));
-        }
-      }
-      ctr++;
+  addServiceCheckErrors(missingDependencies, callback) {
+    for(var i = 0; i < missingDependencies.length; i++) {
+      this.addValidationError({
+        id: 'serviceCheck_' + missingDependencies[i].get('serviceName'),
+        callback: this.needToAddMissingDependency,
+        callbackParams: [missingDependencies[i], 'serviceCheck', callback]
+      });
     }
   },
 
@@ -486,7 +454,7 @@ App.WizardStep4Controller = Em.ArrayController.extend({
     return App.ModalPopup.show({
       'data-qa': 'need-add-service-confirmation-modal',
       header: Em.I18n.t('installer.step4.' + i18nSuffix + 
'.popup.header').format(serviceName),
-      body: Em.I18n.t('installer.step4.' + i18nSuffix + 
'.popup.body').format(serviceName),
+      body: Em.I18n.t('installer.step4.' + i18nSuffix + 
'.popup.body').format(serviceName, serviceName),
       onPrimary: function () {
         Em.makeArray(services).forEach(function (service) {
           self.findProperty('serviceName', 
service.serviceName).set('isSelected', service.selected);
@@ -509,6 +477,49 @@ App.WizardStep4Controller = Em.ArrayController.extend({
     });
   },
 
+  needToAddMissingDependency: function (missingDependency, i18nSuffix, 
callback, id) {
+    var self = this;
+    var displayName = missingDependency.get('displayName');
+    if (missingDependency.hasMultipleOptions()) {
+      return this.showDependencyPopup(
+        id,
+        Em.I18n.t('installer.step4.' + i18nSuffix + 
'.popup.header').format(displayName),
+        Em.I18n.t('installer.step4.' + i18nSuffix + 
'.popup.body2').format(displayName, missingDependency.displayOptions()),
+        callback
+      );
+    } else {
+      return this.showDependencyPopup(
+        id,
+        Em.I18n.t('installer.step4.' + i18nSuffix + 
'.popup.header').format(displayName),
+        Em.I18n.t('installer.step4.' + i18nSuffix + 
'.popup.body').format(displayName, missingDependency.get('serviceName')),
+        callback,
+        function () {
+          missingDependency.selectFirstCompatible();
+          self.onPrimaryPopupCallback(callback, id);
+          this.hide();
+        }
+      );
+    }
+  },
+
+  showDependencyPopup: function(id, header, body, callback, primaryAction) {
+    return App.ModalPopup.show({
+        'data-qa': 'need-add-service-confirmation-modal',
+        header: header,
+        body: body,
+        onPrimary: primaryAction || function() { this.onClose(); },
+        onSecondary: function() {
+          this.onClose();
+        },
+        onClose: function() {
+          if (callback) {
+            callback(id);
+          }
+          this._super();
+        }
+      });
+  },
+
   /**
    * Show popup with info about not selected service
    * @param {function} callback

http://git-wip-us.apache.org/repos/asf/ambari/blob/97cea11f/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index 390f803..1afb6fe 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -879,7 +879,8 @@ Em.I18n.translations = {
   'installer.step4.multipleDFS.popup.header':'Multiple File Systems Selected',
   'installer.step4.multipleDFS.popup.body':'You selected more than one file 
system. We will automatically select only {0}. Is this OK?',
   'installer.step4.serviceCheck.popup.header':'{0} Needed',
-  'installer.step4.serviceCheck.popup.body':'You did not select {0}, but it is 
needed by other services you selected. We will automatically add {0}. Is this 
OK?',
+  'installer.step4.serviceCheck.popup.body':'You did not select {0}, but it is 
needed by other services you selected. We will automatically add {1}. Is this 
OK?',
+  'installer.step4.serviceCheck.popup.body2':'You did not select {0}, but it 
is needed by other services you selected. Select a compatible service from the 
following list: {1}',
   'installer.step4.limitedFunctionality.popup.header':'Limited Functionality 
Warning',
   'installer.step4.ambariMetricsCheck.popup.body':'Ambari Metrics collects 
metrics from the cluster and makes them available to Ambari.  If you do not 
install Ambari Metrics service, metrics will not be accessible from Ambari.  
Are you sure you want to proceed without Ambari Metrics?',
   'installer.step4.ambariInfraCheck.popup.body':'Since Ambari Infra is not 
selected, you must supply your own Solr to make Atlas work. Are you sure you 
want to proceed?',

http://git-wip-us.apache.org/repos/asf/ambari/blob/97cea11f/ambari-web/app/models/stack_service.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/stack_service.js 
b/ambari-web/app/models/stack_service.js
index 241f6ab..46dfee5 100644
--- a/ambari-web/app/models/stack_service.js
+++ b/ambari-web/app/models/stack_service.js
@@ -21,6 +21,56 @@ var stringUtils = require('utils/string_utils');
 require('utils/helper');
 require('models/configs/objects/service_config_category');
 
+var Dependency = Ember.Object.extend({
+  name: Ember.computed('service', function() {
+    return this.get('service').get('serviceName');
+  }),
+
+  displayName: Ember.computed('service', function() {
+    return this.get('service').get('displayNameOnSelectServicePage');
+  }),
+
+  compatibleServices: function(services) {
+    return services.filterProperty('serviceName', this.get('name'));
+  },
+
+  isMissing: function(selectedServices) {
+    return Em.isEmpty(this.compatibleServices(selectedServices));
+  }
+});
+
+var HcfsDependency = Dependency.extend({
+  displayName: Ember.computed('service', function() {
+    return 'a Hadoop Compatible File System';
+  }),
+
+  compatibleServices: function(services) {
+    return services.filterProperty("isDFS", true);
+  }
+});
+
+Dependency.reopenClass({
+  fromService: function(service) {
+    return service.get('isDFS')
+      ? HcfsDependency.create({service: service})
+      : Dependency.create({service: service});
+  }
+});
+
+var MissingDependency = Ember.Object.extend({
+  hasMultipleOptions: function() {
+    return this.get('compatibleServices').length > 1;
+  },
+
+  selectFirstCompatible: function() {
+    this.get('compatibleServices')[0].set('isSelected', true);
+  },
+
+  displayOptions: function() {
+    return this.get('compatibleServices').mapProperty('serviceName').join(', 
');
+  }
+});
+
 /**
  * This model loads all services supported by the stack
  * The model maps to the  
http://hostname:8080/api/v1/stacks/HDP/versions/${versionNumber}/services?fields=StackServices/*,serviceComponents/*
@@ -71,6 +121,38 @@ App.StackService = DS.Model.extend({
    */
   dependentServiceNames: DS.attr('array', {defaultValue: []}),
 
+  dependencies: function(availableServices) {
+    return this.get('requiredServices')
+      .map(function (serviceName) { return 
availableServices.findProperty('serviceName', serviceName)})
+      .filter(function (each) { return !!each })
+      .map(function (service) { return Dependency.fromService(service); });
+  },
+
+  /**
+   * Add dependencies which are not already included in selectedServices to 
the given missingDependencies collection
+  */
+  collectMissingDependencies: function(selectedServices, availableServices, 
missingDependencies) {
+    this._missingDependencies(selectedServices, 
availableServices).forEach(function (dependency) {
+      this._addMissingDependency(dependency, availableServices, 
missingDependencies);
+    }.bind(this));
+  },
+
+  _missingDependencies: function(selectedServices, availableServices) {
+    return this.dependencies(availableServices).filter(function(dependency) {
+      return dependency.isMissing(selectedServices);
+    });
+  },
+
+  _addMissingDependency: function(dependency, availableServices, 
missingDependencies) {
+    if(!missingDependencies.some(function(each) { return 
each.get('serviceName') == dependency.get('name'); })) {
+      missingDependencies.push(MissingDependency.create({
+         serviceName: dependency.get('name'),
+         displayName: dependency.get('displayName'),
+         compatibleServices: dependency.compatibleServices(availableServices)
+      }));
+    }
+  },
+
   // Is the service a distributed filesystem
   isDFS: function () {
     return this.get('serviceType') === 'HCFS' || ['HDFS', 
'GLUSTERFS'].contains(this.get('serviceName'));

http://git-wip-us.apache.org/repos/asf/ambari/blob/97cea11f/ambari-web/test/controllers/wizard/step4_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/wizard/step4_test.js 
b/ambari-web/test/controllers/wizard/step4_test.js
index 0fad0c1..1cd4863 100644
--- a/ambari-web/test/controllers/wizard/step4_test.js
+++ b/ambari-web/test/controllers/wizard/step4_test.js
@@ -32,7 +32,7 @@ describe('App.WizardStep4Controller', function () {
   beforeEach(function() {
     controller = App.WizardStep4Controller.create();
     services.forEach(function(serviceName) {
-      controller.pushObject(Ember.Object.create({
+      controller.pushObject(App.StackService.createRecord({
         'serviceName':serviceName, 'isSelected': true, 
'isHiddenOnSelectServicePage': false, 'isInstalled': false, 'isDisabled': 
'HDFS' === serviceName, isDFS: 'HDFS' === serviceName
       }));
     });
@@ -43,7 +43,7 @@ describe('App.WizardStep4Controller', function () {
     modelSetup.setupStackServiceComponent();
     if (selectedServiceNames.contains('GLUSTERFS')) 
allServices.push('GLUSTERFS');
     allServices = allServices.map(function(serviceName) {
-      return [Ember.Object.create({
+      return [App.StackService.createRecord({
         'serviceName': serviceName,
         'isSelected': false,
         'canBeSelected': true,
@@ -51,7 +51,7 @@ describe('App.WizardStep4Controller', function () {
         isPrimaryDFS: serviceName === 'HDFS',
         isDFS: ['HDFS','GLUSTERFS'].contains(serviceName),
         isMonitoringService: ['GANGLIA'].contains(serviceName),
-        requiredServices: 
App.StackService.find(serviceName).get('requiredServices'),
+        requiredServices: 
App.StackService.find(serviceName).get('requiredServices') || [],
         displayNameOnSelectServicePage: App.format.role(serviceName, true),
         coSelectedServices: function() {
           return App.StackService.coSelected[this.get('serviceName')] || [];
@@ -167,7 +167,7 @@ describe('App.WizardStep4Controller', function () {
         beforeEach(function () {
           controller.clear();
           Object.keys(testCase.condition).forEach(function (id) {
-            controller.pushObject(Ember.Object.create({
+            controller.pushObject(App.StackService.createRecord({
               serviceName: id,
               isSelected: testCase.condition[id],
               canBeSelected: true,
@@ -1006,7 +1006,7 @@ describe('App.WizardStep4Controller', function () {
     });
 
     it('serviceValidation should not be called when service not selected', 
function() {
-      controller.pushObject(Em.Object.create({
+      controller.pushObject(App.StackService.createRecord({
         serviceName: 'S1',
         isSelected: false
       }));
@@ -1016,7 +1016,7 @@ describe('App.WizardStep4Controller', function () {
 
     it('serviceValidation should not be called when dependent service does not 
exist', function() {
       controller.pushObjects([
-        Em.Object.create({
+        App.StackService.createRecord({
           serviceName: 'S1',
           isSelected: true
         })
@@ -1027,7 +1027,7 @@ describe('App.WizardStep4Controller', function () {
 
     it('serviceValidation should not be called when dependent service is 
selected', function() {
       controller.pushObjects([
-        Em.Object.create({
+        App.StackService.createRecord({
           serviceName: 'S1',
           isSelected: true
         }),
@@ -1042,7 +1042,7 @@ describe('App.WizardStep4Controller', function () {
 
     it('serviceValidation should be called when dependent service is not 
selected', function() {
       controller.pushObjects([
-        Em.Object.create({
+        App.StackService.createRecord({
           serviceName: 'S1',
           isSelected: true
         }),

Reply via email to