changeset 222:0e10eacbeeb7

Add byteman subview to jvm-info Reviewed-by: jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/025146.html
author Andrew Azores <aazores@redhat.com>
date Fri, 22 Sep 2017 07:51:59 -0400
parents 362ef36b1cef
children 57a4676a9343
files mock-api/endpoints/jvm-byteman.endpoint.js src/app/components/jvm-info/byteman/byteman.component.js src/app/components/jvm-info/byteman/byteman.controller.js src/app/components/jvm-info/byteman/byteman.controller.spec.js src/app/components/jvm-info/byteman/byteman.html src/app/components/jvm-info/byteman/byteman.routing.js src/app/components/jvm-info/byteman/byteman.routing.spec.js src/app/components/jvm-info/byteman/byteman.service.js src/app/components/jvm-info/byteman/byteman.service.spec.js src/app/components/jvm-info/byteman/en.locale.yaml src/app/components/jvm-info/en.locale.yaml src/app/components/jvm-info/jvm-info.html
diffstat 12 files changed, 942 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mock-api/endpoints/jvm-byteman.endpoint.js	Fri Sep 22 07:51:59 2017 -0400
@@ -0,0 +1,44 @@
+function jvmByteman (server) {
+  // web-gateway
+  var _ = require('lodash');
+  server.init('jvmByteman');
+  server.app.get('/jvm-byteman/0.0.1/status/jvms/:jvmId', function (req, res) {
+    server.logRequest('jvm-byteman', req);
+
+    var jvmId = req.params.jvmId;
+
+    var response = [];
+    var data = {
+      agentId: 'foo-agentId',
+      jvmId: jvmId,
+      timeStamp: { $numberLong: Date.now().toString() },
+      rule: '',
+      listenPort: 9999
+    };
+    response.push(data);
+
+    res.setHeader('Content-Type', 'application/json');
+    res.send(JSON.stringify(
+      {
+        response: response
+      }
+    ));
+  });
+
+  // command channel
+  server.init('byteman-command');
+  server.app.ws('/commands/v1/actions/byteman/systems/:systemId/agents/:agentId/jvms/:jvmId/sequence/:seqId', function (ws, req) {
+    server.logRequest('byteman-command', req);
+    ws.on('message', function (msg) {
+      ws.send(JSON.stringify(
+        {
+          payload: {
+            respType: 'OK'
+          }
+        }
+      ));
+    });
+  });
+}
+
+module.exports = jvmByteman;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/byteman.component.js	Fri Sep 22 07:51:59 2017 -0400
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import BytemanController from './byteman.controller.js';
+
+export default angular
+  .module('byteman', [BytemanController])
+  .component('byteman', {
+    controller: 'BytemanController',
+    template: require('./byteman.html')
+  })
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/byteman.controller.js	Fri Sep 22 07:51:59 2017 -0400
@@ -0,0 +1,96 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import service from './byteman.service.js';
+
+class BytemanController {
+  constructor ($stateParams, $translate, bytemanService) {
+    'ngInject';
+    this.jvmId = $stateParams.jvmId;
+    this.systemId = $stateParams.systemId;
+    this._translate = $translate;
+    this._svc = bytemanService;
+
+    this.loadedRule = '';
+  }
+
+  $onInit () {
+    this._updateRules();
+  }
+
+  $onDestroy () {
+  }
+
+  _updateRules () {
+    return this._svc.getLoadedRules(this.jvmId)
+      .then(res => {
+        this.loadedRule = res;
+        this._clearInput();
+      });
+  }
+
+  _clearInput () {
+    this.ruleText = '';
+  }
+
+  refresh () {
+    return this._updateRules();
+  }
+
+  unload () {
+    if (!this.loadedRule) {
+      return;
+    }
+    return this._svc.unloadRules(this.systemId, this.jvmId)
+      .then(() => this._updateRules());
+  }
+
+  push () {
+    return this._svc.loadRule(this.systemId, this.jvmId, this.ruleText)
+      .then(() => this._updateRules());
+  }
+
+  pull () {
+    return this._svc.getLoadedRules(this.jvmId)
+      .then(res => {
+        this.loadedRule = res;
+        if (res) {
+          this.ruleText = res;
+        }
+      });
+  }
+
+  generateTemplate () {
+    return this._translate('byteman.RULE_TEMPLATE')
+      .then(res => this.ruleText = res);
+  }
+}
+
+export default angular
+  .module('byteman.controller', [service])
+  .controller('BytemanController', BytemanController)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/byteman.controller.spec.js	Fri Sep 22 07:51:59 2017 -0400
@@ -0,0 +1,183 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import controllerModule from './byteman.controller.js';
+
+describe('BytemanController', () => {
+
+  let ctrl, stateParams, translate, svc;
+  beforeEach(() => {
+    angular.mock.module(controllerModule);
+
+    stateParams = {
+      jvmId: 'foo-jvmId',
+      systemId: 'foo-systemId'
+    };
+
+    translate = sinon.stub();
+    translate.then = sinon.stub();
+    translate.returns({ then: translate.then });
+
+    svc = {
+      getLoadedRules: sinon.stub(),
+      loadRule: sinon.stub(),
+      unloadRules: sinon.stub()
+    };
+
+    angular.mock.inject($controller => {
+      'ngInject';
+      ctrl = $controller('BytemanController', {
+        $stateParams: stateParams,
+        $translate: translate,
+        bytemanService: svc
+      });
+    });
+  });
+
+  describe('$onInit ()', () => {
+    it('should load injected rules', () => {
+      svc.getLoadedRules.should.not.be.called();
+      svc.getLoadedRules.returns({
+        then: sinon.stub().yields('fake rule')
+      });
+
+      ctrl.$onInit();
+
+      svc.getLoadedRules.should.be.calledOnce();
+      svc.getLoadedRules.should.be.calledWith(stateParams.jvmId);
+      ctrl.loadedRule.should.equal('fake rule');
+    });
+  });
+
+  describe('_clearInput ()', () => {
+    it('should reset ruleText property to the empty string', () => {
+      ctrl.ruleText = 'foo';
+      ctrl._clearInput();
+      ctrl.ruleText.should.equal('');
+    });
+  });
+
+  describe('refresh ()', () => {
+    it('should load injected rules', () => {
+      svc.getLoadedRules.should.not.be.called();
+      svc.getLoadedRules.returns({
+        then: sinon.stub().yields('fake rule')
+      });
+
+      ctrl.refresh();
+
+      svc.getLoadedRules.should.be.calledOnce();
+      svc.getLoadedRules.should.be.calledWith(stateParams.jvmId);
+      ctrl.loadedRule.should.equal('fake rule');
+    });
+  });
+
+  describe('unload ()', () => {
+    it('should do nothing if no loaded rule', () => {
+      svc.unloadRules.should.not.be.called();
+      ctrl.unload();
+      svc.unloadRules.should.not.be.called();
+    });
+
+    it('should unload rules', () => {
+      svc.getLoadedRules.should.not.be.called();
+      svc.getLoadedRules.returns({
+        then: sinon.stub().yields('fake rule')
+      });
+      ctrl.refresh();
+
+      svc.getLoadedRules.returns({
+        then: sinon.stub().yields('')
+      });
+      svc.unloadRules.returns({
+        then: sinon.stub().yields()
+      });
+      ctrl.unload();
+      ctrl.loadedRule.should.equal('');
+    });
+  });
+
+  describe('push ()', () => {
+    it('should send local rule text to service', () => {
+      const injectedRule = 'injected rule';
+      ctrl.ruleText = injectedRule;
+      svc.loadRule.returns({
+        then: sinon.stub().yields()
+      });
+      svc.getLoadedRules.returns({
+        then: sinon.stub().yields(injectedRule)
+      });
+
+      ctrl.push();
+
+      svc.loadRule.should.be.calledOnce();
+      svc.loadRule.should.be.calledWith(stateParams.systemId, stateParams.jvmId, injectedRule);
+      ctrl.loadedRule.should.equal(injectedRule);
+    });
+  });
+
+  describe('pull ()', () => {
+    it('should pull injected rule into editor', () => {
+      const loadedRule = 'loaded rule';
+      svc.getLoadedRules.returns({
+        then: sinon.stub().yields(loadedRule)
+      });
+
+      ctrl.pull();
+
+      svc.getLoadedRules.should.be.calledOnce();
+      svc.getLoadedRules.should.be.calledWith(stateParams.jvmId);
+
+      ctrl.loadedRule.should.equal(loadedRule);
+      ctrl.ruleText.should.equal(loadedRule);
+    });
+
+    it('should not clobber rule text if no remotely injected rules', () => {
+      svc.getLoadedRules.returns({
+        then: sinon.stub().yields()
+      });
+
+      ctrl.ruleText = 'locally edited rule';
+      ctrl.pull();
+
+      svc.getLoadedRules.should.be.calledOnce();
+      svc.getLoadedRules.should.be.calledWith(stateParams.jvmId);
+
+      should(ctrl.loadedRule).be.undefined();
+      ctrl.ruleText.should.equal('locally edited rule');
+    });
+  });
+
+  describe('generateTemplate ()', () => {
+    it('should set rule text from translate service', () => {
+      translate.then.yields('rule template');
+      ctrl.generateTemplate();
+      ctrl.ruleText.should.equal('rule template');
+    });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/byteman.html	Fri Sep 22 07:51:59 2017 -0400
@@ -0,0 +1,40 @@
+<div class="container-fluid" style="margin-top: 2vh;">
+  <div class="col-md-8">
+
+    <div class="row">
+      <div role="group" class="btn-group pull-right">
+        <button type="button" class="btn btn-secondary" ng-click="$ctrl.refresh()"><span class="fa fa-refresh"></span></button>
+        <button type="button" class="btn btn-secondary" ng-click="$ctrl.unload()" translate>byteman.UNLOAD_BTN_LABEL</button>
+      </div>
+    </div>
+
+    <div class="row">
+
+      <div class="form-group col-md-6">
+        <label for="localRule" class="label label-info" translate>byteman.LOCAL_RULE_LABEL</label>
+        <textarea name="localRule" class="form-control" ng-model="$ctrl.ruleText" rows="8"/>
+      </div>
+
+      <div role="group" class="btn-group col-md-1">
+        <button type="button" class="btn btn-default"
+                              ng-click="$ctrl.pull()" ng-disabled="!$ctrl.loadedRule" translate>byteman.PULL_BTN_LABEL</button>
+        <button type="button" class="btn btn-default"
+                              ng-click="$ctrl.push()" ng-disabled="!$ctrl.ruleText.length" translate>byteman.PUSH_BTN_LABEL</button>
+      </div>
+
+      <div class="form-group col-md-5">
+        <label for="remoteRule" class="label label-info" translate>byteman.REMOTE_RULE_LABEL</label>
+        <textarea name="remoteRule" class="form-control"
+                                    translate-attr="{placeholder: 'byteman.NO_RULES_LABEL'}" ng-model="$ctrl.loadedRule" rows="8" readonly/>
+      </div>
+
+    </div>
+
+    <div class="row">
+      <div role="group" class="btn-group pull-right">
+        <button type="button" class="btn btn-secondary" ng-click="$ctrl.generateTemplate()" translate>byteman.GENERATE_RULE_BTN_LABEL</button>
+      </div>
+    </div>
+
+  </div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/byteman.routing.js	Fri Sep 22 07:51:59 2017 -0400
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+function config ($stateProvider) {
+  'ngInject';
+
+  $stateProvider.state('jvmInfo.byteman', {
+    url: '/byteman',
+    component: 'byteman',
+    resolve: {
+      lazyLoad: ($q, $ocLazyLoad) => {
+        'ngInject';
+        return $q(resolve => {
+          require.ensure(['./byteman.component.js'], () => {
+            let module = require('./byteman.component.js');
+            $ocLazyLoad.load({ name: module.default });
+            resolve(module);
+          });
+        });
+      }
+    }
+  });
+}
+
+export { config };
+
+export default angular
+  .module('byteman.routing', [
+    'ui.router',
+    'oc.lazyLoad'
+  ])
+  .config(config)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/byteman.routing.spec.js	Fri Sep 22 07:51:59 2017 -0400
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+describe('BytemanRouting', () => {
+
+  let module = require('./byteman.routing.js');
+  let stateProvider, args, q, ocLazyLoad;
+  beforeEach(() => {
+    angular.mock.module(module.default);
+    stateProvider = {
+      state: sinon.spy()
+    };
+    module.config(stateProvider);
+    args = stateProvider.state.args[0];
+    q = sinon.spy();
+    ocLazyLoad = {
+      load: sinon.spy()
+    };
+  });
+
+  describe('stateProvider', () => {
+    it('should call $stateProvider.state', () => {
+      stateProvider.state.should.be.calledOnce();
+    });
+
+    it('should define a \'jvmInfo.byteman\' state', () => {
+      args[0].should.equal('jvmInfo.byteman');
+    });
+
+    it('should map to /byteman', () => {
+      args[1].url.should.equal('/byteman');
+    });
+
+    it('resolve should load byteman component', done => {
+      let resolveFn = args[1].resolve.lazyLoad[2];
+      resolveFn.should.be.a.Function();
+      resolveFn(q, ocLazyLoad);
+      q.should.be.calledOnce();
+
+      let deferred = q.args[0][0];
+      deferred.should.be.a.Function();
+
+      let resolve = sinon.stub().callsFake(val => {
+        let mod = require('./byteman.component.js');
+        ocLazyLoad.load.should.be.calledWith({ name: mod.default });
+        val.should.equal(mod);
+        done();
+      });
+      deferred(resolve);
+    });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/byteman.service.js	Fri Sep 22 07:51:59 2017 -0400
@@ -0,0 +1,141 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import services from 'shared/services/services.module.js';
+import config from 'shared/config/config.module.js';
+import urlJoin from 'url-join';
+
+const LOAD_RULE_ACTION = 0;
+const UNLOAD_RULE_ACTION = 1;
+const INITIAL_LISTEN_PORT = -1;
+
+class BytemanService {
+
+  constructor ($q, $http, gatewayUrl, commandChannelService) {
+    'ngInject';
+    this._q = $q;
+    this._http = $http;
+    this._gatewayUrl = gatewayUrl;
+    this._cmdChan = commandChannelService;
+  }
+
+  getLoadedRules (jvmId) {
+    return this._getBytemanStatus(jvmId).then(res => {
+      if (!res) {
+        return '';
+      }
+      return res.rule;
+    });
+  }
+
+  loadRule (systemId, jvmId, rule) {
+    return this._sendCmdChanRequest(systemId, jvmId, LOAD_RULE_ACTION, rule);
+  }
+
+  unloadRules (systemId, jvmId) {
+    return this._sendCmdChanRequest(systemId, jvmId, UNLOAD_RULE_ACTION);
+  }
+
+  _sendCmdChanRequest (systemId, jvmId, action, rule) {
+    let defer = this._q.defer();
+
+    this._q.all({
+      jvmInfo: this._getJvmInfo(systemId, jvmId),
+      listenPort: this._getListenPort(jvmId)
+    }).then(result => {
+      const jvmInfo = result.jvmInfo;
+      const agentId = jvmInfo.agentId;
+      const pid = jvmInfo.jvmPid;
+      const port = result.listenPort;
+
+      let path = urlJoin(
+        'commands',
+        'v1',
+        'actions',
+        'byteman',
+        'systems',
+        systemId,
+        'agents',
+        agentId,
+        'jvms',
+        jvmId,
+        'sequence',
+        this._cmdChan.sequence
+      );
+
+      let payload = {
+        'byteman-action': action,
+        'listen-port': port,
+        'vm-pid': pid
+      };
+      if (rule) {
+        payload['byteman-rule'] = rule;
+      }
+      this._cmdChan.sendMessage(path, payload).then(
+        success => {
+          defer.resolve({
+            status: success.payload.respType.value === this._cmdChan.responseCodes.OK.value,
+            reason: success.payload.respType.message
+          });
+        }
+      );
+    });
+
+    return defer.promise;
+  }
+
+  _getJvmInfo (systemId, jvmId) {
+    return this._http.get(urlJoin(this._gatewayUrl, 'jvms', '0.0.1', 'systems', systemId, 'jvms', jvmId))
+      .then(res => {
+        return res.data.response[0];
+      });
+  }
+
+  _getBytemanStatus (jvmId) {
+    return this._http.get(urlJoin(this._gatewayUrl, 'jvm-byteman', '0.0.1', 'status', 'jvms', jvmId))
+      .then(res => {
+        return res.data.response[0];
+      });
+  }
+
+  _getListenPort (jvmId) {
+    return this._getBytemanStatus(jvmId).then(res => {
+      if (!res) {
+        return INITIAL_LISTEN_PORT;
+      }
+      return res.listenPort;
+    });
+  }
+}
+
+export default angular
+  .module('byteman.service', [
+    services,
+    config
+  ])
+  .service('bytemanService', BytemanService)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/byteman.service.spec.js	Fri Sep 22 07:51:59 2017 -0400
@@ -0,0 +1,248 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import serviceModule from './byteman.service.js';
+import servicesModule from 'shared/services/services.module.js';
+
+describe('BytemanService', () => {
+
+  let cmdChan;
+  beforeEach(() => {
+    angular.mock.module('configModule', $provide => {
+      'ngInject';
+      $provide.constant('gatewayUrl', 'http://example.com:1234');
+    });
+
+    cmdChan = {
+      sendMessage: sinon.stub().returns({
+        then: sinon.stub().yields({
+          payload: {
+            respType: {
+              value: 'OK'
+            }
+          }
+        })
+      }),
+      sequence: 5,
+      responseCodes: {
+        OK: {
+          value: 'OK'
+        }
+      }
+    };
+    angular.mock.module(servicesModule, $provide => {
+      'ngInject';
+      $provide.constant('commandChannelService', cmdChan);
+    });
+
+    angular.mock.module(serviceModule);
+  });
+
+  let httpBackend, scope, svc;
+  beforeEach(inject(($httpBackend, $rootScope, bytemanService) => {
+    'ngInject';
+    httpBackend = $httpBackend;
+
+    scope = $rootScope;
+    svc = bytemanService;
+  }));
+
+  afterEach(() => {
+    httpBackend.verifyNoOutstandingExpectation();
+    httpBackend.verifyNoOutstandingRequest();
+  });
+
+  it('should exist', () => {
+    should.exist(svc);
+  });
+
+  describe('getLoadedRules (jvmId)', () => {
+    it('should resolve mock data', done => {
+      let response = {
+        response: [
+          {
+            rule: 'loaded rule'
+          }
+        ]
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvm-byteman/0.0.1/status/jvms/foo-jvmId')
+        .respond(response);
+      svc.getLoadedRules('foo-jvmId').then(res => {
+        res.should.equal(response.response[0].rule);
+        done();
+      });
+      httpBackend.expectGET('http://example.com:1234/jvm-byteman/0.0.1/status/jvms/foo-jvmId');
+      httpBackend.flush();
+      scope.$apply();
+    });
+
+    it('should return empty string if no results', done => {
+      let response = {
+        response: []
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvm-byteman/0.0.1/status/jvms/foo-jvmId')
+        .respond(response);
+      svc.getLoadedRules('foo-jvmId').then(res => {
+        res.should.equal('');
+        done();
+      });
+      httpBackend.expectGET('http://example.com:1234/jvm-byteman/0.0.1/status/jvms/foo-jvmId');
+      httpBackend.flush();
+      scope.$apply();
+    });
+  });
+
+  describe('loadRule (systemId, jvmId, rule)', () => {
+    it('should send rule in command channel request payload', done => {
+      let jvmInfo = {
+        response: [
+          {
+            agentId: 'foo-agentId',
+            jvmPid: 100,
+          }
+        ]
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvms/0.0.1/systems/foo-systemId/jvms/foo-jvmId')
+        .respond(jvmInfo);
+      httpBackend.expectGET('http://example.com:1234/jvms/0.0.1/systems/foo-systemId/jvms/foo-jvmId');
+
+      let bytemanStatus = {
+        response: [
+          {
+            listenPort: 9999
+          }
+        ]
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvm-byteman/0.0.1/status/jvms/foo-jvmId')
+        .respond(bytemanStatus);
+      httpBackend.expectGET('http://example.com:1234/jvm-byteman/0.0.1/status/jvms/foo-jvmId');
+
+      svc.loadRule('foo-systemId', 'foo-jvmId', 'fake rule').then(res => {
+        cmdChan.sendMessage.should.be.calledOnce();
+        cmdChan.sendMessage.should.be.calledWith(
+          'commands/v1/actions/byteman/systems/foo-systemId/agents/foo-agentId/jvms/foo-jvmId/sequence/5',
+          {
+            'byteman-action': 0,
+            'byteman-rule': 'fake rule',
+            'listen-port': 9999,
+            'vm-pid': 100
+          }
+        );
+        res.should.deepEqual({ status: true, reason: undefined });
+        done();
+      });
+
+      httpBackend.flush();
+      scope.$apply();
+    });
+
+    it('should use listenPort:-1 if none in storage', done => {
+      let jvmInfo = {
+        response: [
+          {
+            agentId: 'foo-agentId',
+            jvmPid: 100,
+          }
+        ]
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvms/0.0.1/systems/foo-systemId/jvms/foo-jvmId')
+        .respond(jvmInfo);
+      httpBackend.expectGET('http://example.com:1234/jvms/0.0.1/systems/foo-systemId/jvms/foo-jvmId');
+
+      let bytemanStatus = {
+        response: []
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvm-byteman/0.0.1/status/jvms/foo-jvmId')
+        .respond(bytemanStatus);
+      httpBackend.expectGET('http://example.com:1234/jvm-byteman/0.0.1/status/jvms/foo-jvmId');
+
+      svc.loadRule('foo-systemId', 'foo-jvmId', 'fake rule').then(res => {
+        cmdChan.sendMessage.should.be.calledOnce();
+        cmdChan.sendMessage.should.be.calledWith(
+          'commands/v1/actions/byteman/systems/foo-systemId/agents/foo-agentId/jvms/foo-jvmId/sequence/5',
+          {
+            'byteman-action': 0,
+            'byteman-rule': 'fake rule',
+            'listen-port': -1,
+            'vm-pid': 100
+          }
+        );
+        res.should.deepEqual({ status: true, reason: undefined });
+        done();
+      });
+
+      httpBackend.flush();
+      scope.$apply();
+    });
+  });
+
+  describe('unloadRules (systemId, jvmId)', () => {
+    it('should send unload rules request on command channel', done => {
+      let jvmInfo = {
+        response: [
+          {
+            agentId: 'foo-agentId',
+            jvmPid: 100,
+          }
+        ]
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvms/0.0.1/systems/foo-systemId/jvms/foo-jvmId')
+        .respond(jvmInfo);
+      httpBackend.expectGET('http://example.com:1234/jvms/0.0.1/systems/foo-systemId/jvms/foo-jvmId');
+
+      let bytemanStatus = {
+        response: [
+          {
+            listenPort: 9999
+          }
+        ]
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvm-byteman/0.0.1/status/jvms/foo-jvmId')
+        .respond(bytemanStatus);
+      httpBackend.expectGET('http://example.com:1234/jvm-byteman/0.0.1/status/jvms/foo-jvmId');
+
+      svc.unloadRules('foo-systemId', 'foo-jvmId').then(res => {
+        cmdChan.sendMessage.should.be.calledOnce();
+        cmdChan.sendMessage.should.be.calledWith(
+          'commands/v1/actions/byteman/systems/foo-systemId/agents/foo-agentId/jvms/foo-jvmId/sequence/5',
+          {
+            'byteman-action': 1,
+            'listen-port': 9999,
+            'vm-pid': 100
+          }
+        );
+        res.should.deepEqual({ status: true, reason: undefined });
+        done();
+      });
+
+      httpBackend.flush();
+      scope.$apply();
+    });
+  });
+
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/en.locale.yaml	Fri Sep 22 07:51:59 2017 -0400
@@ -0,0 +1,18 @@
+byteman:
+  LOCAL_RULE_LABEL: Local Rule
+  REMOTE_RULE_LABEL: Injected Rule
+  NO_RULES_LABEL: '<no-rules-loaded>'
+  PUSH_BTN_LABEL: '&gt;'
+  PULL_BTN_LABEL: '&lt;'
+  UNLOAD_BTN_LABEL: Unload Rule
+  GENERATE_RULE_BTN_LABEL: Generate Rule Template
+  RULE_TEMPLATE: |
+    RULE Thermostat byteman template rule for com.redhat.thermostat.main.Thermostat
+    CLASS com.redhat.thermostat.main.Thermostat
+    METHOD main
+    HELPER org.jboss.byteman.thermostat.helper.ThermostatHelper
+    AT ENTRY
+    IF true
+    DO
+    send("foo-marker", "action", "com.redhat.thermostat.main.Thermostat.main() called");
+    ENDRULE
--- a/src/app/components/jvm-info/en.locale.yaml	Thu Sep 21 17:08:50 2017 -0400
+++ b/src/app/components/jvm-info/en.locale.yaml	Fri Sep 22 07:51:59 2017 -0400
@@ -35,3 +35,4 @@
     MEMORY: Memory Usage
     GC: Garbage Collection
     IO: File I/O
+    BYTEMAN: Byteman
--- a/src/app/components/jvm-info/jvm-info.html	Thu Sep 21 17:08:50 2017 -0400
+++ b/src/app/components/jvm-info/jvm-info.html	Fri Sep 22 07:51:59 2017 -0400
@@ -133,6 +133,7 @@
         <option value="jvmMemory" translate>jvmInfo.subview.MEMORY</option>
         <option value="jvmGc" translate>jvmInfo.subview.GC</option>
         <option value="jvmIo" translate>jvmInfo.subview.IO</option>
+        <option value="byteman" translate>jvmInfo.subview.BYTEMAN</option>
       </select>
     </div>