You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by at...@apache.org on 2017/06/19 12:57:38 UTC

ambari git commit: AMBARI-21278 Integrate cluster topology updates with websocket events. (atkach)

Repository: ambari
Updated Branches:
  refs/heads/branch-3.0-perf 94fed5503 -> d986503ca


AMBARI-21278 Integrate cluster topology updates with websocket events. (atkach)


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

Branch: refs/heads/branch-3.0-perf
Commit: d986503ca0fb4301e5b01595299162da3453f41c
Parents: 94fed55
Author: Andrii Tkach <at...@apache.org>
Authored: Mon Jun 19 15:25:44 2017 +0300
Committer: Andrii Tkach <at...@apache.org>
Committed: Mon Jun 19 15:25:44 2017 +0300

----------------------------------------------------------------------
 ambari-web/app/app.js                           |   1 +
 ambari-web/app/assets/test/tests.js             |   1 +
 .../controllers/global/cluster_controller.js    |   1 +
 .../app/controllers/global/update_controller.js |   3 +
 ambari-web/app/controllers/main/host/details.js |  14 --
 ambari-web/app/mappers.js                       |   3 +-
 .../app/mappers/socket/topology_mapper.js       | 163 +++++++++++++++
 ambari-web/app/models/host_component.js         |   9 +
 ambari-web/app/utils/ajax/ajax.js               |   4 +-
 ambari-web/app/views/main/menu.js               |   2 +-
 .../global/cluster_controller_test.js           |   6 +-
 .../global/update_controller_test.js            |   2 +
 .../test/controllers/main/host/details_test.js  |  45 -----
 .../test/mappers/socket/topology_mapper_test.js | 201 +++++++++++++++++++
 14 files changed, 391 insertions(+), 64 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/app/app.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/app.js b/ambari-web/app/app.js
