AMBARI-14934 Can't remove YARN or MapReduce2.(ababiichuk)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/315f502b Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/315f502b Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/315f502b Branch: refs/heads/branch-dev-patch-upgrade Commit: 315f502b0e7fed166f7d91387fa41f85e8dc891a Parents: 94932c9 Author: ababiichuk <ababiic...@hortonworks.com> Authored: Fri Feb 5 12:37:25 2016 +0200 Committer: ababiichuk <ababiic...@hortonworks.com> Committed: Fri Feb 5 12:37:25 2016 +0200 ---------------------------------------------------------------------- ambari-web/app/controllers/main/service/item.js | 107 ++++++++++++---- ambari-web/app/messages.js | 4 + ambari-web/app/models/stack_service.js | 2 +- ambari-web/app/utils/ajax/ajax.js | 5 - .../test/controllers/main/service/item_test.js | 122 +++++++++++++++---- 5 files changed, 189 insertions(+), 51 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/315f502b/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 a26c820..1aae7cd 100644 --- a/ambari-web/app/controllers/main/service/item.js +++ b/ambari-web/app/controllers/main/service/item.js @@ -966,31 +966,76 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow }, /** + * Returns interdependent services + * + * @param serviceName + * @returns {string[]} + */ + interDependentServices: function(serviceName) { + var interDependentServices = []; + App.StackService.find(serviceName).get('requiredServices').forEach(function(requiredService) { + if (App.StackService.find(requiredService).get('requiredServices').contains(serviceName)) { + interDependentServices.push(requiredService); + } + }); + return interDependentServices; + }, + + /** * find dependent services - * @param {string} serviceName + * @param {string[]} serviceNamesToDelete * @returns {Array} */ - findDependentServices: function(serviceName) { + findDependentServices: function (serviceNamesToDelete) { var dependentServices = []; - App.StackService.find().forEach(function(stackService) { - if (App.Service.find(stackService.get('serviceName')).get('isLoaded') - && stackService.get('requiredServices').contains(serviceName)) { - dependentServices.push(stackService.get('serviceName')); + App.Service.find().forEach(function (service) { + if (!serviceNamesToDelete.contains(service.get('serviceName'))) { + var requiredServices = App.StackService.find(service.get('serviceName')).get('requiredServices'); + serviceNamesToDelete.forEach(function (dependsOnService) { + if (requiredServices.contains(dependsOnService)) { + dependentServices.push(service.get('serviceName')); + } + }); } }, this); return dependentServices; }, /** + * @param serviceNames + * @returns {string} + */ + servicesDisplayNames: function(serviceNames) { + return serviceNames.map(function(serviceName) { + return App.format.role(serviceName); + }).join(','); + }, + + /** + * Is services can be removed based on work status + * @param serviceNames + */ + allowUninstallServices: function(serviceNames) { + return !App.Service.find().filter(function (service) { + return serviceNames.contains(service.get('serviceName')); + }).mapProperty('workStatus').some(function (workStatus) { + return !App.Service.allowUninstallStates.contains(workStatus); + }); + }, + + /** * delete service action * @param {string} serviceName */ deleteService: function(serviceName) { - var dependentServices = this.findDependentServices(serviceName), - self = this, - displayName = App.format.role(serviceName), - popupHeader = Em.I18n.t('services.service.delete.popup.header'); + var self = this, + interDependentServices = this.interDependentServices(serviceName), + serviceNamesToDelete = interDependentServices.concat(serviceName), + dependentServices = this.findDependentServices(serviceNamesToDelete), + displayName = App.format.role(serviceName), + popupHeader = Em.I18n.t('services.service.delete.popup.header'), + dependentServicesToDeleteFmt = this.servicesDisplayNames(interDependentServices); if (App.Service.find().get('length') === 1) { //at least one service should be installed @@ -1002,21 +1047,26 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow }); } else if (dependentServices.length > 0) { this.dependentServicesWarning(serviceName, dependentServices); - } else if (App.Service.allowUninstallStates.contains(App.Service.find(serviceName).get('workStatus'))) { + } else if (this.allowUninstallServices(serviceNamesToDelete)) { App.showConfirmationPopup( - function() {self.confirmDeleteService(serviceName)}, - Em.I18n.t('services.service.delete.popup.warning').format(displayName), + function() {self.confirmDeleteService(serviceName, interDependentServices, dependentServicesToDeleteFmt)}, + Em.I18n.t('services.service.delete.popup.warning').format(displayName) + + (interDependentServices.length ? Em.I18n.t('services.service.delete.popup.warning.dependent').format(dependentServicesToDeleteFmt) : ''), null, popupHeader, Em.I18n.t('common.delete'), true ); } else { + var body = Em.I18n.t('services.service.delete.popup.mustBeStopped').format(displayName); + if (interDependentServices.length) { + body += Em.I18n.t('services.service.delete.popup.mustBeStopped.dependent').format(dependentServicesToDeleteFmt) + } App.ModalPopup.show({ secondary: null, header: popupHeader, encodeBody: false, - body: Em.I18n.t('services.service.delete.popup.mustBeStopped').format(displayName) + body: body }); } }, @@ -1048,9 +1098,13 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow /** * Confirmation popup of service deletion * @param {string} serviceName + * @param {string[]} [dependentServiceNames] + * @param {string} [servicesToDeleteFmt] */ - confirmDeleteService: function (serviceName) { - var message = Em.I18n.t('services.service.confirmDelete.popup.body').format(App.format.role(serviceName)), + confirmDeleteService: function (serviceName, dependentServiceNames, servicesToDeleteFmt) { + var message = dependentServiceNames && dependentServiceNames.length + ? Em.I18n.t('services.service.confirmDelete.popup.body.dependent').format(App.format.role(serviceName), servicesToDeleteFmt) + : Em.I18n.t('services.service.confirmDelete.popup.body').format(App.format.role(serviceName)), confirmKey = 'yes', self = this; @@ -1060,7 +1114,7 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow * @function onPrimary */ onPrimary: function() { - self.deleteServiceCall(serviceName); + self.deleteServiceCall([serviceName].concat(dependentServiceNames)); this._super(); }, @@ -1105,22 +1159,31 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow /** * Ajax call to delete service - * @param {string} serviceName + * @param {string[]} serviceNames * @returns {$.ajax} */ - deleteServiceCall: function(serviceName) { + deleteServiceCall: function(serviceNames) { + var serviceToDeleteNow = serviceNames[0]; + if (serviceNames.length > 1) { + var servicesToDeleteNext = serviceNames.slice(1); + } return App.ajax.send({ - name : 'service.item.delete', + name : 'common.delete.service', sender: this, data : { - serviceName : serviceName + serviceName : serviceToDeleteNow, + servicesToDeleteNext: servicesToDeleteNext }, success : 'deleteServiceCallSuccessCallback' }); }, deleteServiceCallSuccessCallback: function(data, ajaxOptions, params) { - window.location.reload(); + if (params.servicesToDeleteNext) { + this.deleteServiceCall(params.servicesToDeleteNext); + } else { + window.location.reload(); + } } }); http://git-wip-us.apache.org/repos/asf/ambari/blob/315f502b/ambari-web/app/messages.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js index e341de4..f24876b 100644 --- a/ambari-web/app/messages.js +++ b/ambari-web/app/messages.js @@ -1693,12 +1693,16 @@ Em.I18n.translations = { 'services.service.delete.lastService.popup.body': 'The <b>{0}</b> service can\'t be deleted, at least one service must be installed.', 'services.service.delete.popup.dependentServices': 'Prior to deleting <b>{0}</b>, you must delete the following dependent services:', 'services.service.delete.popup.mustBeStopped': 'Prior to deleting <b>{0}</b>, you must stop the service.', + 'services.service.delete.popup.mustBeStopped.dependent': ' Along with dependent service <b>{0}</b>.', 'services.service.delete.popup.warning': 'The <b>{0} service will be removed from Ambari and all configurations' + ' and configuration history will be lost.</b>', + 'services.service.delete.popup.warning.dependent': '<b>Note! {0} will be deleted too.</b>', 'services.service.confirmDelete.popup.header': 'Confirm Delete', 'services.service.confirmDelete.popup.body': 'You must confirm delete of <b>{0}</b> by typing "yes"' + ' in the confirmation box. <b>This operation is not reversible and all configuration history will be lost.</b>', + 'services.service.confirmDelete.popup.body.dependent': 'You must confirm delete of <b>{0}</b> and <b>{1}</b> by typing "yes"' + + ' in the confirmation box. <b>This operation is not reversible and all configuration history will be lost.</b>', 'services.service.summary.unknown':'unknown', 'services.service.summary.notRunning':'Not Running', 'services.service.summary.notAvailable':'n/a', http://git-wip-us.apache.org/repos/asf/ambari/blob/315f502b/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 a7f5f4e..5814386 100644 --- a/ambari-web/app/models/stack_service.js +++ b/ambari-web/app/models/stack_service.js @@ -42,7 +42,7 @@ App.StackService = DS.Model.extend({ stack: DS.belongsTo('App.Stack'), serviceComponents: DS.hasMany('App.StackServiceComponent'), configs: DS.attr('array'), - requiredServices: DS.attr('array'), + requiredServices: DS.attr('array', {defaultValue: []}), /** * @type {String[]} http://git-wip-us.apache.org/repos/asf/ambari/blob/315f502b/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 423941f..8b89365 100644 --- a/ambari-web/app/utils/ajax/ajax.js +++ b/ambari-web/app/utils/ajax/ajax.js @@ -498,11 +498,6 @@ var urls = { }; } }, - 'service.item.delete': { - 'real': '/clusters/{clusterName}/services/{serviceName}', - 'mock': '', - 'type': 'DELETE' - }, 'service.item.smoke': { 'real': '/clusters/{clusterName}/requests', 'mock': '/data/wizard/deploy/poll_1.json', http://git-wip-us.apache.org/repos/asf/ambari/blob/315f502b/ambari-web/test/controllers/main/service/item_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/controllers/main/service/item_test.js b/ambari-web/test/controllers/main/service/item_test.js index 90a1d90..8fb772a 100644 --- a/ambari-web/test/controllers/main/service/item_test.js +++ b/ambari-web/test/controllers/main/service/item_test.js @@ -28,6 +28,28 @@ require('controllers/main/service/reassign_controller'); require('controllers/main/service/item'); var batchUtils = require('utils/batch_scheduled_requests'); var testHelpers = require('test/helpers'); +var stackSerivceModel = { + 'HDFS': Em.Object.create({ + serviceName: 'HDFS', + requiredServices: ['ZOOKEEPER'] + }), + 'YARN': Em.Object.create({ + serviceName: 'YARN', + requiredServices: ['MAPREDUCE2', 'HDFS'] + }), + 'MAPREDUCE2': Em.Object.create({ + serviceName: 'MAPREDUCE2', + requiredServices: ['YARN'] + }), + 'TEZ': Em.Object.create({ + serviceName: 'TEZ', + requiredServices: ['YARN'] + }), + 'HIVE': Em.Object.create({ + serviceName: 'HIVE', + requiredServices: ['YARN', 'TEZ'] + }) +}; describe('App.MainServiceItemController', function () { @@ -1182,35 +1204,50 @@ describe('App.MainServiceItemController', function () { beforeEach(function() { mainServiceItemController = App.MainServiceItemController.create({}); - this.mockStackService = sinon.stub(App.StackService, 'find'); + sinon.stub(App.StackService, 'find', function (serviceName) { + return stackSerivceModel[serviceName]; + }); this.mockService = sinon.stub(App.Service, 'find'); }); afterEach(function() { - this.mockStackService.restore(); + App.StackService.find.restore(); this.mockService.restore(); }); - it("no stack services", function() { - this.mockStackService.returns([]); - expect(mainServiceItemController.findDependentServices('S1')).to.be.empty; + it("no services", function() { + this.mockService.returns([]); + expect(mainServiceItemController.findDependentServices(['S1'])).to.be.empty; }); - it("dependent service not installed", function() { - this.mockStackService.returns([Em.Object.create({serviceName: 'S2'})]); - this.mockService.withArgs('S2').returns(Em.Object.create({isLoaded: false})); - expect(mainServiceItemController.findDependentServices('S1')).to.be.empty; + it("service has dependencies", function() { + this.mockService.returns([ + Em.Object.create({ serviceName: 'HDFS' }), + Em.Object.create({ serviceName: 'YARN' }), + Em.Object.create({ serviceName: 'MAPREDUCE2' }), + Em.Object.create({ serviceName: 'TEZ' }), + Em.Object.create({ serviceName: 'HIVE' }) + ]); + expect(mainServiceItemController.findDependentServices(['YARN', 'MAPREDUCE2'])).to.eql(['TEZ', 'HIVE']); }); it("service has no dependencies", function() { - this.mockStackService.returns([Em.Object.create({serviceName: 'S2', requiredServices: []})]); - this.mockService.withArgs('S2').returns(Em.Object.create({isLoaded: true})); - expect(mainServiceItemController.findDependentServices('S1')).to.be.empty; + this.mockService.returns([ + Em.Object.create({ serviceName: 'HDFS' }), + Em.Object.create({ serviceName: 'YARN' }), + Em.Object.create({ serviceName: 'MAPREDUCE2' }), + Em.Object.create({ serviceName: 'TEZ' }), + Em.Object.create({ serviceName: 'HIVE' }) + ]); + expect(mainServiceItemController.findDependentServices(['HIVE'])).to.be.empty; }); - it("service has dependencies", function() { - this.mockStackService.returns([Em.Object.create({serviceName: 'S2', requiredServices: ['S1']})]); - this.mockService.withArgs('S2').returns(Em.Object.create({isLoaded: true})); - expect(mainServiceItemController.findDependentServices('S1')).to.eql(['S2']); + it("service has no dependencies (except interdependent)", function() { + this.mockService.returns([ + Em.Object.create({ serviceName: 'HDFS' }), + Em.Object.create({ serviceName: 'YARN' }), + Em.Object.create({ serviceName: 'MAPREDUCE2' }) + ]); + expect(mainServiceItemController.findDependentServices(['YARN', 'MAPREDUCE2'])).to.be.empty; }); }); @@ -1222,12 +1259,20 @@ describe('App.MainServiceItemController', function () { mainServiceItemController = App.MainServiceItemController.create({}); this.mockDependentServices = sinon.stub(mainServiceItemController, 'findDependentServices'); sinon.stub(mainServiceItemController, 'dependentServicesWarning'); + sinon.stub(mainServiceItemController, 'servicesDisplayNames', function(servicesDisplayNames) { + return servicesDisplayNames; + }); + sinon.stub(mainServiceItemController, 'interDependentServices').returns([]); + this.allowUninstallServices = sinon.stub(mainServiceItemController, 'allowUninstallServices'); this.mockService = sinon.stub(App.Service, 'find'); sinon.stub(App, 'showConfirmationPopup'); sinon.stub(App.ModalPopup, 'show'); sinon.stub(App.format, 'role', function(name) {return name}); }); afterEach(function() { + mainServiceItemController.allowUninstallServices.restore(); + mainServiceItemController.interDependentServices.restore(); + mainServiceItemController.servicesDisplayNames.restore(); this.mockDependentServices.restore(); this.mockService.restore(); mainServiceItemController.dependentServicesWarning.restore(); @@ -1250,21 +1295,23 @@ describe('App.MainServiceItemController', function () { it("service has installed dependent services", function() { this.mockDependentServices.returns(['S2']); - this.mockService.returns(Em.Object.create({workStatus: App.Service.statesMap.stopped})); + this.mockService.returns([Em.Object.create({workStatus: App.Service.statesMap.stopped}), Em.Object.create({workStatus: App.Service.statesMap.stopped})]); mainServiceItemController.deleteService('S1'); expect(mainServiceItemController.dependentServicesWarning.calledWith('S1', ['S2'])).to.be.true; }); it("service has not dependent services, and stopped", function() { this.mockDependentServices.returns([]); - this.mockService.returns(Em.Object.create({workStatus: App.Service.statesMap.stopped})); + this.allowUninstallServices.returns(true); + this.mockService.returns([Em.Object.create({workStatus: App.Service.statesMap.stopped}), Em.Object.create({workStatus: App.Service.statesMap.stopped})]); mainServiceItemController.deleteService('S1'); expect(App.showConfirmationPopup.calledOnce).to.be.true; }); it("service has not dependent services, and install failed", function() { this.mockDependentServices.returns([]); - this.mockService.returns(Em.Object.create({workStatus: App.Service.statesMap.install_failed})); + this.allowUninstallServices.returns(true); + this.mockService.returns([Em.Object.create({workStatus: App.Service.statesMap.install_failed}), Em.Object.create({workStatus: App.Service.statesMap.install_failed})]); mainServiceItemController.deleteService('S1'); expect(App.showConfirmationPopup.calledOnce).to.be.true; }); @@ -1318,6 +1365,26 @@ describe('App.MainServiceItemController', function () { }); }); + describe('#interDependentServices', function() { + var mainServiceItemController; + + beforeEach(function() { + sinon.stub(App.StackService, 'find', function (serviceName) { + return stackSerivceModel[serviceName]; + }); + mainServiceItemController = App.MainServiceItemController.create({}); + }); + + afterEach(function() { + App.StackService.find.restore(); + }); + + it('get interdependent services', function() { + expect(mainServiceItemController.interDependentServices('YARN')).to.eql(['MAPREDUCE2']); + expect(mainServiceItemController.interDependentServices('MAPREDUCE2')).to.eql(['YARN']); + }); + }); + describe("#deleteServiceCall()", function() { var mainServiceItemController; @@ -1326,12 +1393,13 @@ describe('App.MainServiceItemController', function () { }); it("App.ajax.send should be called", function() { - mainServiceItemController.deleteServiceCall('S1'); - var args = testHelpers.findAjaxRequest('name', 'service.item.delete'); + mainServiceItemController.deleteServiceCall(['S1', 'S2']); + var args = testHelpers.findAjaxRequest('name', 'common.delete.service'); expect(args[0]).exists; expect(args[0].sender).to.be.eql(mainServiceItemController); expect(args[0].data).to.be.eql({ - serviceName : 'S1' + serviceName : 'S1', + servicesToDeleteNext: ['S2'] }); }); }); @@ -1342,15 +1410,23 @@ describe('App.MainServiceItemController', function () { beforeEach(function() { mainServiceItemController = App.MainServiceItemController.create({}); sinon.stub(window.location, 'reload'); + sinon.spy(mainServiceItemController, 'deleteServiceCall'); }); afterEach(function() { window.location.reload.restore(); }); it("window.location.reload should be called", function() { - mainServiceItemController.deleteServiceCallSuccessCallback(); + mainServiceItemController.deleteServiceCallSuccessCallback([], null, {}); + expect(mainServiceItemController.deleteServiceCall.called).to.be.false; expect(window.location.reload.calledOnce).to.be.true; }); + + it("deleteServiceCall should be called", function() { + mainServiceItemController.deleteServiceCallSuccessCallback([], null, {servicesToDeleteNext: true}); + expect(mainServiceItemController.deleteServiceCall.calledOnce).to.be.true; + expect(window.location.reload.called).to.be.false; + }); }); });