Updated Branches: refs/heads/trunk 7e73eba61 -> 93338ed9f
AMBARI-4268. Implement rolling restart dialog (srimanth) Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/93338ed9 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/93338ed9 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/93338ed9 Branch: refs/heads/trunk Commit: 93338ed9fd80bc996f8251abba4b429d2e0be213 Parents: 7e73eba Author: Srimanth Gunturi <[email protected]> Authored: Fri Jan 10 13:36:02 2014 -0800 Committer: Srimanth Gunturi <[email protected]> Committed: Fri Jan 10 13:36:02 2014 -0800 ---------------------------------------------------------------------- ambari-web/app/controllers/main/service/item.js | 8 + ambari-web/app/messages.js | 21 +++ ambari-web/app/styles/application.less | 27 +++ .../templates/common/rolling_restart_view.hbs | 62 +++++++ ambari-web/app/utils/ajax.js | 53 ++++++ .../app/utils/batch_scheduled_requests.js | 167 +++++++++++++++++++ ambari-web/app/views.js | 1 + .../app/views/common/rolling_restart_view.js | 125 ++++++++++++++ ambari-web/app/views/main/service/item.js | 10 +- 9 files changed, 473 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/93338ed9/ambari-web/app/controllers/main/service/item.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/controllers/main/service/item.js b/ambari-web/app/controllers/main/service/item.js index 325e738..72cec23 100644 --- a/ambari-web/app/controllers/main/service/item.js +++ b/ambari-web/app/controllers/main/service/item.js @@ -18,6 +18,7 @@ var App = require('app'); var service_components = require('data/service_components'); +var batchUtils = require('utils/batch_scheduled_requests'); App.MainServiceItemController = Em.Controller.extend({ name: 'mainServiceItemController', @@ -186,6 +187,13 @@ App.MainServiceItemController = Em.Controller.extend({ }); }, + restartAllHostComponents: function(event) { + }, + + rollingRestart: function(hostComponentName) { + batchUtils.launchHostComponentRollingRestart(hostComponentName, false); + }, + runSmokeTestPrimary: function() { App.ajax.send({ 'name': 'service.item.smoke', http://git-wip-us.apache.org/repos/asf/ambari/blob/93338ed9/ambari-web/app/messages.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js index 9b1d6bf..ea1500d 100644 --- a/ambari-web/app/messages.js +++ b/ambari-web/app/messages.js @@ -1759,6 +1759,27 @@ Em.I18n.translations = { 'mirroring.dateOrder.error': 'End Date must be after Start Date', 'mirroring.required.invalidNumberError' : 'Enter valid number', + 'rollingrestart.dialog.title': 'Rolling Restart {0}s', + 'rollingrestart.dialog.primary': 'Trigger Restart', + 'rollingrestart.notsupported.hostComponent': 'Rolling restart not supported for {0} components', + 'rollingrestart.dialog.msg.restart': 'This will restart {0} {1} in batches to keep your cluster operational and minimize downtime.', + 'rollingrestart.dialog.msg.restart.plural': 'This will restart {0} {1}s in batches to keep your cluster operational and minimize downtime.', + 'rollingrestart.dialog.msg.noRestartHosts': 'There are no {0}s to do rolling restarts', + 'rollingrestart.dialog.msg.maintainance': 'Note: {0} {1} in Maintainance Mode will not be restarted', + 'rollingrestart.dialog.msg.maintainance.plural': 'Note: {0} {1}s in Maintainance Mode will not be restarted', + 'rollingrestart.dialog.msg.componentsAtATime': '{0}s at a time', + 'rollingrestart.dialog.msg.timegap.prefix': 'Wait ', + 'rollingrestart.dialog.msg.timegap.suffix': 'seconds between batches ', + 'rollingrestart.dialog.msg.toleration.prefix': 'Tolerate up to ', + 'rollingrestart.dialog.msg.toleration.suffix': 'failures', + 'rollingrestart.dialog.err.empty.batchsize': 'Restart batch size cannot be empty', + 'rollingrestart.dialog.err.empty.waittime': 'Wait interval cannot be empty', + 'rollingrestart.dialog.err.empty.tolerate': 'Failure toleration size cannot be empty', + 'rollingrestart.dialog.err.invalid.batchsize': 'Restart batch size should be between 1 and {0}', + 'rollingrestart.dialog.err.invalid.waitTime': 'Wait time cannot be negative', + 'rollingrestart.dialog.err.invalid.toleratesize': 'Failure toleration cannot be negative', + 'rollingrestart.dialog.msg.staleConfigsOnly': 'Only restart {0}s with stale configs', + 'rollingrestart.rest.context': 'Rolling Restart of {0}s - batch {1} of {2}', 'menu.item.dashboard':'Dashboard', 'menu.item.heatmaps':'Heatmaps', http://git-wip-us.apache.org/repos/asf/ambari/blob/93338ed9/ambari-web/app/styles/application.less ---------------------------------------------------------------------- diff --git a/ambari-web/app/styles/application.less b/ambari-web/app/styles/application.less index ab97c5e..e919a43 100644 --- a/ambari-web/app/styles/application.less +++ b/ambari-web/app/styles/application.less @@ -5172,3 +5172,30 @@ i.icon-asterisks { .margin-bottom-5 { margin-bottom: 5px; } + +.rolling-restart-view { + table { + td:first-of-type { + width: 30%; + text-align: right; + } + td:nth-of-type(2) { + input { + margin-left: 7px; + margin-right: 5px; + } + } + td:last-of-type { + width: 60%; + text-align: left; + } + tr:last-of-type { + td { + text-align: center; + input { + vertical-align: top; + } + } + } + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/93338ed9/ambari-web/app/templates/common/rolling_restart_view.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/common/rolling_restart_view.hbs b/ambari-web/app/templates/common/rolling_restart_view.hbs new file mode 100644 index 0000000..8b38044 --- /dev/null +++ b/ambari-web/app/templates/common/rolling_restart_view.hbs @@ -0,0 +1,62 @@ +{{! +* 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="rolling-restart-view"> + <div class="alert alert-info"> + <p> + {{view.restartMessage}} + </p> + {{#if view.maintainanceMessage}} + <p> + {{view.maintainanceMessage}} + </p> + {{/if}} + </div> + <table> + <tr> + <td>{{t common.restart}}</td> + <td>{{view Ember.TextField valueBinding="view.batchSize" class="span1"}}</td> + <td>{{view.batchSizeMessage}}</td> + </tr> + <tr> + <td>{{t rollingrestart.dialog.msg.timegap.prefix}}</td> + <td>{{view Ember.TextField valueBinding="view.interBatchWaitTimeSeconds" class="span1"}}</td> + <td>{{t rollingrestart.dialog.msg.timegap.suffix}}</td> + </tr> + <tr> + <td>{{t rollingrestart.dialog.msg.toleration.prefix}}</td> + <td>{{view Ember.TextField valueBinding="view.tolerateSize" class="span1"}}</td> + <td>{{t rollingrestart.dialog.msg.toleration.suffix}}</td> + </tr> + <tr> + <td colspan="3"> + {{view Ember.Checkbox checkedBinding="view.staleConfigsOnly"}} + {{view.staleConfigsOnlyMessage}} + </td> + </tr> + </table> + {{#if view.errors}} + <div class="alert alert-warn"> + <ul> + {{#each error in view.errors}} + <li>{{error}}</li> + {{/each}} + </ul> + </div> + {{/if}} +</div> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/93338ed9/ambari-web/app/utils/ajax.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/utils/ajax.js b/ambari-web/app/utils/ajax.js index 992f12d..e690b67 100644 --- a/ambari-web/app/utils/ajax.js +++ b/ambari-web/app/utils/ajax.js @@ -1269,6 +1269,59 @@ var urls = { data: JSON.stringify(data.data) } } + }, + 'rolling_restart.post': { + 'real': '/clusters/{clusterName}/request_schedules', + 'mock': '', + 'format' : function(data) { + var hostIndex = 0; + var batchSize = data.batchSize; + var hostComponents = data.restartHostComponents; + var length = hostComponents.length; + var batches = []; + var batchCount = Math.ceil(length / batchSize); + var sampleHostComponent = hostComponents.objectAt(0); + var componentName = sampleHostComponent.get('componentName'); + var componentDisplayName = App.format.role(componentName); + var serviceName = sampleHostComponent.get('service.serviceName'); + for ( var count = 0; count < batchCount; count++) { + var hostNames = []; + for ( var hc = 0; hc < batchSize && hostIndex < length; hc++) { + hostNames.push(hostComponents.objectAt(hostIndex++).get('host.hostName')); + } + if (hostNames.length > 0) { + batches.push({ + "order_id" : count + 1, + "type" : "POST", + "uri" : "/api/v1/clusters/" + data.clusterName + "/requests", + "RequestBodyInfo" : { + "RequestInfo" : { + "context" : Em.I18n.t('rollingrestart.rest.context').format(componentDisplayName, (count + 1), batchCount), + "command" : "RESTART", + "service_name" : serviceName, + "component_name" : componentName, + "hosts" : hostNames.join(",") + } + } + }); + } + } + return { + type : 'POST', + data : JSON.stringify([ { + "RequestSchedule" : { + "batch" : [ { + "requests" : batches + }, { + "batch_settings" : { + "batch_separation_in_seconds" : data.intervalTimeSeconds, + "task_failure_tolerance" : data.tolerateSize + } + } ] + } + } ]) + } + } } }; /** http://git-wip-us.apache.org/repos/asf/ambari/blob/93338ed9/ambari-web/app/utils/batch_scheduled_requests.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/utils/batch_scheduled_requests.js b/ambari-web/app/utils/batch_scheduled_requests.js new file mode 100644 index 0000000..2f8ae78 --- /dev/null +++ b/ambari-web/app/utils/batch_scheduled_requests.js @@ -0,0 +1,167 @@ +/** + * 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. + */ + +/** + * Contains helpful utilities for handling batch and scheduled requests. + */ +module.exports = { + + /** + * Some services have components which have a need for rolling restarts. This + * method returns the name of the host-component which supports rolling + * restarts for a service. + */ + getRollingRestartComponentName : function(serviceName) { + var rollingRestartComponent = null; + switch (serviceName) { + case 'HDFS': + rollingRestartComponent = 'DATANODE'; + break; + case 'YARN': + rollingRestartComponent = 'NODEMANAGER'; + break; + case 'MAPREDUCE': + rollingRestartComponent = 'TASKTRACKER'; + break; + case 'HBASE': + rollingRestartComponent = 'HBASE_REGIONSERVER'; + break; + case 'STORM': + rollingRestartComponent = 'SUPERVISOR'; + break; + default: + break; + } + return rollingRestartComponent; + }, + + /** + * Makes a REST call to the server requesting the rolling restart of the + * provided host components. + */ + doPostBatchRollingRestartRequest : function(restartHostComponents, batchSize, intervalTimeSeconds, tolerateSize, successCallback, errorCallback) { + var clusterName = App.get('clusterName'); + var data = { + restartHostComponents : restartHostComponents, + batchSize : batchSize, + intervalTimeSeconds : intervalTimeSeconds, + tolerateSize : tolerateSize, + clusterName : clusterName + } + var sender = { + successFunction : function() { + successCallback(); + }, + errorFunction : function(xhr, textStatus, error, opt) { + errorCallback(xhr, textStatus, error, opt); + } + } + App.ajax.send({ + name : 'rolling_restart.post', + sender : sender, + data : data, + success : 'successFunction', + error : 'errorFunction' + }); + }, + + /** + * Launches dialog to handle rolling restarts of host components. + * + * Rolling restart is supported for the following host components only + * <ul> + * <li>Data Nodes (HDFS) + * <li>Task Trackers (MapReduce) + * <li>Node Managers (YARN) + * <li>Region Servers (HBase) + * <li>Supervisors (Storm) + * </ul> + * + * @param {String} + * hostComponentName Type of host-component to restart across cluster + * (ex: DATANODE) + * @param {Boolean} + * staleConfigsOnly Pre-select host-components which have stale + * configurations + */ + launchHostComponentRollingRestart : function(hostComponentName, staleConfigsOnly) { + var componentDisplayName = App.format.role(hostComponentName); + if (!componentDisplayName) { + componentDisplayName = hostComponentName; + } + var self = this; + var title = Em.I18n.t('rollingrestart.dialog.title').format(componentDisplayName) + if (hostComponentName == "DATANODE" || hostComponentName == "TASKTRACKER" || hostComponentName == "NODEMANAGER" + || hostComponentName == "HBASE_REGIONSERVER" || hostComponentName == "SUPERVISOR") { + App.ModalPopup.show({ + header : title, + hostComponentName : hostComponentName, + staleConfigsOnly : staleConfigsOnly, + innerView : null, + bodyClass : App.RollingRestartView.extend({ + hostComponentName : hostComponentName, + staleConfigsOnly : staleConfigsOnly, + didInsertElement : function() { + this.set('parentView.innerView', this); + this.initialize(); + } + }), + classNames : [ 'rolling-restart-popup' ], + primary : Em.I18n.t('rollingrestart.dialog.primary'), + secondary : Em.I18n.t('common.cancel'), + onPrimary : function() { + var dialog = this; + if (!dialog.get('enablePrimary')) { + return false; + } + var restartComponents = this.get('innerView.restartHostComponents'); + var batchSize = this.get('innerView.batchSize'); + var waitTime = this.get('innerView.interBatchWaitTimeSeconds'); + var tolerateSize = this.get('innerView.tolerateSize'); + self.doPostBatchRollingRestartRequest(restartComponents, batchSize, waitTime, tolerateSize, function() { + dialog.hide(); + }, function(xhr, textStatus, error, opt) { + App.ajax.defaultErrorHandler(xhr, opt.url, 'POST', xhr.status); + }); + return; + }, + onSecondary : function() { + this.hide(); + }, + onClose : function() { + this.hide(); + }, + updateButtons : function() { + var errors = this.get('innerView.errors'); + this.set('enablePrimary', !(errors != null && errors.length > 0)) + }.observes('innerView.errors'), + }); + } else { + var msg = Em.I18n.t('rollingrestart.notsupported.hostComponent').format(componentDisplayName); + console.log(msg); + App.ModalPopup.show({ + header : title, + secondary : false, + msg : msg, + bodyClass : Ember.View.extend({ + template : Ember.Handlebars.compile('<div class="alert alert-warning">{{msg}}</div>'), + msg : msg + }) + }); + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/93338ed9/ambari-web/app/views.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views.js b/ambari-web/app/views.js index 9a9b36a..ed21192 100644 --- a/ambari-web/app/views.js +++ b/ambari-web/app/views.js @@ -25,6 +25,7 @@ require('views/common/chart/pie'); require('views/common/chart/linear'); require('views/common/chart/linear_time'); require('views/common/modal_popup'); +require('views/common/rolling_restart_view'); require('views/common/metric'); require('views/common/time_range'); require('views/common/form/field'); http://git-wip-us.apache.org/repos/asf/ambari/blob/93338ed9/ambari-web/app/views/common/rolling_restart_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/rolling_restart_view.js b/ambari-web/app/views/common/rolling_restart_view.js new file mode 100644 index 0000000..2671ddd --- /dev/null +++ b/ambari-web/app/views/common/rolling_restart_view.js @@ -0,0 +1,125 @@ +/** + * 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'); + +/** + * View content of the rolling restart dialog. + * + * Callers provide the context in which this dialog is invoked. + */ +App.RollingRestartView = Em.View.extend({ + templateName : require('templates/common/rolling_restart_view'), + hostComponentName : null, + staleConfigsOnly : false, + batchSize : -1, + interBatchWaitTimeSeconds : -1, + tolerateSize : -1, + errors : null, + initialize : function() { + if (this.get('batchSize') == -1 && this.get('interBatchWaitTimeSeconds') == -1 && this.get('tolerateSize') == -1) { + var restartCount = this.get('restartHostComponents'); + var batchSize = 1; + if (restartCount > 10) { + batchSize = Math.ceil(restartCount / 10); + } + var tolerateCount = batchSize; + this.set('batchSize', batchSize); + this.set('tolerateSize', tolerateCount); + this.set('interBatchWaitTimeSeconds', 120); + } + }, + validate : function() { + var displayName = this.get('hostComponentDisplayName'); + var totalCount = this.get('restartHostComponents.length'); + var bs = this.get('batchSize'); + var ts = this.get('tolerateSize'); + var wait = this.get('interBatchWaitTimeSeconds'); + var errors = []; + if (totalCount < 1) { + errors.push(Em.I18n.t('rollingrestart.dialog.msg.noRestartHosts').format(displayName)); + } else { + if (!bs) { + errors.push(Em.I18n.t('rollingrestart.dialog.err.empty.batchsize')); + } else if (bs > totalCount || bs < 0) { + errors.push(Em.I18n.t('rollingrestart.dialog.err.invalid.batchsize').format(totalCount)); + } + if (!ts) { + errors.push(Em.I18n.t('rollingrestart.dialog.err.empty.waittime')); + } else if (ts < 0) { + errors.push(Em.I18n.t('rollingrestart.dialog.err.invalid.toleratesize')); + } + } + if (!wait) { + errors.push(Em.I18n.t('rollingrestart.dialog.err.empty.tolerate')); + } else if (wait < 0) { + errors.push(Em.I18n.t('rollingrestart.dialog.err.invalid.waitTime')); + } + if (errors.length < 1) { + errors = null; + } + this.set('errors', errors); + }.observes('batchSize', 'interBatchWaitTimeSeconds', 'tolerateSize', 'restartHostComponents', 'hostComponentDisplayName'), + hostComponentDisplayName : function() { + return App.format.role(this.get('hostComponentName')); + }.property('hostComponentName'), + allHostComponents : function() { + return App.HostComponent.find().filterProperty('componentName', this.get('hostComponentName')); + }.property('hostComponentName'), + nonMaintainanceHostComponents : function() { + var hostComponents = this.get('allHostComponents'); + hostComponents = hostComponents.filter(function(item) { + if (item.get('workStatus') !== App.HostComponentStatus.maintenance) { + return true; + } + }); + return hostComponents; + }.property('allHostComponents', '[email protected]'), + restartHostComponents : function() { + var hostComponents = this.get('nonMaintainanceHostComponents'); + if (this.get('staleConfigsOnly')) { + hostComponents = hostComponents.filterProperty('staleConfigs', true); + } + return hostComponents; + }.property('nonMaintainanceHostComponents', 'staleConfigsOnly'), + restartMessage : function() { + var rhc = this.get('restartHostComponents.length'); + if (rhc > 1) { + return Em.I18n.t('rollingrestart.dialog.msg.restart.plural').format(rhc, this.get('hostComponentDisplayName')) + } + return Em.I18n.t('rollingrestart.dialog.msg.restart').format(rhc, this.get('hostComponentDisplayName')) + }.property('restartHostComponents', 'hostComponentDisplayName'), + maintainanceMessage : function() { + var allCount = this.get('allHostComponents.length'); + var nonMaintainCount = this.get('nonMaintainanceHostComponents.length'); + var count = allCount - nonMaintainCount; + if (count > 0) { + var name = this.get('hostComponentDisplayName'); + if (count > 1) { + return Em.I18n.t('rollingrestart.dialog.msg.maintainance.plural').format(count, name) + } + return Em.I18n.t('rollingrestart.dialog.msg.maintainance').format(count, name) + } + return null; + }.property('allHostComponents', 'nonMaintainanceHostComponents', 'hostComponentDisplayName'), + batchSizeMessage : function() { + return Em.I18n.t('rollingrestart.dialog.msg.componentsAtATime').format(this.get('hostComponentDisplayName')); + }.property('hostComponentDisplayName'), + staleConfigsOnlyMessage : function() { + return Em.I18n.t('rollingrestart.dialog.msg.staleConfigsOnly').format(this.get('hostComponentDisplayName')); + }.property('hostComponentDisplayName') +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/93338ed9/ambari-web/app/views/main/service/item.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/main/service/item.js b/ambari-web/app/views/main/service/item.js index fc1adcf..e67b303 100644 --- a/ambari-web/app/views/main/service/item.js +++ b/ambari-web/app/views/main/service/item.js @@ -17,6 +17,7 @@ */ var App = require('app'); +var batchUtils = require('utils/batch_scheduled_requests'); App.MainServiceItemView = Em.View.extend({ templateName: require('templates/main/service/item'), @@ -26,7 +27,8 @@ App.MainServiceItemView = Em.View.extend({ var hosts = App.Host.find().content.length; var allMasters = this.get('controller.content.hostComponents').filterProperty('isMaster').mapProperty('componentName').uniq(); var disabled = this.get('controller.isStopDisabled'); - switch (service.get('serviceName')) { + var serviceName = service.get('serviceName'); + switch (serviceName) { case 'GANGLIA': case 'NAGIOS': break; @@ -44,6 +46,12 @@ App.MainServiceItemView = Em.View.extend({ default: options.push({action: 'runSmokeTest', 'label': Em.I18n.t('services.service.actions.run.smoke').format(service.get('serviceName')), disabled:disabled}); } + + var rrComponentName = batchUtils.getRollingRestartComponentName(serviceName); + if (rrComponentName) { + var label = Em.I18n.t('rollingrestart.dialog.title').format(App.format.role(rrComponentName)); + options.push({action:'rollingRestart', context: rrComponentName, 'label': label, disabled: false}); + } return options; }.property('controller.content', 'controller.isStopDisabled'), isMaintenanceActive: function() {