index c667992..8d5dd58 100644
--- a/ambari-web/app/app.js
+++ b/ambari-web/app/app.js
@@ -180,6 +180,7 @@ module.exports = Em.Application.create({
     return false;
   }.property('router.clusterController.isLoaded'),
 
+  clusterId: null,
   clusterName: null,
   clockDistance: null, // server clock - client clock
   currentStackVersion: '',

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/app/assets/test/tests.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/test/tests.js b/ambari-web/app/assets/test/tests.js
index 23d9f66..567539b 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -167,6 +167,7 @@ var files = [
   'test/mappers/configs/service_config_version_mapper_test',
   'test/mappers/configs/themes_mapper_test',
   'test/mappers/socket_events_mapper_test',
+  'test/mappers/socket/topology_mapper_test',
   'test/mixins/common/configs/enhanced_configs_test',
   'test/mixins/common/configs/config_recommendations_test',
   'test/mixins/common/configs/config_recommendation_parser_test',

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/app/controllers/global/cluster_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/global/cluster_controller.js b/ambari-web/app/controllers/global/cluster_controller.js
index 29c979e..b13cc8b 100644
--- a/ambari-web/app/controllers/global/cluster_controller.js
+++ b/ambari-web/app/controllers/global/cluster_controller.js
@@ -134,6 +134,7 @@ App.ClusterController = Em.Controller.extend(App.ReloadPopupMixin, {
     this._super();
     if (data.items && data.items.length > 0) {
       App.setProperties({
+        clusterId: data.items[0].Clusters.cluster_id,
         clusterName: data.items[0].Clusters.cluster_name,
         currentStackVersion: data.items[0].Clusters.version,
         isKerberosEnabled: data.items[0].Clusters.security_type === 'KERBEROS'

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/app/controllers/global/update_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/global/update_controller.js b/ambari-web/app/controllers/global/update_controller.js
index e7ed53f..d9c882f 100644
--- a/ambari-web/app/controllers/global/update_controller.js
+++ b/ambari-web/app/controllers/global/update_controller.js
@@ -191,6 +191,7 @@ App.UpdateController = Em.Controller.extend({
       //TODO limit updates by location
       App.StompClient.subscribe('/events/hostcomponents', socket.applyHostComponentStatusEvents.bind(socket));
       App.StompClient.subscribe('/events/alerts', socket.applyAlertDefinitionSummaryEvents.bind(socket));
+      App.StompClient.subscribe('/events/topologies', App.topologyMapper.map.bind(App.topologyMapper));
 
       App.updater.run(this, 'updateServices', 'isWorking');
       App.updater.run(this, 'updateHost', 'isWorking');
@@ -210,6 +211,7 @@ App.UpdateController = Em.Controller.extend({
     } else {
       App.StompClient.unsubscribe('/events/hostcomponents');
       App.StompClient.unsubscribe('/events/alerts');
+      App.StompClient.unsubscribe('/events/topologies');
     }
   }.observes('isWorking', 'App.router.mainAlertInstancesController.isUpdating'),
 
@@ -581,6 +583,7 @@ App.UpdateController = Em.Controller.extend({
   },
 
   updateAlertDefinitionSummary: function(callback) {
+    //TODO move to clusterController
     var testUrl = '/data/alerts/alert_summary.json';
     var realUrl = '/alerts?format=groupedSummary';
     var url = this.getUrl(testUrl, realUrl);

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/app/controllers/main/host/details.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/host/details.js b/ambari-web/app/controllers/main/host/details.js
index def75d6..aba8ed5 100644
--- a/ambari-web/app/controllers/main/host/details.js
+++ b/ambari-web/app/controllers/main/host/details.js
@@ -630,7 +630,6 @@ App.MainHostDetailsController = Em.Controller.extend(App.SupportClientConfigsDow
    */
   _doDeleteHostComponentSuccessCallback: function (response, request, data) {
     this.set('_deletedHostComponentResult', null);
-    this.removeHostComponentModel(data.componentName, data.hostName);
   },
 
   /**
@@ -645,19 +644,6 @@ App.MainHostDetailsController = Em.Controller.extend(App.SupportClientConfigsDow
   },
 
   /**
-   * Remove host component data from App.HostComponent model.
-   *
-   * @param {String} componentName
-   * @param {String} hostName
-   */
-  removeHostComponentModel: function (componentName, hostName) {
-    var component = App.HostComponent.find().filterProperty('componentName', componentName).findProperty('hostName', hostName);
-    var serviceInCache = App.cache['services'].findProperty('ServiceInfo.service_name', component.get('service.serviceName'));
-    serviceInCache.host_components = serviceInCache.host_components.without(component.get('id'));
-    App.serviceMapper.deleteRecord(component);
-  },
-
-  /**
    * Send command to server to upgrade selected host component
    * @param {object} event
    * @method upgradeComponent

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/app/mappers.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mappers.js b/ambari-web/app/mappers.js
index d77c834..6cba547 100644
--- a/ambari-web/app/mappers.js
+++ b/ambari-web/app/mappers.js
@@ -45,4 +45,5 @@ require('mappers/root_service_mapper');
 require('mappers/widget_mapper');
 require('mappers/widget_layout_mapper');
 require('mappers/stack_upgrade_history_mapper');
-require('mappers/socket_events_mapper');
\ No newline at end of file
+require('mappers/socket_events_mapper');
+require('mappers/socket/topology_mapper');

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/app/mappers/socket/topology_mapper.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mappers/socket/topology_mapper.js b/ambari-web/app/mappers/socket/topology_mapper.js
new file mode 100644
index 0000000..d46fa5e
--- /dev/null
+++ b/ambari-web/app/mappers/socket/topology_mapper.js
@@ -0,0 +1,163 @@
+/**
+ * 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');
+var stringUtils = require('utils/string_utils');
+
+App.topologyMapper = App.QuickDataMapper.create({
+
+  /**
+   *
+   * @param {object} event
+   */
+  map: function(event) {
+    if (event.clusters[App.get('clusterId')].components) {
+      this.applyComponentTopologyChanges(event.clusters[App.get('clusterId')].components, event.eventType);
+    }
+    if (event.clusters[App.get('clusterId')].hosts) {
+      //TODO test hosts add/remove when hosts registration will be fixed
+      App.router.get('updateController').updateHost(Em.K, null, true);
+    }
+  },
+
+  /**
+   *
+   * @param {Array} components
+   * @param {string} eventType
+   */
+  applyComponentTopologyChanges: function(components, eventType) {
+    components.forEach((component) => {
+      component.hostNames.forEach((hostName, index) => {
+        if (eventType === 'UPDATE' && component.version === 'UNKNOWN') {
+          this.addServiceIfNew(component.serviceName);
+          this.createHostComponent(component, hostName, component.publicHostNames[index]);
+        } else if (eventType === 'DELETE') {
+          this.deleteHostComponent(component, hostName);
+          this.deleteServiceIfHasNoComponents(component.serviceName);
+        }
+      });
+    });
+  },
+
+  /**
+   *
+   * @param {string} serviceName
+   */
+  addServiceIfNew: function(serviceName) {
+    if (!App.Service.find(serviceName).get('isLoaded')) {
+      App.store.safeLoad(App.Service, {
+        id: serviceName,
+        service_name: serviceName,
+        work_status: 'INIT',
+        passive_state: 'OFF',
+        host_components: []
+      });
+    }
+  },
+
+  /**
+   *
+   * @param {string} serviceName
+   */
+  deleteServiceIfHasNoComponents: function(serviceName) {
+    if (App.Service.find(serviceName).get('isLoaded') &&
+        App.Service.find(serviceName).get('hostComponents.length') === 0) {
+      this.deleteRecord(App.Service.find(serviceName));
+    }
+  },
+
+  /**
+   *
+   * @param {object} component
+   * @param {string} hostName
+   */
+  deleteHostComponent: function(component, hostName) {
+    const id = App.HostComponent.getId(component.componentName, hostName);
+    if (!App.HostComponent.find(id).get('isLoaded')) {
+      //App.HostComponent does not contain all host-components of cluster
+      return;
+    }
+    this.deleteRecord(App.HostComponent.find(id));
+    const host = App.Host.find(hostName);
+    this.updateHostComponentsOfHost(host, host.get('hostComponents').rejectProperty('id', id).mapProperty('id'));
+
+    const service = App.Service.find(component.serviceName);
+    this.updateHostComponentsOfService(service, service.get('hostComponents').rejectProperty('id', id).mapProperty('id'));
+  },
+
+  /**
+   *
+   * @param {object} component
+   * @param {string} hostName
+   * @param {string} publicHostName
+   */
+  createHostComponent: function(component, hostName, publicHostName) {
+    const id = App.HostComponent.getId(component.componentName, hostName);
+    if (!App.Host.find(hostName).get('isLoaded')) {
+      //App.Host does not contain all hosts of cluster
+      return;
+    }
+    App.store.safeLoad(App.HostComponent, {
+      id: id,
+      host_id: hostName,
+      display_name: component.displayName,
+      service_id: component.serviceName,
+      host_name: hostName,
+      passive_state: 'OFF',
+      work_status: 'INIT',
+      public_host_name: publicHostName,
+      component_name: component.componentName
+    });
+    const host = App.Host.find(hostName);
+    this.updateHostComponentsOfHost(host, host.get('hostComponents').mapProperty('id').concat(id));
+
+    const service = App.Service.find(component.serviceName);
+    this.updateHostComponentsOfService(service, service.get('hostComponents').mapProperty('id').concat(id));
+  },
+
+  /**
+   *
+   * @param {Em.Object} host
+   * @param {Array} hostComponents
+   */
+  updateHostComponentsOfHost: function(host, hostComponents) {
+    const updatedHost = {};
+    for (let i in App.hostsMapper.config) {
+      if (host.get(stringUtils.underScoreToCamelCase(i)) !== undefined) {
+        updatedHost[i] = host.get(stringUtils.underScoreToCamelCase(i));
+      }
+    }
+    updatedHost.host_components = hostComponents;
+    App.store.safeLoad(App.Host, updatedHost);
+  },
+
+  /**
+   *
+   * @param {Em.Object} service
+   * @param {Array} hostComponents
+   */
+  updateHostComponentsOfService: function(service, hostComponents) {
+    const updatedService = {};
+    for (let i in App.serviceMapper.config) {
+      if (service.get(stringUtils.underScoreToCamelCase(i)) !== undefined) {
+        updatedService[i] = service.get(stringUtils.underScoreToCamelCase(i));
+      }
+    }
+    updatedService.host_components = hostComponents;
+    App.store.safeLoad(App.Service, updatedService);
+  }
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/app/models/host_component.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/host_component.js b/ambari-web/app/models/host_component.js
index cdcf991..7217dbf 100644
--- a/ambari-web/app/models/host_component.js
+++ b/ambari-web/app/models/host_component.js
@@ -195,6 +195,15 @@ App.HostComponent.getCount = function (componentName, type) {
   }
 };
 
+/**
+ * @param {string} componentName
+ * @param {string} hostName
+ * @returns {string}
+ */
+App.HostComponent.getId = function(componentName, hostName) {
+  return componentName + '_' + hostName;
+};
+
 App.HostComponentStatus = {
   started: "STARTED",
   starting: "STARTING",

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/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 5d7108f..e6dbc06 100644
--- a/ambari-web/app/utils/ajax/ajax.js
+++ b/ambari-web/app/utils/ajax/ajax.js
@@ -1351,7 +1351,7 @@ var urls = {
     }
   },
   'cluster.load_cluster_name': {
-    'real': '/clusters?fields=Clusters/security_type',
+    'real': '/clusters?fields=Clusters/security_type,Clusters/cluster_id',
     'mock': '/data/clusters/info.json'
   },
   'cluster.load_last_upgrade': {
@@ -2287,7 +2287,7 @@ var urls = {
     mock: '/data/users/privileges_{userName}.json'
   },
   'router.login.clusters': {
-    'real': '/clusters?fields=Clusters/provisioning_state,Clusters/security_type',
+    'real': '/clusters?fields=Clusters/provisioning_state,Clusters/security_type,Clusters/cluster_id',
     'mock': '/data/clusters/info.json'
   },
   'router.login.message': {

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/app/views/main/menu.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/menu.js b/ambari-web/app/views/main/menu.js
index 6e79aba..d423402 100644
--- a/ambari-web/app/views/main/menu.js
+++ b/ambari-web/app/views/main/menu.js
@@ -156,7 +156,7 @@ App.SideNavServiceMenuView = Em.CollectionView.extend({
     return App.router.get('mainServiceController.content').filter(function (item) {
       return !this.get('disabledServices').contains(item.get('id'));
     }, this);
-  }.property('App.router.mainServiceController.content', 'App.router.mainServiceController.content.length'),
+  }.property('App.router.mainServiceController.content.length').volatile(),
 
   didInsertElement:function () {
     App.router.location.addObserver('lastSetURL', this, 'renderOnRoute');

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/test/controllers/global/cluster_controller_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/global/cluster_controller_test.js b/ambari-web/test/controllers/global/cluster_controller_test.js
index 170ed78..4dc8a19 100644
--- a/ambari-web/test/controllers/global/cluster_controller_test.js
+++ b/ambari-web/test/controllers/global/cluster_controller_test.js
@@ -122,7 +122,9 @@ describe('App.clusterController', function () {
         {
           "Clusters": {
             "cluster_name": "tdk",
-            "version": "HDP-1.3.0"
+            "version": "HDP-1.3.0",
+            "security_type": "KERBEROS",
+            "cluster_id": 1
           }
         }
       ]
@@ -130,6 +132,8 @@ describe('App.clusterController', function () {
     it('Check cluster', function () {
       controller.reloadSuccessCallback(testData);
       expect(App.get('clusterName')).to.equal('tdk');
+      expect(App.get('clusterId')).to.equal(1);
+      expect(App.get('isKerberosEnabled')).to.be.true;
       expect(App.get('currentStackVersion')).to.equal('HDP-1.3.0');
     });
   });

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/test/controllers/global/update_controller_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/global/update_controller_test.js b/ambari-web/test/controllers/global/update_controller_test.js
index eafa032..0725944 100644
--- a/ambari-web/test/controllers/global/update_controller_test.js
+++ b/ambari-web/test/controllers/global/update_controller_test.js
@@ -71,6 +71,7 @@ describe('App.UpdateController', function () {
       expect(App.updater.run.called).to.equal(false);
       expect(App.StompClient.unsubscribe.calledWith('/events/hostcomponents')).to.be.true;
       expect(App.StompClient.unsubscribe.calledWith('/events/alerts')).to.be.true;
+      expect(App.StompClient.unsubscribe.calledWith('/events/topologies')).to.be.true;
     });
 
     it('isWorking = true', function () {
@@ -78,6 +79,7 @@ describe('App.UpdateController', function () {
       expect(App.updater.run.callCount).to.equal(12);
       expect(App.StompClient.subscribe.calledWith('/events/hostcomponents')).to.be.true;
       expect(App.StompClient.subscribe.calledWith('/events/alerts')).to.be.true;
+      expect(App.StompClient.subscribe.calledWith('/events/topologies')).to.be.true;
     });
   });
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/test/controllers/main/host/details_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/main/host/details_test.js b/ambari-web/test/controllers/main/host/details_test.js
index c7e1808..7827eb7 100644
--- a/ambari-web/test/controllers/main/host/details_test.js
+++ b/ambari-web/test/controllers/main/host/details_test.js
@@ -2539,22 +2539,13 @@ describe('App.MainHostDetailsController', function () {
     };
 
     beforeEach(function () {
-      sinon.stub(controller, 'removeHostComponentModel', Em.K);
       controller.set('_deletedHostComponentResult', {});
       controller._doDeleteHostComponentSuccessCallback({}, {}, data);
     });
 
-    afterEach(function () {
-      controller.removeHostComponentModel.restore();
-    });
-
     it('should reset `_deletedHostComponentResult`', function () {
       expect(controller.get('_deletedHostComponentResult')).to.be.null;
     });
-
-    it('should call `removeHostComponentModel` with correct params', function () {
-      expect(controller.removeHostComponentModel.calledWith('COMPONENT', 'h1')).to.be.true;
-    });
   });
 
   describe('#upgradeComponentSuccessCallback()', function () {
@@ -3328,42 +3319,6 @@ describe('App.MainHostDetailsController', function () {
 
   });
 
-  describe("#removeHostComponentModel()", function () {
-
-    beforeEach(function () {
-      App.cache.services = [
-        {
-          ServiceInfo: {
-            service_name: 'S1'
-          },
-          host_components: ['C1_host1']
-        }
-      ];
-      sinon.stub(App.HostComponent, 'find').returns([
-        Em.Object.create({
-          id: 'C1_host1',
-          componentName: 'C1',
-          hostName: 'host1',
-          service: Em.Object.create({
-            serviceName: 'S1'
-          })
-        })
-      ]);
-      sinon.stub(App.serviceMapper, 'deleteRecord', Em.K);
-      controller.removeHostComponentModel('C1', 'host1');
-    });
-    afterEach(function () {
-      App.HostComponent.find.restore();
-      App.serviceMapper.deleteRecord.restore();
-    });
-    it("App.cache is updated", function () {
-      expect(App.cache.services[0].host_components).to.be.empty;
-    });
-    it('Record is deleted', function () {
-      expect(App.serviceMapper.deleteRecord.calledOnce).to.be.true;
-    });
-  });
-
   describe("#parseNnCheckPointTime", function () {
     var tests = [
       {

http://git-wip-us.apache.org/repos/asf/ambari/blob/d986503c/ambari-web/test/mappers/socket/topology_mapper_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/mappers/socket/topology_mapper_test.js b/ambari-web/test/mappers/socket/topology_mapper_test.js
new file mode 100644
index 0000000..7ebaf75
--- /dev/null
+++ b/ambari-web/test/mappers/socket/topology_mapper_test.js
@@ -0,0 +1,201 @@
+/**
+ * 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('mappers/socket/topology_mapper');
+
+describe('App.topologyMapper', function () {
+  const mapper = App.topologyMapper;
+
+
+  describe('#map', function () {
+    const mockCtrl = {
+      updateHost: sinon.spy()
+    };
+    beforeEach(function () {
+      sinon.stub(mapper, 'applyComponentTopologyChanges');
+      sinon.stub(App.router, 'get').returns(mockCtrl);
+      App.set('clusterId', 1);
+    });
+    afterEach(function() {
+      mapper.applyComponentTopologyChanges.restore();
+      App.router.get.restore();
+    });
+    it('applyComponentTopologyChanges should be called', function () {
+      mapper.map({clusters: {1: {components: []}}, eventType: 'UPDATE'});
+      expect(mapper.applyComponentTopologyChanges.calledWith([], 'UPDATE')).to.be.true;
+    });
+    it('updateHost should be called', function () {
+      mapper.map({clusters: {1: {hosts: []}}, eventType: 'UPDATE'});
+      expect(mockCtrl.updateHost.calledWith(Em.K, null, true)).to.be.true;
+    });
+  });
+
+  describe('#applyComponentTopologyChanges', function () {
+    beforeEach(function () {
+      sinon.stub(mapper, 'addServiceIfNew');
+      sinon.stub(mapper, 'createHostComponent');
+      sinon.stub(mapper, 'deleteHostComponent');
+      sinon.stub(mapper, 'deleteServiceIfHasNoComponents');
+    });
+    afterEach(function() {
+      mapper.addServiceIfNew.restore();
+      mapper.createHostComponent.restore();
+      mapper.deleteHostComponent.restore();
+      mapper.deleteServiceIfHasNoComponents.restore();
+    });
+    it('CREATE component event', function () {
+      const components = [
+        {
+          hostNames: ['host1'],
+          serviceName: 'S1',
+          version: 'UNKNOWN',
+          publicHostNames: ['public1']
+        }
+      ];
+      mapper.applyComponentTopologyChanges(components, 'UPDATE');
+      expect(mapper.addServiceIfNew.calledWith('S1')).to.be.true;
+      expect(mapper.createHostComponent.calledWith(components[0], 'host1', 'public1')).to.be.true;
+    });
+
+    it('DELETE component event', function () {
+      const components = [
+        {
+          hostNames: ['host1'],
+          serviceName: 'S1'
+        }
+      ];
+      mapper.applyComponentTopologyChanges(components, 'DELETE');
+      expect(mapper.deleteHostComponent.calledWith(components[0], 'host1')).to.be.true;
+      expect(mapper.deleteServiceIfHasNoComponents.calledWith('S1')).to.be.true;
+    });
+  });
+
+  describe('#addServiceIfNew', function () {
+    beforeEach(function () {
+      sinon.stub(App.Service, 'find').returns(Em.Object.create({isLoaded: false}));
+      sinon.stub(App.store, 'safeLoad');
+    });
+    afterEach(function() {
+      App.Service.find.restore();
+      App.store.safeLoad.restore();
+    });
+    it('should load service if it does not exist yet', function () {
+      mapper.addServiceIfNew('S1');
+      expect(App.store.safeLoad.calledOnce).to.be.true;
+    });
+  });
+
+  describe('#deleteServiceIfHasNoComponents', function () {
+    beforeEach(function () {
+      sinon.stub(App.Service, 'find').returns(Em.Object.create({isLoaded: true, hostComponents: []}));
+      sinon.stub(mapper, 'deleteRecord');
+    });
+    afterEach(function() {
+      App.Service.find.restore();
+      mapper.deleteRecord.restore();
+    });
+    it('should delete service record', function () {
+      mapper.deleteServiceIfHasNoComponents('S1');
+      expect(mapper.deleteRecord.calledOnce).to.be.true;
+    });
+  });
+
+  describe('#deleteHostComponent', function () {
+    beforeEach(function () {
+      sinon.stub(App.HostComponent, 'find').returns(Em.Object.create({isLoaded: true}));
+      sinon.stub(mapper, 'deleteRecord');
+      sinon.stub(mapper, 'updateHostComponentsOfHost');
+      sinon.stub(mapper, 'updateHostComponentsOfService');
+      sinon.stub(App.Host, 'find').returns(Em.Object.create({hostComponents: [{id: 'C1_host1'}]}));
+      sinon.stub(App.Service, 'find').returns(Em.Object.create({hostComponents: [{id: 'C1_host1'}]}));
+      mapper.deleteHostComponent({componentName: 'C1'}, 'host1');
+    });
+    afterEach(function() {
+      App.HostComponent.find.restore();
+      mapper.deleteRecord.restore();
+      mapper.updateHostComponentsOfHost.restore();
+      mapper.updateHostComponentsOfService.restore();
+      App.Host.find.restore();
+      App.Service.find.restore();
+    });
+    it('deleteRecord should be called', function () {
+      expect(mapper.deleteRecord.calledOnce).to.be.true;
+    });
+    it('updateHostComponentsOfHost should be called', function () {
+      expect(mapper.updateHostComponentsOfHost.calledWith(Em.Object.create({hostComponents: [{id: 'C1_host1'}]}), [])).to.be.true;
+    });
+    it('updateHostComponentsOfService should be called', function () {
+      expect(mapper.updateHostComponentsOfService.calledWith(Em.Object.create({hostComponents: [{id: 'C1_host1'}]}), [])).to.be.true;
+    });
+  });
+
+  describe('#createHostComponent', function () {
+    beforeEach(function () {
+      sinon.stub(App.store, 'safeLoad');
+      sinon.stub(mapper, 'updateHostComponentsOfHost');
+      sinon.stub(mapper, 'updateHostComponentsOfService');
+      sinon.stub(App.Host, 'find').returns(Em.Object.create({hostComponents: [], isLoaded: true}));
+      sinon.stub(App.Service, 'find').returns(Em.Object.create({hostComponents: []}));
+      mapper.createHostComponent({componentName: 'C1'}, 'host1');
+    });
+    afterEach(function() {
+      App.store.safeLoad.restore();
+      mapper.updateHostComponentsOfHost.restore();
+      mapper.updateHostComponentsOfService.restore();
+      App.Host.find.restore();
+      App.Service.find.restore();
+    });
+    it('deleteRecord should be called', function () {
+      expect(App.store.safeLoad.calledOnce).to.be.true;
+    });
+    it('updateHostComponentsOfHost should be called', function () {
+      expect(mapper.updateHostComponentsOfHost.calledOnce).to.be.true;
+    });
+    it('updateHostComponentsOfService should be called', function () {
+      expect(mapper.updateHostComponentsOfService.calledOnce).to.be.true;
+    });
+  });
+
+  describe('#updateHostComponentsOfHost', function () {
+    beforeEach(function () {
+      sinon.stub(App.store, 'safeLoad');
+    });
+    afterEach(function() {
+      App.store.safeLoad.restore();
+    });
+    it('App.store.safeLoad should be called', function () {
+      mapper.updateHostComponentsOfHost(Em.Object.create({id: 1}), [{id: 2}]);
+      expect(App.store.safeLoad.calledWith(App.Host, {id: 1, hostComponents: [{id: 2}]}));
+    });
+  });
+
+  describe('#updateHostComponentsOfService', function () {
+    beforeEach(function () {
+      sinon.stub(App.store, 'safeLoad');
+    });
+    afterEach(function() {
+      App.store.safeLoad.restore();
+    });
+    it('App.store.safeLoad should be called', function () {
+      mapper.updateHostComponentsOfService(Em.Object.create({id: 1}), [{id: 2}]);
+      expect(App.store.safeLoad.calledWith(App.Service, {id: 1, hostComponents: [{id: 2}]}));
+    });
+  });
+});