Bladeren bron

AMBARI-6009 Usability: provide database connectivity checks in Customize Services page. (Buzhor Denys via ababiichuk)

aBabiichuk 11 jaren geleden
bovenliggende
commit
5831b8dd71

+ 1 - 0
ambari-web/app/assets/test/tests.js

@@ -161,6 +161,7 @@ require('test/views/main/charts/heatmap/heatmap_rack_test');
 require('test/views/main/service/info/config_test');
 require('test/views/main/mirroring/edit_dataset_view_test');
 require('test/views/common/configs/services_config_test');
+require('test/views/wizard/controls_view_test');
 require('test/views/wizard/step3/hostLogPopupBody_view_test');
 require('test/views/wizard/step3/hostWarningPopupBody_view_test');
 require('test/views/wizard/step3/hostWarningPopupFooter_view_test');

+ 2 - 1
ambari-web/app/config.js

@@ -77,7 +77,8 @@ App.supports = {
   jobs: true,
   ubuntu: false,
   views: false,
-  flume: false
+  flume: false,
+  databaseConnection: true
 };
 
 if (App.enableExperimental) {

+ 1 - 2
ambari-web/app/controllers/global/cluster_controller.js

@@ -437,13 +437,12 @@ App.ClusterController = Em.Controller.extend({
   },
 
   loadAmbariProperties: function () {
-    App.ajax.send({
+    return App.ajax.send({
       name: 'ambari.service',
       sender: this,
       success: 'loadAmbariPropertiesSuccess',
       error: 'loadAmbariPropertiesError'
     });
-    return this.get('ambariProperties');
   },
 
   loadAmbariPropertiesSuccess: function (data) {

+ 70 - 1
ambari-web/app/controllers/wizard/step7_controller.js

@@ -986,6 +986,7 @@ App.WizardStep7Controller = Em.Controller.extend({
     var hiveService = this.get('content.services').findProperty('serviceName', 'HIVE');
     if (!hiveService || !hiveService.get('isSelected') || hiveService.get('isInstalled')) {
       this.moveNext();
+      return;
     }
     var hiveDBType = this.get('stepConfigs').findProperty('serviceName', 'HIVE').configs.findProperty('name', 'hive_database').value;
     if (hiveDBType == 'New MySQL Database') {
@@ -1025,6 +1026,71 @@ App.WizardStep7Controller = Em.Controller.extend({
     }
   },
 
+  checkDatabaseConnectionTest: function() {
+    var deferred = $.Deferred();
+    if (!App.supports.databaseConnection) {
+      deferred.resolve();
+      return deferred;
+    }
+    var configMap = [
+      {
+        serviceName: 'OOZIE',
+        ignored: Em.I18n.t('installer.step7.oozie.database.new')
+      },
+      {
+        serviceName: 'HIVE',
+        ignored: Em.I18n.t('installer.step7.hive.database.new')
+      }
+    ];
+    configMap.forEach(function(config) {
+      var isConnectionNotTested = false;
+      var service = this.get('content.services').findProperty('serviceName', config.serviceName);
+      if (service && service.get('isSelected') && !service.get('isInstalled')) {
+        var serviceConfigs = this.get('stepConfigs').findProperty('serviceName', config.serviceName).configs;
+        var serviceDatabase = serviceConfigs.findProperty('name', config.serviceName.toLowerCase() + '_database').get('value');
+        if (serviceDatabase !== config.ignored) {
+          var filledProperties = App.db.get('tmp', config.serviceName + '_connection');
+          if (!filledProperties || App.isEmptyObject(filledProperties)) {
+            isConnectionNotTested = true;
+          } else {
+            for (var key in filledProperties) {
+              if (serviceConfigs.findProperty('name', key).get('value') !== filledProperties[key])
+                isConnectionNotTested = true;
+            }
+          }
+        }
+      }
+      config.isCheckIgnored = isConnectionNotTested;
+    }, this);
+    var ignoredServices = configMap.filterProperty('isCheckIgnored', true);
+    if (ignoredServices.length) {
+      var displayedServiceNames = ignoredServices.mapProperty('serviceName').map(function(serviceName) {
+        return this.get('content.services').findProperty('serviceName', serviceName).get('displayName');
+      }, this);
+      this.showDatabaseConnectionWarningPopup(displayedServiceNames, deferred);
+    }
+    else {
+      deferred.resolve();
+    }
+    return deferred;
+  },
+
+  showDatabaseConnectionWarningPopup: function(serviceNames, deferred) {
+    return App.ModalPopup.show({
+      header: Em.I18n.t('installer.step7.popup.database.connection.header'),
+      body: Em.I18n.t('installer.step7.popup.database.connection.body').format(serviceNames.join(', ')),
+      secondary: Em.I18n.t('common.cancel'),
+      primary: Em.I18n.t('common.proceedAnyway'),
+      onPrimary: function() {
+        deferred.resolve();
+        this._super();
+      },
+      onSecondary: function() {
+        deferred.reject();
+        this._super();
+      }
+    })
+  },
   /**
    * Proceed to the next step
    **/
@@ -1037,8 +1103,11 @@ App.WizardStep7Controller = Em.Controller.extend({
    * @method submit
    */
   submit: function () {
+    var _this = this;
     if (!this.get('isSubmitDisabled')) {
-      this.resolveHiveMysqlDatabase();
+      this.checkDatabaseConnectionTest().done(function() {
+        _this.resolveHiveMysqlDatabase();
+      });
     }
   }
 

+ 8 - 5
ambari-web/app/data/HDP2/global_properties.js

@@ -1029,7 +1029,7 @@ module.exports =
       "isRequired": false,
       "serviceName": "OOZIE",
       "category": "Oozie Server",
-      "index": 7
+      "index": 9
     },
     {
       "id": "puppet var",
@@ -1057,9 +1057,10 @@ module.exports =
       "isVisible": false,
       "isObserved": true,
       "serviceName": "OOZIE",
-      "category": "Oozie Server"
+      "category": "Oozie Server",
+      "index": 3
     },
-        {
+    {
       "id": "puppet var",
       "name": "oozie_existing_postgresql_host",
       "displayName": "Database Host",
@@ -1071,7 +1072,8 @@ module.exports =
       "isVisible": false,
       "isObserved": true,
       "serviceName": "OOZIE",
-      "category": "Oozie Server"
+      "category": "Oozie Server",
+      "index": 3
     },
     {
       "id": "puppet var",
@@ -1085,7 +1087,8 @@ module.exports =
       "isVisible": false,
       "isObserved": true,
       "serviceName": "OOZIE",
-      "category": "Oozie Server"
+      "category": "Oozie Server",
+      "index": 3
     },
     {
       "id": "puppet var",

+ 5 - 5
ambari-web/app/data/HDP2/site_properties.js

@@ -377,7 +377,7 @@ module.exports =
       "isObserved": true,
       "category": "Oozie Server",
       "serviceName": "OOZIE",
-      "index": 3
+      "index": 4
     },
     {
       "id": "site property",
@@ -387,7 +387,7 @@ module.exports =
       "displayType": "host",
       "category": "Oozie Server",
       "serviceName": "OOZIE",
-      "index": 4
+      "index": 5
     },
     {
       "id": "site property",
@@ -398,7 +398,7 @@ module.exports =
       "category": "Oozie Server",
       "serviceName": "OOZIE",
       "filename": "oozie-site.xml",
-      "index": 5
+      "index": 6
     },
     {
       "id": "site property",
@@ -407,7 +407,7 @@ module.exports =
       "isOverridable": false,
       "category": "Oozie Server",
       "serviceName": "OOZIE",
-      "index": 6
+      "index": 7
     },
     {
       "id": "site property",
@@ -417,7 +417,7 @@ module.exports =
       "displayType": "advanced",
       "category": "Oozie Server",
       "serviceName": "OOZIE",
-      "index": 7
+      "index": 8
     },
 
   /**********************************************hive-site***************************************/

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

@@ -172,6 +172,7 @@ Em.I18n.translations = {
   'common.path': 'Path',
   'common.package': 'Package',
   'common.proceed': 'Proceed',
+  'common.proceedAnyway': 'Proceed Anyway',
   'common.process': 'Process',
   'common.property': 'Property',
   'common.installed': 'Installed',
@@ -609,6 +610,11 @@ Em.I18n.translations = {
   'installer.step7.popup.mySQLWarning.button.dismiss': 'Dismiss',
   'installer.step7.popup.mySQLWarning.confirmation.header': 'Confirmation: Go to Assign Masters',
   'installer.step7.popup.mySQLWarning.confirmation.body': 'You will be brought back to the \"Assign Masters\" step and will lose all your current customizations. Are you sure?',
+  'installer.step7.popup.database.connection.header': 'Database Connectivity Warning',
+  'installer.step7.popup.database.connection.body': 'You have not run the database connectivity check for {0}. It is recommended that you run the check before proceeding to prevent failures during deployment.',
+
+  'installer.step7.oozie.database.new': 'New Derby Database',
+  'installer.step7.hive.database.new': 'New MySQL Database',
 
   'installer.step8.header':'Review',
   'installer.step8.body':'Please review the configuration before installation',
@@ -1282,6 +1288,10 @@ Em.I18n.translations = {
   'services.service.config.propertyFilterPopover.title':'Properties filter',
   'services.service.config.propertyFilterPopover.content':'Enter keywords to filter properties by property name, value, or description.',
   'services.service.config.hive.oozie.postgresql': 'Existing PostgreSQL Database',
+  'services.service.config.database.connection.success': 'Connection OK',
+  'services.service.config.database.connection.failed': 'Connection Failed',
+  'services.service.config.database.btn.idle': 'Test Connection',
+  'services.service.config.database.btn.connecting': 'Connecting...',
 
   'services.add.header':'Add Service Wizard',
   'services.reassign.header':'Move Master Wizard',

+ 1 - 0
ambari-web/app/models/cluster_states.js

@@ -191,6 +191,7 @@ App.clusterStatus = Em.Object.create(App.UserPref, {
     var login = App.db.getLoginName();
     var val = {clusterName: this.get('clusterName')};
     if (newValue) {
+      App.db.cleanTmp();
       //setter
       if (newValue.clusterName) {
         this.set('clusterName', newValue.clusterName);

+ 6 - 0
ambari-web/app/models/service_config.js

@@ -170,6 +170,12 @@ App.ServiceConfigProperty = Ember.Object.extend({
   editDone: false, //Text field: on focusOut: true, on focusIn: false
   serviceValidator: null,
   isNotSaved: false, // user property was added but not saved
+  /**
+   * Usage example see on <code>App.ServiceConfigRadioButtons.handleDBConnectionProperty()</code>
+   *
+   * @property {Ember.View} additionalView - custom view related to property
+   **/
+  additionalView: null,
 
   /**
    * On Overridable property error message, change overrideErrorTrigger value to recount number of errors service have

+ 32 - 0
ambari-web/app/styles/application.less

@@ -1178,6 +1178,34 @@ h1 {
   .dropdown-menu > li > a:hover {
     text-shadow: none;
   }
+  .db-connection {
+    .spinner {
+      width: 30px;
+      height: 30px;
+      background-size: 30px 30px;
+    }
+    .icon-ok-sign, .icon-warning-sign {
+      font-size: 27px;
+      line-height: 30px;
+    }
+    .icon-ok-sign {
+      color: @health-status-green;
+    }
+    .icon-warning-sign {
+      color: @health-status-red;
+    }
+    .connection-result {
+      font-size: 15px;
+      line-height: 30px;
+    }
+    a.mute {
+      color: #333;
+      &:hover {
+        text-decoration: none;
+        color: #333;
+      }
+    }
+  }
 }
 
 .running-host-components-table{
@@ -6100,6 +6128,10 @@ i.icon-asterisks {
   margin-top: @space-m;
 }
 
+.mll {
+  margin-left: @space-l;
+}
+
 #cleanerContainer {
   z-index: 2;
   position: absolute;

+ 3 - 0
ambari-web/app/templates/common/configs/service_config.hbs

@@ -144,6 +144,9 @@
                             {{/if}}
                           </div>
                       </div>
+                    {{#if this.additionalView}}
+                      {{view additionalView}}
+                    {{/if}}
                   {{/each}}
 
                   {{! For Advanced, Advanced Core Site, Advanced HDFS Site sections, show the 'Add Property' link.}}

+ 33 - 0
ambari-web/app/templates/common/error_log_body.hbs

@@ -0,0 +1,33 @@
+{{!
+* 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.
+}}
+
+<div class="task-detail-log-info">
+  <div class="content-area">
+    <div class="task-detail-log-clipboard-wrap"></div>
+    <div class="task-detail-log-maintext">
+      <h5>stderr: &nbsp; <span class="muted">{{view.openedTask.errorLog}} </span></h5>
+      <pre class="stderr">{{view.openedTask.stderr}}</pre>
+      <h5>stdout: &nbsp; <span class="muted"> {{view.openedTask.outputLog}} </span></h5>
+      <pre class="stdout">{{view.openedTask.stdout}}</pre>
+      {{#if view.openedTask.structuredOut}}
+        <h5>structured_out: &nbsp;</h5>
+        <pre class="stdout">{{view.openedTask.structuredOut}}</pre>
+      {{/if}}
+    </div>
+  </div>
+</div>

+ 33 - 0
ambari-web/app/templates/common/form/check_db_connection.hbs

@@ -0,0 +1,33 @@
+{{!
+* 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.
+}}
+
+<div class="entry-row db-connection">
+  <span class="control-label"></span>
+  <div class="controls">
+    <div class="control-group">
+      <div class="span9">
+        <span {{bindAttr class=":pull-left :btn :btn-primary view.isBtnDisabled:disabled"}} {{action connectToDatabase target="view"}}>{{view.btnCaption}}</span>
+        <div {{bindAttr class=":spinner :mll :pull-left view.isConnecting::hide"}}></div>
+        <div class="pull-left connection-result mll">
+            <a {{bindAttr class="view.isConnectionSuccess:mute:action view.isRequestResolved::hide"}} {{action showLogsPopup target="view"}}>{{view.responseCaption}}</a>
+        </div>
+        <i {{bindAttr class=":pull-right view.isConnectionSuccess:icon-ok-sign:icon-warning-sign view.isRequestResolved::hide"}}></i>
+      </div>
+    </div>
+  </div>
+</div>

+ 34 - 0
ambari-web/app/utils/ajax/ajax.js

@@ -2150,6 +2150,40 @@ var urls = {
   'hosts.heatmaps': {
     'real': '/clusters/{clusterName}/hosts?fields=Hosts/host_name,Hosts/maintenance_state,Hosts/public_host_name,Hosts/cpu_count,Hosts/ph_cpu_count,Hosts/total_mem,Hosts/host_status,Hosts/last_heartbeat_time,Hosts/os_arch,Hosts/os_type,Hosts/ip,host_components/HostRoles/state,host_components/HostRoles/maintenance_state,Hosts/disk_info,metrics/disk,metrics/load/load_one,metrics/cpu/cpu_system,metrics/cpu/cpu_user,metrics/memory/mem_total,metrics/memory/mem_free,alerts/summary&minimal_response=true',
     'mock': ''
+  },
+
+  'custom_action.create': {
+    'real': '/requests',
+    'mock': '',
+    'format': function(data) {
+      var requestInfo = {
+        context: 'Check host',
+        action: 'check_host',
+        parameters: { }
+      };
+      $.extend(true, requestInfo, data.requestInfo)
+      return {
+        type: 'POST',
+        async: true,
+        data: JSON.stringify({
+          'RequestInfo': requestInfo,
+          'Requests/resource_filters': [{
+            hosts: data.filteredHosts.join(',')
+          }]
+        })
+      }
+    }
+  },
+  'custom_action.request': {
+    'real': '/requests/{requestId}/tasks/{taskId}',
+    'mock': '',
+    'format': function(data) {
+      return {
+        async: true,
+        requestId: data.requestId,
+        taskId: data.taskId || ''
+      }
+    }
   }
 };
 /**

+ 7 - 1
ambari-web/app/utils/db.js

@@ -37,7 +37,8 @@ var InitialData =  {
   'ReassignMaster' : {},
   'AddSecurity': {},
   'HighAvailabilityWizard': {},
-  'RollbackHighAvailabilityWizard': {}
+  'RollbackHighAvailabilityWizard': {},
+  'tmp': {}
 
 };
 
@@ -74,6 +75,11 @@ App.db.cleanUp = function () {
   localStorage.setObject('ambari', App.db.data);
 };
 
+App.db.cleanTmp = function() {
+  App.db.data.tmp = {};
+  localStorage.setObject('ambari', App.db.data);
+}
+
 App.db.updateStorage = function() {
   App.db.data = localStorage.getObject('ambari');
   if (App.db.data && App.db.data.app && App.db.data.app.tables && App.db.data.app.configs) {

+ 19 - 0
ambari-web/app/utils/helper.js

@@ -275,6 +275,25 @@ App.isEmptyObject = function(obj) {
   for (var prop in obj) { if (obj.hasOwnProperty(prop)) {empty = false; break;} }
   return empty;
 }
+/**
+ * Returns object with defined keys only.
+ *
+ * @memberof App
+ * @method permit
+ * @param {Object} obj - input object
+ * @param {String|Array} keys - allowed keys
+ * @return {Object}
+ */
+App.permit = function(obj, keys) {
+  var result = {};
+  if (typeof obj !== 'object' || App.isEmptyObject(obj)) return result;
+  if (typeof keys == 'string') keys = Array(keys);
+  keys.forEach(function(key) {
+    if (obj.hasOwnProperty(key))
+      result[key] = obj[key];
+  });
+  return result;
+};
 /**
  *
  * @namespace App

+ 338 - 0
ambari-web/app/views/wizard/controls_view.js

@@ -335,6 +335,30 @@ App.ServiceConfigRadioButtons = Ember.View.extend({
     }
   }.property('serviceConfig.serviceName'),
 
+  /**
+   * `Observer` that add <code>additionalView</code> to <code>App.ServiceConfigProperty</code>
+   * that responsible for checking database connection.
+   *
+   * @method handleDBConnectionProperty
+   **/
+  handleDBConnectionProperty: function() {
+    if (!App.supports.databaseConnection) return;
+    if (!['addServiceController', 'installerController'].contains(App.clusterStatus.wizardControllerName)) return;
+    var handledProperties = ['oozie_database', 'hive_database'];
+    var currentValue = this.get('serviceConfig.value');
+    var databases = /MySQL|PostgreSQL|Oracle|Derby/gi;
+    var currentDB = currentValue.match(databases)[0];
+    var existingDatabase = /existing/gi.test(currentValue);
+    var propertyAppendTo = this.get('categoryConfigsAll').findProperty('displayName', 'Database URL');
+    if (currentDB && existingDatabase) {
+      if (handledProperties.contains(this.get('serviceConfig.name'))) {
+        if (propertyAppendTo) propertyAppendTo.set('additionalView', App.CheckDBConnectionView.extend({databaseName: currentDB}));
+      }
+    } else {
+      propertyAppendTo.set('additionalView', null);
+    }
+  }.observes('serviceConfig.value', 'configs.@each.value'),
+
   optionsBinding: 'serviceConfig.options'
 });
 
@@ -656,4 +680,318 @@ App.SlaveComponentChangeGroupNameView = Ember.View.extend({
     }
   }
 });
+/**
+ * View for testing connection to database.
+ **/
+App.CheckDBConnectionView = Ember.View.extend({
+  templateName: require('templates/common/form/check_db_connection'),
+  /** @property {string} btnCaption - text for button **/
+  btnCaption: Em.I18n.t('services.service.config.database.btn.idle'),
+  /** @property {string} responseCaption - text for status link **/
+  responseCaption: null,
+  /** @property {boolean} isConnecting - is request to server activated **/
+  isConnecting: false,
+  /** @property {boolean} isValidationPassed - check validation for required fields **/
+  isValidationPassed: null,
+  /** @property {string} databaseName- name of current database **/
+  databaseName: null,
+  /** @property {boolean} isRequestResolved - check for finished request to server **/
+  isRequestResolved: false,
+  /** @property {boolean} isConnectionSuccess - check for successful connection to database **/
+  isConnectionSuccess: null,
+  /** @property {string} responseFromServer - message from server response **/
+  responseFromServer: null,
+  /** @property {Object} ambariRequiredProperties - properties that need for custom action request **/
+  ambariRequiredProperties: null,
+  /** @property {Number} currentRequestId - current custom action request id **/
+  currentRequestId: null,
+  /** @property {Number} currentTaskId - current custom action task id **/
+  currentTaskId: null,
+  /** @property {jQuery.Deferred} request - current $.ajax request **/
+  request: null,
+  /** @property {Number} pollInterval - timeout interval for ajax polling **/
+  pollInterval: 3000,
+  /** @property {string} hostNameProperty - host name property based on service and database names **/
+  hostNameProperty: function() {
+    return '{0}_existing_{1}_host'.format(this.get('parentView.service.serviceName').toLowerCase(), this.get('databaseName').toLowerCase());
+  }.property('databaseName'),
+  /** @property {boolean} isBtnDisabled - disable button on failed validation or active request **/
+  isBtnDisabled: function() {
+    return !this.get('isValidationPassed') || this.get('isConnecting');
+  }.property('isValidationPassed', 'isConnecting'),
+  /** @property {object} requiredProperties - properties that necessary for database connection **/
+  requiredProperties: function() {
+    var propertiesMap = {
+      OOZIE: ['oozie.db.schema.name','oozie.service.JPAService.jdbc.username','oozie.service.JPAService.jdbc.password','oozie.service.JPAService.jdbc.driver','oozie.service.JPAService.jdbc.url'],
+      HIVE: ['ambari.hive.db.schema.name','javax.jdo.option.ConnectionUserName','javax.jdo.option.ConnectionPassword','javax.jdo.option.ConnectionDriverName','javax.jdo.option.ConnectionURL']
+    };
+    return propertiesMap[this.get('parentView.service.serviceName')];
+  }.property(),
+  /** @property {Object} propertiesPattern - check pattern according to type of connection properties **/
+  propertiesPattern: function() {
+    return {
+      user_name: /username$/ig,
+      user_passwd: /password$/ig,
+      db_connection_url: /jdbc\.url|connectionurl/ig
+    }
+  }.property(),
+  /** @property {Object} connectionProperties - service specific config values mapped for custom action request **/
+  connectionProperties: function() {
+    var propObj = {};
+    for (var key in this.get('propertiesPattern')) {
+      propObj[key] = this.getConnectionProperty(this.get('propertiesPattern')[key]);
+    }
+    return propObj;
+  }.property('parentView.categoryConfigsAll.@each.value'),
+  /**
+   * Properties that stores in local storage used for handling
+   * last success connection.
+   *
+   * @property {Object} preparedDBProperties
+   **/
+  preparedDBProperties: function() {
+    var propObj = {};
+    for (var key in this.get('propertiesPattern')) {
+      var propName = this.getConnectionProperty(this.get('propertiesPattern')[key], true);
+      propObj[propName] = this.get('parentView.categoryConfigsAll').findProperty('name', propName).get('value');
+    }
+    return propObj;
+  }.property(),
+  /** Check validation and load ambari properties **/
+  didInsertElement: function() {
+    this.handlePropertiesValidation();
+    this.getAmbariProperties();
+  },
+  /** On view destroy **/
+  willDestroyElement: function() {
+    this.set('isConnecting', false);
+    this._super();
+  },
+  /**
+   * Observer that take care about enabling/disabling button based on required properties validation.
+   *
+   * @method handlePropertiesValidation
+   **/
+  handlePropertiesValidation: function() {
+    this.restore();
+    var isValid = true;
+    var properties = [].concat(this.get('requiredProperties'));
+    properties.push(this.get('hostNameProperty'));
+    properties.forEach(function(propertyName) {
+      if(!this.get('parentView.categoryConfigsAll').findProperty('name', propertyName).get('isValid')) isValid = false;
+    }, this);
+    this.set('isValidationPassed', isValid);
+  }.observes('parentView.categoryConfigsAll.@each.isValid', 'parentView.categoryConfigsAll.@each.value', 'databaseName'),
+
+  getConnectionProperty: function(regexp, isGetName) {
+    var _this = this;
+    var propertyName = _this.get('requiredProperties').filter(function(item) {
+      return regexp.test(item);
+    })[0];
+    return (isGetName) ? propertyName : _this.get('parentView.categoryConfigsAll').findProperty('name', propertyName).get('value');
+  },
+  /**
+   * Set up ambari properties required for custom action request
+   *
+   * @method getAmbariProperties
+   **/
+  getAmbariProperties: function() {
+    var clusterController = App.router.get('clusterController');
+    var _this = this;
+    if (!App.isEmptyObject(App.db.get('tmp', 'ambariProperties')) && !this.get('ambariProperties')) {
+      this.set('ambariProperties', App.db.get('tmp', 'ambariProperties'));
+      return;
+    }
+    if (App.isEmptyObject(clusterController.get('ambariProperties'))) {
+      clusterController.loadAmbariProperties().done(function(data) {
+        _this.formatAmbariProperties(data.RootServiceComponents.properties);
+      });
+    } else {
+      this.formatAmbariProperties(clusterController.get('ambariProperties'));
+    }
+  },
+
+  formatAmbariProperties: function(properties) {
+    var defaults = {
+      threshold: "60",
+      ambari_server_host: location.hostname,
+      check_execute_list : "db_connection_check"
+    };
+    var properties = App.permit(properties, ['jdk.name','jdk_location','java.home']);
+    var renameKey = function(oldKey, newKey) {
+      if (properties[oldKey]) {
+        defaults[newKey] = properties[oldKey];
+        delete properties[oldKey];
+      }
+    };
+    renameKey('java.home', 'java_home');
+    renameKey('jdk.name', 'jdk_name');
+    $.extend(properties, defaults);
+    App.db.set('tmp', 'ambariProperties', properties);
+    this.set('ambariProperties', properties);
+  },
+  /**
+   * `Action` method for starting connect to current database.
+   *
+   * @method connectToDatabase
+   **/
+  connectToDatabase: function() {
+    var self = this;
+    self.set('isRequestResolved', false);
+    App.db.set('tmp', this.get('parentView.service.serviceName') + '_connection', {});
+    this.setConnectingStatus(true);
+    this.createCustomAction();
+  },
+  /**
+   * Run custom action for database connection.
+   *
+   * @method createCustomAction
+   **/
+  createCustomAction: function() {
+    var databaseHost = this.get('parentView.categoryConfigsAll').findProperty('name', this.get('hostNameProperty')).get('value');
+    var params = $.extend(true, {}, { db_name: this.get('databaseName').toLowerCase() }, this.get('connectionProperties'), this.get('ambariProperties'));
+    App.ajax.send({
+      name: 'custom_action.create',
+      sender: this,
+      data: {
+        requestInfo: {
+          parameters: params
+        },
+        filteredHosts: [databaseHost]
+      },
+      success: 'onCreateActionSuccess',
+      error: 'onCreateActionError'
+    });
+  },
+  /**
+   * Run updater if task is created successfully.
+   *
+   * @method onConnectActionS
+   **/
+  onCreateActionSuccess: function(data) {
+    this.set('currentRequestId', data.Requests.id);
+    App.ajax.send({
+      name: 'custom_action.request',
+      sender: this,
+      data: {
+        requestId: this.get('currentRequestId')
+      },
+      success: 'setCurrentTaskId'
+    });
+  },
+
+  setCurrentTaskId: function(data) {
+    this.set('currentTaskId', data.items[0].Tasks.id);
+    this.startPolling();
+  },
+
+  startPolling: function() {
+    if (this.get('isConnecting'))
+      this.getTaskInfo();
+  },
+
+  getTaskInfo: function() {
+    var request = App.ajax.send({
+      name: 'custom_action.request',
+      sender: this,
+      data: {
+        requestId: this.get('currentRequestId'),
+        taskId: this.get('currentTaskId')
+      },
+      success: 'getTaskInfoSuccess'
+    });
+    this.set('request', request);
+  },
+
+  getTaskInfoSuccess: function(data) {
+    var task = data.Tasks;
+    if (task.status === 'COMPLETED') {
+      var structuredOut = JSON.parse(task.structured_out).db_connection_check;
+      if (structuredOut.exit_code != 0) {
+        this.set('responseFromServer', {
+          stderr: task.stderr,
+          stdout: task.stdout,
+          structuredOut: structuredOut.message
+        });
+        this.setResponseStatus('failed');
+      } else {
+        App.db.set('tmp', this.get('parentView.service.serviceName') + '_connection', this.get('preparedDBProperties'));
+        this.setResponseStatus('success');
+      }
+    }
+    if (task.status === 'FAILED') {
+      this.set('responseFromServer', {
+        stderr: task.stderr,
+        stdout: task.stdout
+      });
+      this.setResponseStatus('failed');
+    }
+    if (/PENDING|QUEUED|IN_PROGRESS/.test(task.status)) {
+      Em.run.later(this, function() {
+        this.startPolling();
+      }, this.get('pollInterval'));
+    }
+  },
+
+  onCreateActionError: function(jqXhr, status, errorMessage) {
+    this.setResponseStatus('failed');
+    this.set('responseFromServer', errorMessage);
+  },
+
+  setResponseStatus: function(isSuccess) {
+    var isSuccess = isSuccess == 'success';
+    this.setConnectingStatus(false);
+    this.set('responseCaption', isSuccess ? Em.I18n.t('services.service.config.database.connection.success') : Em.I18n.t('services.service.config.database.connection.failed'));
+    this.set('isConnectionSuccess', isSuccess);
+    this.set('isRequestResolved', true);
+  },
+  /**
+   * Switch captions and statuses for active/non-active request.
+   *
+   * @method setConnectionStatus
+   * @param {Boolean} [active]
+   */
+  setConnectingStatus: function(active) {
+    if (active) {
+      this.set('responseCaption', null);
+      this.set('responseFromServer', null);
+    }
+    this.set('btnCaption', !!active ? Em.I18n.t('services.service.config.database.btn.connecting') : Em.I18n.t('services.service.config.database.btn.idle'));
+    this.set('isConnecting', !!active);
+  },
+  /**
+   * Set view to init status.
+   *
+   * @method restore
+   **/
+  restore: function() {
+    if (this.get('request')) {
+      this.get('request').abort();
+      this.set('request', null);
+    }
+    this.set('responseCaption', null);
+    this.set('responseFromServer', null);
+    this.setConnectingStatus(false);
+    this.set('isRequestResolved', false);
+  },
+  /**
+   * `Action` method for showing response from server in popup.
+   *
+   * @method showLogsPopup
+   **/
+  showLogsPopup: function() {
+    if (this.get('isConnectionSuccess')) return;
+    var _this = this;
+    var popup = App.showAlertPopup('Error: {0} connection'.format(this.get('databaseName')));
+    if (typeof this.get('responseFromServer') == 'object') {
+      popup.set('bodyClass', Em.View.extend({
+        templateName: require('templates/common/error_log_body'),
+        openedTask: _this.get('responseFromServer')
+      }));
+    } else {
+      popup.set('body', this.get('responseFromServer'));
+    }
+    return popup;
+  }
+});
 

+ 31 - 0
ambari-web/test/utils/helper_test.js

@@ -206,4 +206,35 @@ describe('utils/helper', function() {
       });
     });
   });
+  describe('#App.permit()', function() {
+    var obj = {
+      a1: 'v1',
+      a2: 'v2',
+      a3: 'v3'
+    }
+
+    var tests = [
+      {
+        keys: 'a1',
+        e: {
+          a1: 'v1'
+        }
+      },
+      {
+        keys: ['a2','a3','a4'],
+        e: {
+          a2: 'v2',
+          a3: 'v3'
+        }
+      }
+    ];
+
+    tests.forEach(function(test) {
+      it('should return object `{0}` permitted keys `{1}`'.format(JSON.stringify(test.e), JSON.stringify(test.keys)), function() {
+        expect(App.permit(obj, test.keys)).to.deep.eql(test.e);
+      });
+    });
+
+
+  });
 });

+ 406 - 0
ambari-web/test/views/wizard/controls_view_test.js

@@ -0,0 +1,406 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+require('views/wizard/controls_view');
+require('utils/ajax/ajax');
+require('router');
+
+describe('views/wizard/control_views', function() {
+  describe('App.CheckDBConnectionView', function() {
+    var createView = function(serviceName) {
+      return App.CheckDBConnectionView.extend({
+        parentView: Em.View.create({
+          service: Em.Object.create({
+            serviceName: serviceName
+          }),
+          categoryConfigsAll: function() {
+            return Em.A(
+              require('data/HDP2/global_properties').configProperties.concat(require('data/HDP2/site_properties').configProperties)
+                .filterProperty('serviceName', serviceName).map(function(property) { return App.ServiceConfigProperty.create(property)})
+            );
+          }.property()
+        })
+      });
+    };
+    var generateTypeValueProp = function(type, value) {
+      return {
+        type: type,
+        value: value
+      }
+    };
+    var prepareConfigsTesting = function() {
+      var view = createView('HIVE').create({ databaseName: 'MySQL'});
+      var setConfigProperty = function(name, value) {
+        view.get('parentView.categoryConfigsAll').findProperty('name', name).set('value', value);
+      };
+
+      setConfigProperty('javax.jdo.option.ConnectionUserName', 'hive_user');
+      setConfigProperty('javax.jdo.option.ConnectionPassword', 'hive_pass');
+      setConfigProperty('ambari.hive.db.schema.name', 'hive_scheme');
+      setConfigProperty('javax.jdo.option.ConnectionURL', 'hive_c_url');
+      return view;
+    }
+    describe('`Oozie` properties checking', function() {
+      var view = createView('OOZIE').create();
+      describe('#requiredProperties', function() {
+        var expectedProperties = ['oozie.db.schema.name','oozie.service.JPAService.jdbc.username','oozie.service.JPAService.jdbc.password','oozie.service.JPAService.jdbc.driver','oozie.service.JPAService.jdbc.url'];
+        it('required properties present {0}'.format(expectedProperties.join(',')), function() {
+          expect(view.get('requiredProperties')).to.have.members(expectedProperties);
+        });
+      });
+      describe('#hostNameProperty', function() {
+        var testMessage = 'property name should be `{0}`';
+        var tests = [
+          {
+            databaseName: 'MySQL',
+            e: 'oozie_existing_mysql_host'
+          },
+          {
+            databaseName: 'PostgreSQL',
+            e: 'oozie_existing_postgresql_host'
+          },
+          {
+            databaseName: 'Oracle',
+            e: 'oozie_existing_oracle_host'
+          }
+        ];
+        tests.forEach(function(test) {
+          it(testMessage.format(test.e), function() {
+            view.set('databaseName', test.databaseName);
+            expect(view.get('hostNameProperty')).to.eql(test.e);
+          });
+        });
+      });
+    });
+
+    describe('`Hive` properties checking', function() {
+      var view = createView('HIVE').create();
+      describe('#requiredProperties', function() {
+        var expectedProperties = ['ambari.hive.db.schema.name','javax.jdo.option.ConnectionUserName','javax.jdo.option.ConnectionPassword','javax.jdo.option.ConnectionDriverName','javax.jdo.option.ConnectionURL'];
+        it('required properties present {0}'.format(expectedProperties.join(',')), function() {
+          expect(view.get('requiredProperties')).to.have.members(expectedProperties);
+        });
+      });
+      describe('#hostNameProperty', function() {
+        var testMessage = 'property name should be `{0}`';
+        var tests = [
+          {
+            databaseName: 'MySQL',
+            e: 'hive_existing_mysql_host'
+          },
+          {
+            databaseName: 'PostgreSQL',
+            e: 'hive_existing_postgresql_host'
+          },
+          {
+            databaseName: 'Oracle',
+            e: 'hive_existing_oracle_host'
+          }
+        ];
+        tests.forEach(function(test) {
+          it(testMessage.format(test.e), function() {
+            view.set('databaseName', test.databaseName);
+            expect(view.get('hostNameProperty')).to.eql(test.e);
+          });
+        }, this);
+      });
+      describe('#connectionProperties', function() {
+        var view = prepareConfigsTesting();
+        var tests = [
+          generateTypeValueProp('user_name', 'hive_user'),
+          generateTypeValueProp('user_passwd', 'hive_pass'),
+          generateTypeValueProp('db_connection_url', 'hive_c_url'),
+          generateTypeValueProp('db_name', 'hive_scheme')
+        ];
+        var testMessage = 'property `{0}` should have `{1}`';
+        tests.forEach(function(test) {
+          it(testMessage.format(test.value, test.type), function() {
+            expect(view.get('connectionProperties')[test.type]).to.eql(test.value);
+          });
+        });
+      });
+
+      describe('#preparedDBProperties', function() {
+        var view = prepareConfigsTesting();
+        var tests = [
+          generateTypeValueProp('javax.jdo.option.ConnectionUserName', 'hive_user'),
+          generateTypeValueProp('javax.jdo.option.ConnectionPassword', 'hive_pass'),
+          generateTypeValueProp('javax.jdo.option.ConnectionURL', 'hive_c_url'),
+          generateTypeValueProp('ambari.hive.db.schema.name', 'hive_scheme')
+        ];
+        var testMessage = 'property `{1}` should have `{0}`';
+        tests.forEach(function(test) {
+          it(testMessage.format(test.value, test.type), function() {
+            expect(view.get('preparedDBProperties')[test.type]).to.eql(test.value);
+          });
+        });
+      });
+
+
+    });
+
+    describe('#isBtnDisabled', function() {
+      var view = createView('HIVE').create({ databaseName: 'MySQL' });
+      var testMessage = 'button should be {0} if `isValidationPassed`/`isConnecting`: {1}/{2}';
+      var tests = [
+        {
+          isValidationPassed: true,
+          isConnecting: true,
+          e: true
+        },
+        {
+          isValidationPassed: true,
+          isConnecting: false,
+          e: false
+        }
+      ];
+      tests.forEach(function(test) {
+        it(testMessage.format(!!test.e ? 'disabled' : 'enabled', test.isValidationPassed, test.isConnecting), function() {
+          view.set('isValidationPassed', test.isValidationPassed);
+          view.set('isConnecting', test.isConnecting);
+          expect(view.get('isBtnDisabled')).to.be.eql(test.e);
+        });
+      })
+    });
+
+    describe('#connectToDatabase()', function() {
+      before(function() {
+        sinon.spy(App.ajax, 'send');
+      });
+      describe('connection request validation', function() {
+        var view = createView('HIVE').create({ databaseName: 'MySQL'});
+        var setConfigProperty = function(name, value) {
+          view.get('parentView.categoryConfigsAll').findProperty('name', name).set('value', value);
+        };
+
+        setConfigProperty('javax.jdo.option.ConnectionUserName', 'hive_user');
+        setConfigProperty('javax.jdo.option.ConnectionPassword', 'hive_pass');
+        setConfigProperty('ambari.hive.db.schema.name', 'hive_scheme');
+        setConfigProperty('javax.jdo.option.ConnectionURL', 'hive_c_url');
+
+        it('request should be passed with correct params', function() {
+          view.connectToDatabase();
+          expect(App.ajax.send.calledOnce).to.be.ok;
+        })
+      });
+      after(function() {
+        App.ajax.send.restore();
+      })
+    });
+
+  });
+
+  describe('App.ServiceConfigRadioButtons', function() {
+    var createView = function(serviceName) {
+      return App.ServiceConfigRadioButtons.extend({
+        categoryConfigsAll: function() {
+          return Em.A(
+            require('data/HDP2/global_properties').configProperties.concat(require('data/HDP2/site_properties').configProperties)
+              .filterProperty('serviceName', serviceName).map(function(property) { return App.ServiceConfigProperty.create(property)})
+          );
+        }.property()
+      });
+    };
+
+    var setProperties = function(properties, propertyMap) {
+      for (var propertyName in propertyMap) {
+        properties.findProperty('name', propertyName).set('value', propertyMap[propertyName]);
+      }
+    };
+
+    before(function() {
+      App.clusterStatus.set('wizardControllerName','installerController');
+    });
+    describe('#onOptionsChange()', function() {
+      var oozieDerby =  {
+        serviceConfig: { value: 'New Derby Database' },
+        setupProperties: {
+          'oozie.db.schema.name': 'derby.oozie.schema',
+          'oozie.service.JPAService.jdbc.driver': 'oozie.driver',
+          'oozie_ambari_host': 'derby.host.com'
+        },
+        expectedProperties: [
+          {
+            path: 'databaseName',
+            value: 'derby.oozie.schema'
+          },
+          {
+            path: 'dbClass.name',
+            value: 'oozie.service.JPAService.jdbc.driver'
+          },
+          {
+            path: 'dbClass.value',
+            value: 'org.apache.derby.jdbc.EmbeddedDriver'
+          },
+          {
+            path: 'hostName',
+            value: 'derby.host.com'
+          },
+          {
+            path: 'connectionUrl.name',
+            value: 'oozie.service.JPAService.jdbc.url'
+          },
+          {
+            path: 'connectionUrl.value',
+            value: 'jdbc:derby:${oozie.data.dir}/${oozie.db.schema.name}-db;create=true'
+          }
+        ]
+      };
+      var oozieExistingMysql = {
+        serviceConfig: { value: 'Existing MySQL Database' },
+        setupProperties: {
+          'oozie.db.schema.name': 'mysql.oozie.schema',
+          'oozie.service.JPAService.jdbc.driver': 'oozie.driver',
+          'oozie_existing_mysql_host': 'mysql.host.com'
+        },
+        expectedProperties: [
+          {
+            path: 'databaseName',
+            value: 'mysql.oozie.schema'
+          },
+          {
+            path: 'dbClass.name',
+            value: 'oozie.service.JPAService.jdbc.driver'
+          },
+          {
+            path: 'dbClass.value',
+            value: 'com.mysql.jdbc.Driver'
+          },
+          {
+            path: 'hostName',
+            value: 'mysql.host.com'
+          },
+          {
+            path: 'connectionUrl.name',
+            value: 'oozie.service.JPAService.jdbc.url'
+          },
+          {
+            path: 'connectionUrl.value',
+            value: 'jdbc:mysql://mysql.host.com/mysql.oozie.schema'
+          }
+        ]
+      };
+      var oozieExistingPostgresql = {
+        serviceConfig: { value: 'Existing PostgreSQL Database' },
+        setupProperties: {
+          'oozie.db.schema.name': 'postgresql.oozie.schema',
+          'oozie.service.JPAService.jdbc.driver': 'oozie.driver',
+          'oozie_existing_postgresql_host': 'postgresql.host.com'
+        },
+        expectedProperties: [
+          {
+            path: 'databaseName',
+            value: 'postgresql.oozie.schema'
+          },
+          {
+            path: 'dbClass.name',
+            value: 'oozie.service.JPAService.jdbc.driver'
+          },
+          {
+            path: 'dbClass.value',
+            value: 'org.postgresql.Driver'
+          },
+          {
+            path: 'hostName',
+            value: 'postgresql.host.com'
+          },
+          {
+            path: 'connectionUrl.name',
+            value: 'oozie.service.JPAService.jdbc.url'
+          },
+          {
+            path: 'connectionUrl.value',
+            value: 'jdbc:postgresql://postgresql.host.com:5432/postgresql.oozie.schema'
+          }
+        ]
+      };
+      var oozieExistingOracle = {
+        serviceConfig: { value: 'Existing Oracle Database' },
+        setupProperties: {
+          'oozie.db.schema.name': 'oracle.oozie.schema',
+          'oozie.service.JPAService.jdbc.driver': 'oozie.driver',
+          'oozie_existing_oracle_host': 'oracle.host.com'
+        },
+        expectedProperties: [
+          {
+            path: 'databaseName',
+            value: 'oracle.oozie.schema'
+          },
+          {
+            path: 'dbClass.name',
+            value: 'oozie.service.JPAService.jdbc.driver'
+          },
+          {
+            path: 'dbClass.value',
+            value: 'oracle.jdbc.driver.OracleDriver'
+          },
+          {
+            path: 'hostName',
+            value: 'oracle.host.com'
+          },
+          {
+            path: 'connectionUrl.name',
+            value: 'oozie.service.JPAService.jdbc.url'
+          },
+          {
+            path: 'connectionUrl.value',
+            value: 'jdbc:oracle:thin:@//oracle.host.com:1521/oracle.oozie.schema'
+          }
+        ]
+      };
+      var tests = [
+        {
+          serviceName: 'OOZIE',
+          mockData: [
+            oozieDerby,
+            oozieExistingMysql,
+            oozieExistingPostgresql,
+            oozieExistingOracle
+          ]
+        }
+      ];
+      tests.forEach(function(test) {
+        describe('`{0}` service processing'.format(test.serviceName), function() {
+          test.mockData.forEach(function(test) {
+            describe('`oozie_database` value "{0}"'.format(test.serviceConfig.value), function() {
+              var view = createView('OOZIE').create();
+              before(function() {
+                var categoryConfigs = view.get('categoryConfigsAll');
+                view.reopen({
+                  serviceConfig: function() {
+                    var property = categoryConfigs.findProperty('name', 'oozie_database');
+                    property.set('value', test.serviceConfig.value);
+                    return property;
+                  }.property()
+                });
+                setProperties(categoryConfigs, test.setupProperties);
+                view.didInsertElement();
+              })
+              test.expectedProperties.forEach(function(property) {
+                it('#{0} should be "{1}"'.format(property.path, property.value), function() {
+                  expect(view.get(property.path)).to.eql(property.value);
+                });
+              });
+            });
+          });
+        })
+      });
+    });
+  });
+});