changeset 2528:3c990cdff9ad

Implement jmx-client-cli Reviewed-by: omajid Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-November/021503.html
author Andrew Azores <aazores@redhat.com>
date Fri, 28 Oct 2016 15:24:30 -0400
parents edf71d9262f8
children 753e5d12c580
files vm-jmx/client-cli/pom.xml vm-jmx/client-cli/src/main/java/com/redhat/thermostat/vm/jmx/client/cli/NotificationsCommand.java vm-jmx/client-cli/src/main/java/com/redhat/thermostat/vm/jmx/client/cli/internal/Activator.java vm-jmx/client-cli/src/main/java/com/redhat/thermostat/vm/jmx/client/cli/internal/NotificationsSinceParser.java vm-jmx/client-cli/src/main/java/com/redhat/thermostat/vm/jmx/client/cli/locale/LocaleResources.java vm-jmx/client-cli/src/main/resources/com/redhat/thermostat/vm/jmx/client/cli/locale/strings.properties vm-jmx/client-cli/src/test/java/com/redhat/thermostat/vm/jmx/client/cli/NotificationsCommandTest.java vm-jmx/client-cli/src/test/java/com/redhat/thermostat/vm/jmx/client/cli/internal/ActivatorTest.java vm-jmx/client-cli/src/test/java/com/redhat/thermostat/vm/jmx/client/cli/internal/NotificationsSinceParserTest.java vm-jmx/client-cli/src/test/java/com/redhat/thermostat/vm/jmx/client/cli/locale/LocaleResourcesTest.java vm-jmx/client-core/src/main/java/com/redhat/thermostat/vm/jmx/client/core/JmxToggleNotificationRequest.java vm-jmx/client-core/src/main/java/com/redhat/thermostat/vm/jmx/client/core/internal/JmxNotificationsViewController.java vm-jmx/client-core/src/main/java/com/redhat/thermostat/vm/jmx/client/core/internal/JmxToggleNotificationRequest.java vm-jmx/client-core/src/test/java/com/redhat/thermostat/vm/jmx/client/core/JmxToggleNotificationRequestTest.java vm-jmx/client-core/src/test/java/com/redhat/thermostat/vm/jmx/client/core/internal/JmxNotificationsViewControllerTest.java vm-jmx/client-core/src/test/java/com/redhat/thermostat/vm/jmx/client/core/internal/JmxToggleNotificationRequestTest.java vm-jmx/distribution/pom.xml vm-jmx/distribution/thermostat-plugin.xml vm-jmx/pom.xml
diffstat 19 files changed, 2102 insertions(+), 332 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-cli/pom.xml	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Copyright 2012-2016 Red Hat, Inc.
+
+ This file is part of Thermostat.
+
+ Thermostat is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published
+ by the Free Software Foundation; either version 2, or (at your
+ option) any later version.
+
+ Thermostat is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Thermostat; see the file COPYING.  If not see
+ <http://www.gnu.org/licenses/>.
+
+ Linking this code with other modules is making a combined work
+ based on this code.  Thus, the terms and conditions of the GNU
+ General Public License cover the whole combination.
+
+ As a special exception, the copyright holders of this code 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 this code.  If you modify
+ this code, you may extend this exception to your version of the
+ library, but you are not obligated to do so.  If you do not wish
+ to do so, delete this exception statement from your version.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <artifactId>thermostat-vm-jmx</artifactId>
+    <groupId>com.redhat.thermostat</groupId>
+    <version>1.99.12-SNAPSHOT</version>
+  </parent>
+  <artifactId>thermostat-vm-jmx-client-cli</artifactId>
+  <packaging>bundle</packaging>
+  <name>Thermostat VM JMX CLI plugin</name>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <extensions>true</extensions>
+        <configuration>
+          <instructions>
+            <Bundle-Vendor>Red Hat, Inc.</Bundle-Vendor>
+            <Bundle-SymbolicName>com.redhat.thermostat.vm.jmx.client.cli</Bundle-SymbolicName>
+            <Bundle-Activator>com.redhat.thermostat.vm.jmx.client.cli.internal.Activator</Bundle-Activator>
+            <Export-Package>
+              com.redhat.thermostat.vm.jmx.client.cli
+            </Export-Package>
+            <Private-Package>
+              com.redhat.thermostat.vm.jmx.client.cli.internal,
+              com.redhat.thermostat.vm.jmx.client.cli.locale,
+            </Private-Package>
+            <!-- Do not autogenerate uses clauses in Manifests -->
+            <_nouses>true</_nouses>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-common-test</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.core</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.compendium</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-common-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-storage-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-client-command</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-client-cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-client-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-vm-jmx-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-vm-jmx-client-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+</project>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-cli/src/main/java/com/redhat/thermostat/vm/jmx/client/cli/NotificationsCommand.java	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,358 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code 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 this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.jmx.client.cli;
+
+import com.redhat.thermostat.client.cli.VmArgument;
+import com.redhat.thermostat.client.command.RequestQueue;
+import com.redhat.thermostat.common.ApplicationService;
+import com.redhat.thermostat.common.Clock;
+import com.redhat.thermostat.common.SystemClock;
+import com.redhat.thermostat.common.Timer;
+import com.redhat.thermostat.common.TimerFactory;
+import com.redhat.thermostat.common.Timers;
+import com.redhat.thermostat.common.cli.AbstractCompleterCommand;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.common.cli.DependencyServices;
+import com.redhat.thermostat.shared.locale.LocalizedString;
+import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.HostRef;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.dao.AgentInfoDAO;
+import com.redhat.thermostat.storage.dao.HostInfoDAO;
+import com.redhat.thermostat.storage.dao.VmInfoDAO;
+import com.redhat.thermostat.storage.model.HostInfo;
+import com.redhat.thermostat.storage.model.VmInfo;
+import com.redhat.thermostat.vm.jmx.client.cli.internal.NotificationsSinceParser;
+import com.redhat.thermostat.vm.jmx.client.cli.locale.LocaleResources;
+import com.redhat.thermostat.vm.jmx.client.core.JmxToggleNotificationRequest;
+import com.redhat.thermostat.vm.jmx.common.JmxNotification;
+import com.redhat.thermostat.vm.jmx.common.JmxNotificationDAO;
+import com.redhat.thermostat.vm.jmx.common.JmxNotificationStatus;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+public class NotificationsCommand extends AbstractCompleterCommand {
+
+    public static final String COMMAND_NAME = "notifications";
+
+    private static final String STATUS_SUBCOMMAND = "status";
+    private static final String SHOW_SUBCOMMAND = "show";
+    private static final String ENABLE_SUBCOMMAND = "enable";
+    private static final String DISABLE_SUBCOMMAND = "disable";
+    private static final String FOLLOW_SUBCOMMAND = "follow";
+
+    private static final String ENABLE_FOLLOW_OPTION = "follow";
+    private static final String SHOW_SINCE_OPTION = "since";
+
+    // in milliseconds
+    private static final long FOLLOW_LOOP_INPUT_PERIOD = 500L;
+    private static final long ENABLE_TO_FOLLOW_TRANSITION_DELAY = 500L;
+
+    private static final Translate<LocaleResources> t = LocaleResources.createLocalizer();
+
+    private static final NotificationsSinceParser sinceParser = new NotificationsSinceParser(new SystemClock());
+
+    private final DependencyServices dependencyServices = new DependencyServices();
+
+    @Override
+    public void run(CommandContext ctx) throws CommandException {
+        Arguments args = ctx.getArguments();
+
+        if (args.getNonOptionArguments().size() != 1) {
+            throw new CommandException(t.localize(LocaleResources.EXPECTED_ONE_NONOPTION_ARG));
+        }
+        String subcommand = args.getNonOptionArguments().get(0);
+
+        ApplicationService applicationService = getService(ApplicationService.class);
+        Clock clock = getService(Clock.class);
+        RequestQueue queue = getService(RequestQueue.class);
+        HostInfoDAO hostInfoDAO = getService(HostInfoDAO.class);
+        AgentInfoDAO agentInfoDAO = getService(AgentInfoDAO.class);
+        VmInfoDAO vmInfoDAO = getService(VmInfoDAO.class);
+        JmxNotificationDAO jmxNotificationDAO = getService(JmxNotificationDAO.class);
+
+        VmRef vmRef = getVmRef(args, vmInfoDAO, hostInfoDAO);
+
+        switch (subcommand) {
+            case STATUS_SUBCOMMAND:
+                handleStatusRequest(ctx, jmxNotificationDAO, vmRef);
+                break;
+            case DISABLE_SUBCOMMAND:
+                handleJmxStatusChange(subcommand, ctx, queue, jmxNotificationDAO, agentInfoDAO, vmRef);
+                break;
+            case ENABLE_SUBCOMMAND:
+                handleJmxStatusChange(subcommand, ctx, queue, jmxNotificationDAO, agentInfoDAO, vmRef);
+                if (args.hasArgument(ENABLE_FOLLOW_OPTION)) {
+                    try {
+                        Thread.sleep(ENABLE_TO_FOLLOW_TRANSITION_DELAY);
+                    } catch (InterruptedException e) {
+                        break;
+                    }
+                    // fall through to FOLLOW_SUBCOMMAND
+                } else {
+                    break;
+                }
+            case FOLLOW_SUBCOMMAND:
+                if (!isMonitoringEnabled(jmxNotificationDAO, vmRef)) {
+                    ctx.getConsole().getOutput().println(t.localize(LocaleResources.JMX_NOTIFICATION_MONITORING_FOLLOW_NOT_ENABLED).getContents());
+                    break;
+                }
+                ctx.getConsole().getOutput().println(t.localize(LocaleResources.EXIT_FOLLOW_MODE_HINT).getContents());
+                followNotificationsLoop(ctx, clock, applicationService.getTimerFactory(), jmxNotificationDAO, vmRef);
+                break;
+            case SHOW_SUBCOMMAND:
+                printNotifications(ctx, jmxNotificationDAO, vmRef, parseSinceOption(args));
+                break;
+            default:
+                throw new CommandException(t.localize(LocaleResources.UNRECOGNIZED_SUBCOMMAND, subcommand));
+        }
+    }
+
+    private <T> T getService(Class<T> klazz) throws CommandException {
+        T service = dependencyServices.getService(klazz);
+        requireNonNull(service, t.localize(LocaleResources.MISSING_REQUIRED_SERVICE));
+        return service;
+    }
+
+    private VmRef getVmRef(Arguments arguments, VmInfoDAO vmInfoDAO, HostInfoDAO hostInfoDAO) throws CommandException {
+        VmId vmId = VmArgument.required(arguments).getVmId();
+        VmInfo vmInfo = vmInfoDAO.getVmInfo(vmId);
+        if (vmInfo == null) {
+            throw new CommandException(t.localize(LocaleResources.UNKNOWN_VMID, vmId.get()));
+        }
+        AgentId agentId = new AgentId(vmInfo.getAgentId());
+        HostInfo hostInfo = hostInfoDAO.getHostInfo(agentId);
+        if (hostInfo == null) {
+            throw new CommandException(t.localize(LocaleResources.UNKNOWN_HOST));
+        }
+
+        HostRef hostRef = new HostRef(agentId.get(), hostInfo.getHostname());
+        return new VmRef(hostRef, vmInfo);
+    }
+
+    private void handleStatusRequest(CommandContext ctx, JmxNotificationDAO jmxNotificationDAO, VmRef vmRef) {
+        boolean monitoringEnabled = isMonitoringEnabled(jmxNotificationDAO, vmRef);
+        LocalizedString message = t.localize(monitoringEnabled ? LocaleResources.JMX_NOTIFICATION_MONITORING_STATUS_ENABLED
+                                                                : LocaleResources.JMX_NOTIFICATION_MONITORING_STATUS_DISABLED);
+        ctx.getConsole().getOutput().println(message.getContents());
+    }
+
+    private boolean isMonitoringEnabled(JmxNotificationDAO jmxNotificationDAO, VmRef vmRef) {
+        JmxNotificationStatus status = jmxNotificationDAO.getLatestNotificationStatus(vmRef);
+        return status != null && status.isEnabled();
+    }
+
+    private void handleJmxStatusChange(String subcommand, CommandContext ctx, RequestQueue queue,
+                                       JmxNotificationDAO jmxNotificationDAO, AgentInfoDAO agentInfoDAO, VmRef vmRef) {
+        if (!ENABLE_SUBCOMMAND.equals(subcommand) && !DISABLE_SUBCOMMAND.equals(subcommand)) {
+            throw new AssertionError("Invalid subcommand: " + subcommand);
+        }
+
+        boolean enable = ENABLE_SUBCOMMAND.equals(subcommand);
+        boolean currentlyEnabled = isMonitoringEnabled(jmxNotificationDAO, vmRef);
+        if (enable == currentlyEnabled) {
+            LocalizedString message = t.localize(enable ? LocaleResources.ALREADY_ENABLED : LocaleResources.ALREADY_DISABLED);
+            ctx.getConsole().getOutput().println(message.getContents());
+            return;
+        }
+
+        CountDownLatch latch = new CountDownLatch(1);
+        RequestCompleteAction action = new RequestCompleteAction(ctx, latch, enable);
+        JmxToggleNotificationRequest enableRequest =
+                new JmxToggleNotificationRequest(queue, agentInfoDAO, action.getSuccessAction(), action.getFailureAction());
+        enableRequest.sendEnableNotificationsRequestToAgent(vmRef, enable);
+        try {
+            latch.await();
+        } catch (InterruptedException e) {
+            // ignore
+        }
+    }
+
+    private long parseSinceOption(Arguments args) throws CommandException {
+        if (!args.hasArgument(SHOW_SINCE_OPTION)) {
+            return NotificationsSinceParser.NO_SINCE_OPTION;
+        }
+        String sinceArg = args.getArgument(SHOW_SINCE_OPTION);
+        return sinceParser.parse(sinceArg);
+    }
+
+    private void followNotificationsLoop(final CommandContext ctx, final Clock clock, TimerFactory timerFactory,
+                                         final JmxNotificationDAO jmxNotificationDAO, final VmRef vmRef) {
+
+        Timer pollTimer = null;
+        try {
+            // this timer asynchronously performs the DAO polling and results printing
+            pollTimer = createFollowModeTimer(ctx, clock, timerFactory, jmxNotificationDAO, vmRef);
+            pollTimer.start();
+
+            // loop the main thread waiting for user key input, which tells us to exit follow mode and stop the timer
+            while (continueFollowing(ctx, jmxNotificationDAO, vmRef)) {
+                try {
+                    Thread.sleep(FOLLOW_LOOP_INPUT_PERIOD);
+                } catch (InterruptedException e) {
+                    break;
+                }
+            }
+        } finally {
+            if (pollTimer != null) {
+                pollTimer.stop();
+            }
+        }
+    }
+
+    private Timer createFollowModeTimer(final CommandContext ctx, final Clock clock, TimerFactory timerFactory,
+                                        final JmxNotificationDAO jmxNotificationDAO, final VmRef vmRef) {
+        return Timers.createDataRefreshTimer(timerFactory, new Runnable() {
+            long lastUpdate = clock.getRealTimeMillis();
+
+            @Override
+            public void run() {
+                printNotifications(ctx, jmxNotificationDAO, vmRef, lastUpdate);
+                lastUpdate = clock.getRealTimeMillis();
+            }
+        });
+    }
+
+    private boolean continueFollowing(CommandContext ctx, JmxNotificationDAO jmxNotificationDAO, VmRef vmRef) {
+        try {
+            if (ctx.getConsole().getInput().available() > 0) {
+                return false;
+            }
+        } catch (IOException e) {
+            return false;
+        }
+
+        boolean monitoringActive = isMonitoringEnabled(jmxNotificationDAO, vmRef);
+        if (!monitoringActive) {
+            ctx.getConsole().getOutput().println(t.localize(LocaleResources.JMX_NOTIFICATION_MONITORING_FOLLOW_INTERRUPTED).getContents());
+        }
+        return monitoringActive;
+    }
+
+    private void printNotifications(CommandContext ctx, JmxNotificationDAO jmxNotificationDAO, VmRef vmRef, long since) {
+        List<JmxNotification> notifications = jmxNotificationDAO.getNotifications(vmRef, since);
+        for (JmxNotification notification : notifications) {
+            String timestamp = Clock.DEFAULT_DATE_FORMAT.format(new Date(notification.getTimeStamp()));
+            String details = notification.getSourceDetails();
+            String contents = notification.getContents();
+            ctx.getConsole().getOutput().println(
+                    t.localize(LocaleResources.PRINT_NOTIFICATIONS_FORMAT, timestamp, details, contents).getContents());
+        }
+    }
+
+    public void bindApplicationService(ApplicationService applicationService) {
+        dependencyServices.addService(ApplicationService.class, applicationService);
+    }
+
+    public void bindClock(Clock clock) {
+        dependencyServices.addService(Clock.class, clock);
+    }
+
+    public void bindRequestQueue(RequestQueue requestQueue) {
+        dependencyServices.addService(RequestQueue.class, requestQueue);
+    }
+
+    public void bindHostInfoDao(HostInfoDAO hostInfoDAO) {
+        dependencyServices.addService(HostInfoDAO.class, hostInfoDAO);
+    }
+
+    public void bindAgentInfoDao(AgentInfoDAO agentInfoDAO) {
+        dependencyServices.addService(AgentInfoDAO.class, agentInfoDAO);
+    }
+
+    public void bindVmInfoDao(VmInfoDAO vmInfoDAO) {
+        dependencyServices.addService(VmInfoDAO.class, vmInfoDAO);
+    }
+
+    public void bindJmxNotificationDao(JmxNotificationDAO jmxNotificationDAO) {
+        dependencyServices.addService(JmxNotificationDAO.class, jmxNotificationDAO);
+    }
+
+    public void dependenciesUnavailable() {
+        dependencyServices.removeService(ApplicationService.class);
+        dependencyServices.removeService(Clock.class);
+        dependencyServices.removeService(RequestQueue.class);
+        dependencyServices.removeService(HostInfoDAO.class);
+        dependencyServices.removeService(AgentInfoDAO.class);
+        dependencyServices.removeService(VmInfoDAO.class);
+        dependencyServices.removeService(JmxNotificationDAO.class);
+    }
+
+    private static class RequestCompleteAction {
+
+        private final CommandContext ctx;
+        private final CountDownLatch latch;
+        private final boolean enableRequest;
+
+        RequestCompleteAction(CommandContext ctx, CountDownLatch latch, boolean enableRequest) {
+            this.ctx = ctx;
+            this.latch = latch;
+            this.enableRequest = enableRequest;
+        }
+
+        Runnable getSuccessAction() {
+            return new Runnable() {
+                @Override
+                public void run() {
+                    LocaleResources message = enableRequest ? LocaleResources.ENABLE_SUCCESS : LocaleResources.DISABLE_SUCCESS;
+                    ctx.getConsole().getOutput().println(t.localize(message).getContents());
+                    latch.countDown();
+                }
+            };
+        }
+
+        Runnable getFailureAction() {
+            return new Runnable() {
+                @Override
+                public void run() {
+                    LocaleResources message = enableRequest ? LocaleResources.ENABLE_FAILURE : LocaleResources.DISABLE_FAILURE;
+                    ctx.getConsole().getError().println(t.localize(message).getContents());
+                    latch.countDown();
+                }
+            };
+        }
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-cli/src/main/java/com/redhat/thermostat/vm/jmx/client/cli/internal/Activator.java	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code 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 this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.jmx.client.cli.internal;
+
+import com.redhat.thermostat.client.command.RequestQueue;
+import com.redhat.thermostat.common.ApplicationService;
+import com.redhat.thermostat.common.MultipleServiceTracker;
+import com.redhat.thermostat.common.SystemClock;
+import com.redhat.thermostat.common.cli.Command;
+import com.redhat.thermostat.storage.dao.AgentInfoDAO;
+import com.redhat.thermostat.storage.dao.HostInfoDAO;
+import com.redhat.thermostat.storage.dao.VmInfoDAO;
+import com.redhat.thermostat.vm.jmx.client.cli.NotificationsCommand;
+import com.redhat.thermostat.vm.jmx.common.JmxNotificationDAO;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+public class Activator implements BundleActivator {
+
+    private MultipleServiceTracker depsTracker;
+    private ServiceRegistration registration;
+
+    @Override
+    public void start(final BundleContext context) throws Exception {
+        final Class<?>[] deps = new Class<?>[] {
+                ApplicationService.class,
+                RequestQueue.class,
+                HostInfoDAO.class,
+                AgentInfoDAO.class,
+                VmInfoDAO.class,
+                JmxNotificationDAO.class,
+        };
+
+        final NotificationsCommand cmd = new NotificationsCommand();
+        depsTracker = new MultipleServiceTracker(context, deps, new MultipleServiceTracker.Action() {
+            @Override
+            public void dependenciesAvailable(MultipleServiceTracker.DependencyProvider services) {
+                ApplicationService applicationService = services.get(ApplicationService.class);
+                RequestQueue queue = services.get(RequestQueue.class);
+                HostInfoDAO hostDao = services.get(HostInfoDAO.class);
+                AgentInfoDAO agentDao = services.get(AgentInfoDAO.class);
+                VmInfoDAO vmDao = services.get(VmInfoDAO.class);
+                JmxNotificationDAO jmxDao = services.get(JmxNotificationDAO.class);
+
+                cmd.bindApplicationService(applicationService);
+                cmd.bindClock(new SystemClock());
+                cmd.bindRequestQueue(queue);
+                cmd.bindHostInfoDao(hostDao);
+                cmd.bindAgentInfoDao(agentDao);
+                cmd.bindVmInfoDao(vmDao);
+                cmd.bindJmxNotificationDao(jmxDao);
+            }
+
+            @Override
+            public void dependenciesUnavailable() {
+                cmd.dependenciesUnavailable();
+            }
+
+        });
+        depsTracker.open();
+
+        Dictionary<String, String> props = new Hashtable<>();
+        props.put(Command.NAME, NotificationsCommand.COMMAND_NAME);
+        registration = context.registerService(Command.class.getName(), cmd, props);
+    }
+
+    @Override
+    public void stop(BundleContext context) throws Exception {
+        if (depsTracker != null) {
+            depsTracker.close();
+        }
+        if (registration != null) {
+            registration.unregister();
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-cli/src/main/java/com/redhat/thermostat/vm/jmx/client/cli/internal/NotificationsSinceParser.java	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code 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 this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.jmx.client.cli.internal;
+
+import com.redhat.thermostat.common.Clock;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.vm.jmx.client.cli.locale.LocaleResources;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class NotificationsSinceParser {
+
+    public static final long NO_SINCE_OPTION = -1L;
+
+    private static final Pattern SINCE_SECONDS_OFFSET_PATTERN = Pattern.compile("^([0-9]+)s$");
+    private static final Pattern SINCE_MINUTES_OFFSET_PATTERN = Pattern.compile("^([0-9]+)m$");
+    private static final Pattern SINCE_HOURS_OFFSET_PATTERN = Pattern.compile("^([0-9]+)h$");
+    private static final Pattern SINCE_DAYS_OFFSET_PATTERN = Pattern.compile("^([0-9]+)d$");
+
+    private static final Translate<LocaleResources> t = LocaleResources.createLocalizer();
+
+    private final Clock clock;
+
+    public NotificationsSinceParser(Clock clock) {
+        this.clock = clock;
+    }
+
+    public long parse(String sinceArg) throws CommandException {
+        try {
+            if (hasSinceSecondsArgument(sinceArg)) {
+                return getSinceSecondsArgument(sinceArg);
+            } else if (hasSinceMinutesArgument(sinceArg)) {
+                return getSinceMinutesArgument(sinceArg);
+            } else if (hasSinceHoursArgument(sinceArg)) {
+                return getSinceHoursArgument(sinceArg);
+            } else if (hasSinceDaysArgument(sinceArg)) {
+                return getSinceDaysArgument(sinceArg);
+            }
+            long parsed = Long.parseLong(sinceArg);
+            if (parsed < NO_SINCE_OPTION) {
+                parsed = NO_SINCE_OPTION;
+            }
+            return parsed;
+        } catch (NumberFormatException nfe) {
+            throw new CommandException(t.localize(LocaleResources.UNRECOGNIZED_SINCE_FORMAT), nfe);
+        }
+    }
+
+    private boolean hasSinceSecondsArgument(String s) {
+        return SINCE_SECONDS_OFFSET_PATTERN.matcher(s).matches();
+    }
+
+    private long getSinceSecondsArgument(String s) {
+        Matcher matcher = SINCE_SECONDS_OFFSET_PATTERN.matcher(s);
+        verifyMatcher(matcher);
+        long seconds = Long.parseLong(matcher.group(1));
+        return clock.getRealTimeMillis() - TimeUnit.SECONDS.toMillis(seconds);
+    }
+
+    private boolean hasSinceMinutesArgument(String s) {
+        return SINCE_MINUTES_OFFSET_PATTERN.matcher(s).matches();
+    }
+
+    private long getSinceMinutesArgument(String s) {
+        Matcher matcher = SINCE_MINUTES_OFFSET_PATTERN.matcher(s);
+        verifyMatcher(matcher);
+        long minutes = Long.parseLong(matcher.group(1));
+        return clock.getRealTimeMillis() - TimeUnit.MINUTES.toMillis(minutes);
+    }
+
+    private boolean hasSinceHoursArgument(String s) {
+        return SINCE_HOURS_OFFSET_PATTERN.matcher(s).matches();
+    }
+
+    private long getSinceHoursArgument(String s) {
+        Matcher matcher = SINCE_HOURS_OFFSET_PATTERN.matcher(s);
+        verifyMatcher(matcher);
+        long hours = Long.parseLong(matcher.group(1));
+        return clock.getRealTimeMillis() - TimeUnit.HOURS.toMillis(hours);
+    }
+
+    private boolean hasSinceDaysArgument(String s) {
+        return SINCE_DAYS_OFFSET_PATTERN.matcher(s).matches();
+    }
+
+    private long getSinceDaysArgument(String s) {
+        Matcher matcher = SINCE_DAYS_OFFSET_PATTERN.matcher(s);
+        verifyMatcher(matcher);
+        long days = Long.parseLong(matcher.group(1));
+        return clock.getRealTimeMillis() - TimeUnit.DAYS.toMillis(days);
+    }
+
+    private void verifyMatcher(Matcher matcher) {
+        if (!matcher.matches()) {
+            throw new AssertionError("Invalid \"since\" format");
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-cli/src/main/java/com/redhat/thermostat/vm/jmx/client/cli/locale/LocaleResources.java	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code 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 this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.jmx.client.cli.locale;
+
+import com.redhat.thermostat.shared.locale.Translate;
+
+public enum LocaleResources {
+
+    EXPECTED_ONE_NONOPTION_ARG,
+    UNRECOGNIZED_SUBCOMMAND,
+
+    MISSING_REQUIRED_SERVICE,
+
+    UNKNOWN_VMID,
+    UNKNOWN_HOST,
+
+    JMX_NOTIFICATION_MONITORING_STATUS_ENABLED,
+    JMX_NOTIFICATION_MONITORING_STATUS_DISABLED,
+
+    JMX_NOTIFICATION_MONITORING_FOLLOW_NOT_ENABLED,
+    JMX_NOTIFICATION_MONITORING_FOLLOW_INTERRUPTED,
+
+    ALREADY_ENABLED,
+    ALREADY_DISABLED,
+
+    ENABLE_SUCCESS,
+    ENABLE_FAILURE,
+
+    DISABLE_SUCCESS,
+    DISABLE_FAILURE,
+
+    EXIT_FOLLOW_MODE_HINT,
+
+    UNRECOGNIZED_SINCE_FORMAT,
+
+    PRINT_NOTIFICATIONS_FORMAT,
+
+    ;
+
+    static final String RESOURCE_BUNDLE = "com.redhat.thermostat.vm.jmx.client.cli.locale.strings";
+
+    public static Translate<LocaleResources> createLocalizer() {
+        return new Translate<>(RESOURCE_BUNDLE, LocaleResources.class);
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-cli/src/main/resources/com/redhat/thermostat/vm/jmx/client/cli/locale/strings.properties	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,28 @@
+EXPECTED_ONE_NONOPTION_ARG=Expected one non-option argument (subcommand)
+UNRECOGNIZED_SUBCOMMAND=Unrecognized subcommand: {0}
+
+MISSING_REQUIRED_SERVICE=Required service {0} is missing
+
+UNKNOWN_VMID=Unknown VM ID: {0}
+UNKNOWN_HOST=Unknown host
+
+JMX_NOTIFICATION_MONITORING_STATUS_ENABLED=JMX notification monitoring is enabled
+JMX_NOTIFICATION_MONITORING_STATUS_DISABLED=JMX notification monitoring is disabled
+
+JMX_NOTIFICATION_MONITORING_FOLLOW_NOT_ENABLED=JMX notification monitoring is not enabled for this JVM - notifications cannot be followed
+JMX_NOTIFICATION_MONITORING_FOLLOW_INTERRUPTED=JMX notification monitoring interrupted
+
+ALREADY_ENABLED=JMX notification monitoring is already enabled
+ALREADY_DISABLED=JMX notification monitoring is not enabled
+
+ENABLE_SUCCESS=JMX notification monitoring enabled
+ENABLE_FAILURE=Failed to enable JMX notification monitoring
+
+DISABLE_SUCCESS=JMX notification monitoring disabled
+DISABLE_FAILURE=Failed to disable JMX notification monitoring
+
+EXIT_FOLLOW_MODE_HINT=Press any key to exit follow mode...
+
+UNRECOGNIZED_SINCE_FORMAT=Failed to parse "since" option argument
+
+PRINT_NOTIFICATIONS_FORMAT=[{0}] ({1}) : {2}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-cli/src/test/java/com/redhat/thermostat/vm/jmx/client/cli/NotificationsCommandTest.java	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,552 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code 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 this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.jmx.client.cli;
+
+import com.redhat.thermostat.client.cli.VmArgument;
+import com.redhat.thermostat.client.command.RequestQueue;
+import com.redhat.thermostat.common.ApplicationService;
+import com.redhat.thermostat.common.Clock;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.common.cli.Console;
+import com.redhat.thermostat.common.command.Request;
+import com.redhat.thermostat.common.command.RequestResponseListener;
+import com.redhat.thermostat.common.command.Response;
+import com.redhat.thermostat.common.internal.test.TestTimerFactory;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.dao.AgentInfoDAO;
+import com.redhat.thermostat.storage.dao.HostInfoDAO;
+import com.redhat.thermostat.storage.dao.VmInfoDAO;
+import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.storage.model.HostInfo;
+import com.redhat.thermostat.storage.model.VmInfo;
+import com.redhat.thermostat.vm.jmx.common.JmxNotification;
+import com.redhat.thermostat.vm.jmx.common.JmxNotificationDAO;
+import com.redhat.thermostat.vm.jmx.common.JmxNotificationStatus;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class NotificationsCommandTest {
+
+    private static final String NOTIFICATION_OUTPUT = "[31-Dec-1969 7:00:00 EST PM] (notification source details) : notification contents";
+    private static final String FAR_FUTURE_NOTIFICATION_OUTPUT = "[17-Aug-292278994 2:12:55 EST AM] (notification source details) : notification contents";
+
+    private static final String JMX_NOTIFICATION_MONITORING_ENABLED = "JMX notification monitoring enabled";
+    private static final String PRESS_ANY_KEY_TO_EXIT_FOLLOW_MODE = "Press any key to exit follow mode...";
+    private static final String JMX_NOTIFICATION_MONITORING_INTERRUPTED = "JMX notification monitoring interrupted";
+    private static final String JMX_NOTIFICATION_MONITORING_IS_ALREADY_ENABLED = "JMX notification monitoring is already enabled";
+    private static final String JMX_NOTIFICATION_MONITORING_IS_NOT_ENABLED = "JMX notification monitoring is not enabled";
+    private static final String JMX_NOTIFICATION_MONITORING_DISABLED = "JMX notification monitoring disabled";
+    private static final String JMX_NOTIFICATION_MONITORING_IS_DISABLED = "JMX notification monitoring is disabled";
+    private static final String JMX_NOTIFICATION_MONITORING_IS_ENABLED = "JMX notification monitoring is enabled";
+
+    private static final String FOO_AGENTID = "foo-agentid";
+    private static final String FOO_HOSTNAME = "foo-hostname";
+    private static final String FOO_VMID = "foo-vmid";
+
+    private static final String STATUS_SUBCOMMAND = "status";
+    private static final String DISABLE_SUBCOMMAND = "disable";
+    private static final String ENABLE_SUBCOMMAND = "enable";
+    private static final String FOLLOW_SUBCOMMAND = "follow";
+    private static final String SHOW_SUBCOMMAND = "show";
+
+    private static final String FOLLOW_OPTION = "follow";
+    private static final String SINCE_OPTION = "since";
+
+    private ApplicationService applicationService;
+    private Clock clock;
+    private RequestQueue requestQueue;
+    private HostInfoDAO hostInfoDAO;
+    private AgentInfoDAO agentInfoDAO;
+    private VmInfoDAO vmInfoDAO;
+    private JmxNotificationDAO jmxNotificationDAO;
+
+    private JmxNotificationStatus jmxNotificationStatus;
+    private JmxNotification jmxNotification;
+
+    private Arguments args;
+    private PrintStream outStream;
+    private InputStream inStream;
+    private ArgumentCaptor<String> outCaptor;
+    private CommandContext ctx;
+
+    private NotificationsCommand cmd;
+    private TestTimerFactory timerFactory;
+
+    @Before
+    public void setup() {
+        timerFactory = new TestTimerFactory();
+        applicationService = mock(ApplicationService.class);
+        clock = mock(Clock.class);
+        requestQueue = mock(RequestQueue.class);
+        hostInfoDAO = mock(HostInfoDAO.class);
+        agentInfoDAO = mock(AgentInfoDAO.class);
+        vmInfoDAO = mock(VmInfoDAO.class);
+        jmxNotificationDAO = mock(JmxNotificationDAO.class);
+
+        Console console = mock(Console.class);
+        outStream = mock(PrintStream.class);
+        inStream = mock(InputStream.class);
+        outCaptor = ArgumentCaptor.forClass(String.class);
+        when(console.getOutput()).thenReturn(outStream);
+        when(console.getInput()).thenReturn(inStream);
+
+        args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn(FOO_VMID);
+        ctx = mock(CommandContext.class);
+        when(ctx.getArguments()).thenReturn(args);
+        when(ctx.getConsole()).thenReturn(console);
+
+        cmd = new NotificationsCommand();
+        cmd.bindApplicationService(applicationService);
+        cmd.bindClock(clock);
+        cmd.bindRequestQueue(requestQueue);
+        cmd.bindHostInfoDao(hostInfoDAO);
+        cmd.bindAgentInfoDao(agentInfoDAO);
+        cmd.bindVmInfoDao(vmInfoDAO);
+        cmd.bindJmxNotificationDao(jmxNotificationDAO);
+
+        setupMocks();
+    }
+
+    private void setupMocks() {
+        when(applicationService.getTimerFactory()).thenReturn(timerFactory);
+        when(clock.getRealTimeMillis()).thenReturn(1L);
+
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
+                Request request = (Request) invocationOnMock.getArguments()[0];
+                Response response = new Response(Response.ResponseType.OK);
+                for (RequestResponseListener listener : request.getListeners()) {
+                    listener.fireComplete(request, response);
+                }
+                return null;
+            }
+        }).when(requestQueue).putRequest(any(Request.class));
+
+        HostInfo hostInfo = new HostInfo();
+        hostInfo.setAgentId(FOO_AGENTID);
+        hostInfo.setHostname(FOO_HOSTNAME);
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
+        when(hostInfoDAO.getAllHostInfos()).thenReturn(Collections.singletonList(hostInfo));
+
+        AgentInformation agentInfo = new AgentInformation();
+        agentInfo.setAgentId(FOO_AGENTID);
+        agentInfo.setConfigListenAddress("127.0.0.1:22");
+        when(agentInfoDAO.getAgentIds()).thenReturn(Collections.singleton(new AgentId(FOO_AGENTID)));
+        when(agentInfoDAO.getAliveAgentIds()).thenReturn(Collections.singleton(new AgentId(FOO_AGENTID)));
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(agentInfo);
+        when(agentInfoDAO.getAliveAgents()).thenReturn(Collections.singletonList(agentInfo));
+        when(agentInfoDAO.getAllAgentInformation()).thenReturn(Collections.singletonList(agentInfo));
+
+        VmInfo vmInfo = new VmInfo();
+        vmInfo.setAgentId(FOO_AGENTID);
+        vmInfo.setVmId(FOO_VMID);
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(vmInfoDAO.getAllVmInfos()).thenReturn(Collections.singletonList(vmInfo));
+        when(vmInfoDAO.getAllVmInfosForAgent(any(AgentId.class))).thenReturn(Collections.singletonList(vmInfo));
+        when(vmInfoDAO.getVmIds(any(AgentId.class))).thenReturn(Collections.singleton(new VmId(FOO_VMID)));
+        when(vmInfoDAO.getVmInfo(any(VmRef.class))).thenReturn(vmInfo);
+
+        jmxNotificationStatus = new JmxNotificationStatus(FOO_AGENTID);
+        jmxNotificationStatus.setVmId(FOO_VMID);
+        jmxNotificationStatus.setTimeStamp(100L);
+        jmxNotificationStatus.setEnabled(true);
+        jmxNotification = new JmxNotification(FOO_AGENTID);
+        jmxNotification.setVmId(FOO_VMID);
+        jmxNotification.setTimeStamp(110L);
+        jmxNotification.setContents("notification contents");
+        jmxNotification.setSourceBackend("jmxBackend");
+        jmxNotification.setSourceDetails("notification source details");
+        when(jmxNotificationDAO.getLatestNotificationStatus(any(VmRef.class))).thenReturn(jmxNotificationStatus);
+        when(jmxNotificationDAO.getNotifications(any(VmRef.class), anyLong())).thenAnswer(new Answer<List<JmxNotification>>() {
+            @Override
+            public List<JmxNotification> answer(InvocationOnMock invocationOnMock) throws Throwable {
+                long timestamp = (Long) invocationOnMock.getArguments()[1];
+                if (timestamp < jmxNotification.getTimeStamp()) {
+                    return Collections.singletonList(jmxNotification);
+                } else {
+                    return Collections.emptyList();
+                }
+            }
+        });
+    }
+
+    @Test
+    public void testStatusWhenMonitoringIsEnabled() throws CommandException {
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_ENABLED));
+    }
+
+    @Test
+    public void testStatusWhenMonitoringIsDisabled() throws CommandException {
+        jmxNotificationStatus.setEnabled(false);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_DISABLED));
+    }
+
+    @Test
+    public void testStatusWhenMonitoringStatusIsUnknown() throws CommandException {
+        when(jmxNotificationDAO.getLatestNotificationStatus(any(VmRef.class))).thenReturn(null);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_DISABLED));
+    }
+
+    @Test
+    public void testDisableWhenMonitoringIsEnabled() throws CommandException {
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(DISABLE_SUBCOMMAND));
+        assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_DISABLED));
+    }
+
+    @Test
+    public void testDisableWhenMonitoringIsDisabled() throws CommandException {
+        jmxNotificationStatus.setEnabled(false);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(DISABLE_SUBCOMMAND));
+        assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_NOT_ENABLED));
+    }
+
+    @Test
+    public void testDisableWhenMonitoringStatusIsUnknown() throws CommandException {
+        when(jmxNotificationDAO.getLatestNotificationStatus(any(VmRef.class))).thenReturn(null);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(DISABLE_SUBCOMMAND));
+        assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_NOT_ENABLED));
+    }
+
+    @Test
+    public void testEnableWhenMonitoringIsEnabled() throws CommandException {
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_IS_ALREADY_ENABLED));
+    }
+
+    @Test
+    public void testEnableWhenMonitoringIsDisabled() throws CommandException {
+        jmxNotificationStatus.setEnabled(false);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_ENABLED));
+    }
+
+    @Test
+    public void testEnableWhenMonitoringStatusIsUnknown() throws CommandException {
+        when(jmxNotificationDAO.getLatestNotificationStatus(any(VmRef.class))).thenReturn(null);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        assertThat(runCommandForOutput(), is(JMX_NOTIFICATION_MONITORING_ENABLED));
+    }
+
+    @Test
+    public void testEnableWithFollowOption() throws CommandException, IOException {
+        jmxNotificationStatus.setEnabled(false);
+        jmxNotification.setTimeStamp(Long.MAX_VALUE);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        when(args.hasArgument(FOLLOW_OPTION)).thenReturn(true);
+
+        doFollowTestWithKeyboardInterrupt();
+
+        verify(outStream, times(3)).println(outCaptor.capture());
+        String status = outCaptor.getAllValues().get(0);
+        String hint = outCaptor.getAllValues().get(1);
+        String firstNotification = outCaptor.getAllValues().get(2);
+        assertThat(status, is(JMX_NOTIFICATION_MONITORING_ENABLED));
+        assertThat(hint, is(PRESS_ANY_KEY_TO_EXIT_FOLLOW_MODE));
+        assertThat(firstNotification, is(FAR_FUTURE_NOTIFICATION_OUTPUT));
+    }
+
+    @Test
+    public void testEnableWithFollowOptionWhenAlreadyEnabled() throws CommandException, IOException {
+        jmxNotification.setTimeStamp(Long.MAX_VALUE);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        when(args.hasArgument(FOLLOW_OPTION)).thenReturn(true);
+
+        doFollowTestWithKeyboardInterrupt();
+
+        verify(outStream, times(3)).println(outCaptor.capture());
+        String status = outCaptor.getAllValues().get(0);
+        String hint = outCaptor.getAllValues().get(1);
+        String firstNotification = outCaptor.getAllValues().get(2);
+        assertThat(status, is(JMX_NOTIFICATION_MONITORING_IS_ALREADY_ENABLED));
+        assertThat(hint, is(PRESS_ANY_KEY_TO_EXIT_FOLLOW_MODE));
+        assertThat(firstNotification, is(FAR_FUTURE_NOTIFICATION_OUTPUT));
+    }
+
+    @Test
+    public void testEnableWithFollowOptionWithExternalInterrupt() throws CommandException, IOException {
+        jmxNotificationStatus.setEnabled(false);
+        jmxNotification.setTimeStamp(Long.MAX_VALUE);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(ENABLE_SUBCOMMAND));
+        when(args.hasArgument(FOLLOW_OPTION)).thenReturn(true);
+
+        doFollowTestWithExternalMonitoringInterrupt();
+
+        verify(outStream, times(4)).println(outCaptor.capture());
+        String status = outCaptor.getAllValues().get(0);
+        String hint = outCaptor.getAllValues().get(1);
+        String firstNotification = outCaptor.getAllValues().get(2);
+        String interruptNotice = outCaptor.getAllValues().get(3);
+        assertThat(status, is(JMX_NOTIFICATION_MONITORING_ENABLED));
+        assertThat(hint, is(PRESS_ANY_KEY_TO_EXIT_FOLLOW_MODE));
+        assertThat(firstNotification, is(FAR_FUTURE_NOTIFICATION_OUTPUT));
+        assertThat(interruptNotice, is(JMX_NOTIFICATION_MONITORING_INTERRUPTED));
+    }
+
+    @Test
+    public void testFollowWhenDisabled() throws CommandException {
+        jmxNotificationStatus.setEnabled(false);
+        jmxNotification.setTimeStamp(Long.MAX_VALUE);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(FOLLOW_SUBCOMMAND));
+
+        assertThat(runCommandForOutput(), is("JMX notification monitoring is not enabled for this JVM - notifications cannot be followed"));
+    }
+
+    @Test
+    public void testFollowWhenEnabled() throws CommandException, IOException {
+        jmxNotification.setTimeStamp(Long.MAX_VALUE);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(FOLLOW_SUBCOMMAND));
+
+        doFollowTestWithKeyboardInterrupt();
+
+        verify(outStream, times(2)).println(outCaptor.capture());
+        String hint = outCaptor.getAllValues().get(0);
+        String firstNotification = outCaptor.getAllValues().get(1);
+        assertThat(hint, is(PRESS_ANY_KEY_TO_EXIT_FOLLOW_MODE));
+        assertThat(firstNotification, is(FAR_FUTURE_NOTIFICATION_OUTPUT));
+    }
+
+    @Test
+    public void testFollowWithExternalInterrupt() throws CommandException, IOException {
+        jmxNotification.setTimeStamp(Long.MAX_VALUE);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(FOLLOW_SUBCOMMAND));
+
+        doFollowTestWithExternalMonitoringInterrupt();
+
+        verify(outStream, times(3)).println(outCaptor.capture());
+        String hint = outCaptor.getAllValues().get(0);
+        String firstNotification = outCaptor.getAllValues().get(1);
+        String interruptNotice = outCaptor.getAllValues().get(2);
+        assertThat(hint, is(PRESS_ANY_KEY_TO_EXIT_FOLLOW_MODE));
+        assertThat(firstNotification, is(FAR_FUTURE_NOTIFICATION_OUTPUT));
+        assertThat(interruptNotice, is(JMX_NOTIFICATION_MONITORING_INTERRUPTED));
+    }
+
+    private void doFollowTestWithKeyboardInterrupt() throws IOException {
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
+                jmxNotificationStatus.setEnabled(true);
+                Request request = (Request) invocationOnMock.getArguments()[0];
+                Response response = new Response(Response.ResponseType.OK);
+                for (RequestResponseListener listener : request.getListeners()) {
+                    listener.fireComplete(request, response);
+                }
+                return null;
+            }
+        }).when(requestQueue).putRequest(any(Request.class));
+
+        Thread helper = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    cmd.run(ctx);
+                } catch (CommandException e) {
+                    fail();
+                }
+            }
+        });
+        helper.start();
+        try {
+            Thread.sleep(750);
+            timerFactory.getAction().run();
+        } catch (InterruptedException ignored) {
+        }
+        when(inStream.available()).thenReturn(1);
+        try {
+            helper.join();
+        } catch (InterruptedException ignored) {
+        }
+    }
+
+    private void doFollowTestWithExternalMonitoringInterrupt() throws IOException {
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
+                jmxNotificationStatus.setEnabled(true);
+                Request request = (Request) invocationOnMock.getArguments()[0];
+                Response response = new Response(Response.ResponseType.OK);
+                for (RequestResponseListener listener : request.getListeners()) {
+                    listener.fireComplete(request, response);
+                }
+                return null;
+            }
+        }).when(requestQueue).putRequest(any(Request.class));
+
+        Thread helper = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    cmd.run(ctx);
+                } catch (CommandException e) {
+                    fail();
+                }
+            }
+        });
+        helper.start();
+        try {
+            Thread.sleep(750);
+            timerFactory.getAction().run();
+        } catch (InterruptedException ignored) {
+        }
+        jmxNotificationStatus.setEnabled(false);
+        try {
+            helper.join();
+        } catch (InterruptedException ignored) {
+        }
+    }
+
+    @Test
+    public void testShow() throws CommandException {
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(SHOW_SUBCOMMAND));
+        assertThat(runCommandForOutput(), is(NOTIFICATION_OUTPUT));
+    }
+
+    @Test
+    public void testShowSinceWithTimestampNewerThanData() throws CommandException {
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(SHOW_SUBCOMMAND));
+        when(args.hasArgument(SINCE_OPTION)).thenReturn(true);
+        when(args.getArgument(SINCE_OPTION)).thenReturn("500");
+        cmd.run(ctx);
+        verifyZeroInteractions(outStream);
+    }
+
+    @Test
+    public void testShowSinceWithTimestampOlderThanData() throws CommandException {
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(SHOW_SUBCOMMAND));
+        when(args.hasArgument(SINCE_OPTION)).thenReturn(true);
+        when(args.getArgument(SINCE_OPTION)).thenReturn("1");
+        assertThat(runCommandForOutput(), is(NOTIFICATION_OUTPUT));
+    }
+
+    private String runCommandForOutput() throws CommandException {
+        cmd.run(ctx);
+        verify(outStream).println(outCaptor.capture());
+        return outCaptor.getValue();
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRequiresRequestQueue() throws CommandException {
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        cmd.dependenciesUnavailable();
+//        cmd.bindRequestQueue(requestQueue);
+        cmd.bindHostInfoDao(hostInfoDAO);
+        cmd.bindAgentInfoDao(agentInfoDAO);
+        cmd.bindVmInfoDao(vmInfoDAO);
+        cmd.bindJmxNotificationDao(jmxNotificationDAO);
+        cmd.run(ctx);
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRequiresHostInfoDao() throws CommandException {
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        cmd.dependenciesUnavailable();
+        cmd.bindRequestQueue(requestQueue);
+//        cmd.bindHostInfoDao(hostInfoDAO);
+        cmd.bindAgentInfoDao(agentInfoDAO);
+        cmd.bindVmInfoDao(vmInfoDAO);
+        cmd.bindJmxNotificationDao(jmxNotificationDAO);
+        cmd.run(ctx);
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRequiresAgentInfoDao() throws CommandException {
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        cmd.dependenciesUnavailable();
+        cmd.bindRequestQueue(requestQueue);
+        cmd.bindHostInfoDao(hostInfoDAO);
+//        cmd.bindAgentInfoDao(agentInfoDAO);
+        cmd.bindVmInfoDao(vmInfoDAO);
+        cmd.bindJmxNotificationDao(jmxNotificationDAO);
+        cmd.run(ctx);
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRequiresVmInfoDao() throws CommandException {
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        cmd.dependenciesUnavailable();
+        cmd.bindRequestQueue(requestQueue);
+        cmd.bindHostInfoDao(hostInfoDAO);
+        cmd.bindAgentInfoDao(agentInfoDAO);
+//        cmd.bindVmInfoDao(vmInfoDAO);
+        cmd.bindJmxNotificationDao(jmxNotificationDAO);
+        cmd.run(ctx);
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRequiresJmxNotificationDao() throws CommandException {
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(STATUS_SUBCOMMAND));
+        cmd.dependenciesUnavailable();
+        cmd.bindRequestQueue(requestQueue);
+        cmd.bindHostInfoDao(hostInfoDAO);
+        cmd.bindAgentInfoDao(agentInfoDAO);
+        cmd.bindVmInfoDao(vmInfoDAO);
+//        cmd.bindJmxNotificationDao(jmxNotificationDAO);
+        cmd.run(ctx);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-cli/src/test/java/com/redhat/thermostat/vm/jmx/client/cli/internal/ActivatorTest.java	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code 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 this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.jmx.client.cli.internal;
+
+import com.redhat.thermostat.common.cli.Command;
+import com.redhat.thermostat.testutils.StubBundleContext;
+import com.redhat.thermostat.vm.jmx.client.cli.NotificationsCommand;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ActivatorTest {
+
+    @Test
+    public void testActivator() throws Exception {
+        StubBundleContext bundleContext = new StubBundleContext();
+        Activator activator = new Activator();
+
+        activator.start(bundleContext);
+
+        assertTrue(bundleContext.isServiceRegistered(Command.class.getName(), NotificationsCommand.class));
+
+        activator.stop(bundleContext);
+
+        assertFalse(bundleContext.isServiceRegistered(Command.class.getName(), NotificationsCommand.class));
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-cli/src/test/java/com/redhat/thermostat/vm/jmx/client/cli/internal/NotificationsSinceParserTest.java	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code 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 this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.jmx.client.cli.internal;
+
+import com.redhat.thermostat.common.Clock;
+import com.redhat.thermostat.common.cli.CommandException;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class NotificationsSinceParserTest {
+
+    private static final long FAKE_CURRENT_TIMESTAMP = 100_000_000L;
+    private Clock clock;
+    private NotificationsSinceParser parser;
+
+    @Before
+    public void setup() {
+        clock = mock(Clock.class);
+        when(clock.getRealTimeMillis()).thenReturn(FAKE_CURRENT_TIMESTAMP);
+        parser = new NotificationsSinceParser(clock);
+    }
+
+    @Test
+    public void testPlainLongTimestamp() throws CommandException {
+        long result = parser.parse("100");
+        assertThat(result, is(100L));
+    }
+
+    @Test
+    public void testNegativeLongTimestamp() throws CommandException {
+        long result = parser.parse("-5");
+        assertThat(result, is(-1L));
+    }
+
+    @Test(expected = CommandException.class)
+    public void testInvalidOffsetSuffix() throws CommandException {
+        parser.parse("1y"); // we don't support "in the last year" as an offset
+    }
+
+    @Test(expected = CommandException.class)
+    public void testLongTimestampTooLarge() throws CommandException {
+        String timestamp = String.valueOf(Long.MAX_VALUE) + "0"; // 10x Long.MAX_VALUE
+        parser.parse(timestamp);
+    }
+
+    @Test
+    public void testSecondsOffset() throws CommandException {
+        long result = parser.parse("5s");
+        long delta = TimeUnit.SECONDS.toMillis(5);
+        assertThat(result, is(FAKE_CURRENT_TIMESTAMP - delta));
+    }
+
+    @Test(expected = CommandException.class)
+    public void testSecondsOffsetDoesNotAllowNegativeOffsets() throws CommandException {
+        parser.parse("-5s");
+    }
+
+    @Test(expected = CommandException.class)
+    public void testSecondsOffsetRequiresIntegerOffsets() throws CommandException {
+        parser.parse("5.5s");
+    }
+
+    @Test(expected = CommandException.class)
+    public void testSecondsOffsetIsCaseSensitive() throws CommandException {
+        parser.parse("5S");
+    }
+
+    @Test
+    public void testMinutesOffset() throws CommandException {
+        long result = parser.parse("5m");
+        long delta = TimeUnit.MINUTES.toMillis(5);
+        assertThat(result, is(FAKE_CURRENT_TIMESTAMP - delta));
+    }
+
+    @Test(expected = CommandException.class)
+    public void testMinutesOffsetDoesNotAllowNegativeOffsets() throws CommandException {
+        parser.parse("-5m");
+    }
+
+    @Test(expected = CommandException.class)
+    public void testMinutesOffsetRequiresIntegerOffsets() throws CommandException {
+        parser.parse("5.5m");
+    }
+
+    @Test(expected = CommandException.class)
+    public void testMinutesOffsetIsCaseSensitive() throws CommandException {
+        parser.parse("5M");
+    }
+
+    @Test
+    public void testHoursOffset() throws CommandException {
+        long result = parser.parse("5h");
+        long delta = TimeUnit.HOURS.toMillis(5);
+        assertThat(result, is(FAKE_CURRENT_TIMESTAMP - delta));
+    }
+
+    @Test(expected = CommandException.class)
+    public void testHoursOffsetDoesNotAllowNegativeOffsets() throws CommandException {
+        parser.parse("-5h");
+    }
+
+    @Test(expected = CommandException.class)
+    public void testHoursOffsetRequiresIntegerOffsets() throws CommandException {
+        parser.parse("5.5h");
+    }
+
+    @Test(expected = CommandException.class)
+    public void testHoursOffsetIsCaseSensitive() throws CommandException {
+        parser.parse("5H");
+    }
+
+    @Test
+    public void testDaysOffset() throws CommandException {
+        long result = parser.parse("5d");
+        long delta = TimeUnit.DAYS.toMillis(5);
+        assertThat(result, is(FAKE_CURRENT_TIMESTAMP - delta));
+    }
+
+    @Test(expected = CommandException.class)
+    public void testDaysOffsetDoesNotAllowNegativeOffsets() throws CommandException {
+        parser.parse("-5d");
+    }
+
+    @Test(expected = CommandException.class)
+    public void testDaysOffsetRequiresIntegerOffsets() throws CommandException {
+        parser.parse("5.5d");
+    }
+
+    @Test(expected = CommandException.class)
+    public void testDaysOffsetIsCaseSensitive() throws CommandException {
+        parser.parse("5D");
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-cli/src/test/java/com/redhat/thermostat/vm/jmx/client/cli/locale/LocaleResourcesTest.java	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code 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 this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.jmx.client.cli.locale;
+
+import com.redhat.thermostat.testutils.AbstractLocaleResourcesTest;
+
+public class LocaleResourcesTest extends AbstractLocaleResourcesTest<LocaleResources> {
+
+    @Override
+    protected Class<LocaleResources> getEnumClass() {
+        return LocaleResources.class;
+    }
+
+    @Override
+    protected String getResourceBundle() {
+        return LocaleResources.RESOURCE_BUNDLE;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-core/src/main/java/com/redhat/thermostat/vm/jmx/client/core/JmxToggleNotificationRequest.java	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code 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 this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.jmx.client.core;
+
+import com.redhat.thermostat.client.command.RequestQueue;
+import com.redhat.thermostat.common.command.Request;
+import com.redhat.thermostat.common.command.Request.RequestType;
+import com.redhat.thermostat.common.command.RequestResponseListener;
+import com.redhat.thermostat.common.command.Response;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.dao.AgentInfoDAO;
+import com.redhat.thermostat.vm.jmx.common.JmxCommand;
+
+import java.net.InetSocketAddress;
+
+public class JmxToggleNotificationRequest {
+
+    public static final String CMD_CHANNEL_ACTION_NAME = "jmx-toggle-notifications";
+
+    private RequestQueue queue;
+    private AgentInfoDAO agentDAO;
+    private Runnable successAction;
+    private Runnable failureAction;
+    private JmxToggleResponseListenerFactory factory;
+
+    public JmxToggleNotificationRequest(RequestQueue queue, AgentInfoDAO agentDAO,
+            Runnable successAction, Runnable failureAction) {
+        this(queue, agentDAO, successAction, failureAction, new JmxToggleResponseListenerFactory());
+    }
+
+    JmxToggleNotificationRequest(RequestQueue queue, AgentInfoDAO agentDAO, Runnable successAction,
+            Runnable failureAction, JmxToggleResponseListenerFactory factory) {
+        this.queue = queue;
+        this.agentDAO = agentDAO;
+        this.successAction = successAction;
+        this.failureAction = failureAction;
+        this.factory = factory;
+    }
+
+    public void sendEnableNotificationsRequestToAgent(VmRef vm, boolean enable) {
+        AgentId targetId = new AgentId(vm.getHostRef().getAgentId());
+
+        InetSocketAddress target = agentDAO.getAgentInformation(targetId).getRequestQueueAddress();
+        Request req = new Request(RequestType.RESPONSE_EXPECTED, target);
+
+        req.setReceiver(JmxCommand.RECEIVER);
+
+        req.setParameter(Request.ACTION, CMD_CHANNEL_ACTION_NAME);
+        req.setParameter(JmxCommand.class.getName(), enable ? JmxCommand.ENABLE_JMX_NOTIFICATIONS.name() : JmxCommand.DISABLE_JMX_NOTIFICATIONS.name());
+        req.setParameter(JmxCommand.VM_PID, String.valueOf(vm.getPid()));
+        req.setParameter(JmxCommand.VM_ID, vm.getVmId());
+
+        JmxToggleResponseListener listener = factory.createListener(successAction, failureAction);
+        req.addListener(listener);
+
+        queue.putRequest(req);
+    }
+
+    static class JmxToggleResponseListener implements RequestResponseListener {
+
+        private Runnable successAction;
+        private Runnable failureAction;
+
+        public JmxToggleResponseListener(Runnable successAction, Runnable failureAction) {
+            this.successAction = successAction;
+            this.failureAction = failureAction;
+        }
+
+        @Override
+        public void fireComplete(Request request, Response response) {
+            switch (response.getType()) {
+            case OK:
+                successAction.run();
+                break;
+            default:
+                failureAction.run();
+                break;
+            }
+        }
+
+    }
+
+    static class JmxToggleResponseListenerFactory {
+
+        public JmxToggleResponseListener createListener(Runnable successAction,
+                Runnable failureAction) {
+            return new JmxToggleResponseListener(successAction, failureAction);
+        }
+
+    }
+}
+
--- a/vm-jmx/client-core/src/main/java/com/redhat/thermostat/vm/jmx/client/core/internal/JmxNotificationsViewController.java	Tue Nov 15 18:18:31 2016 -0500
+++ b/vm-jmx/client-core/src/main/java/com/redhat/thermostat/vm/jmx/client/core/internal/JmxNotificationsViewController.java	Fri Oct 28 15:24:30 2016 -0400
@@ -65,6 +65,7 @@
 import com.redhat.thermostat.vm.jmx.common.JmxNotification;
 import com.redhat.thermostat.vm.jmx.common.JmxNotificationDAO;
 import com.redhat.thermostat.vm.jmx.common.JmxNotificationStatus;
+import com.redhat.thermostat.vm.jmx.client.core.JmxToggleNotificationRequest;
 
 public class JmxNotificationsViewController implements InformationServiceController<VmRef> {
 
--- a/vm-jmx/client-core/src/main/java/com/redhat/thermostat/vm/jmx/client/core/internal/JmxToggleNotificationRequest.java	Tue Nov 15 18:18:31 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,127 +0,0 @@
-/*
- * Copyright 2012-2016 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code 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 this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-package com.redhat.thermostat.vm.jmx.client.core.internal;
-
-import java.net.InetSocketAddress;
-
-import com.redhat.thermostat.client.command.RequestQueue;
-import com.redhat.thermostat.common.command.Request;
-import com.redhat.thermostat.common.command.Request.RequestType;
-import com.redhat.thermostat.common.command.RequestResponseListener;
-import com.redhat.thermostat.common.command.Response;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.dao.AgentInfoDAO;
-import com.redhat.thermostat.vm.jmx.common.JmxCommand;
-
-public class JmxToggleNotificationRequest {
-    
-    static final String CMD_CHANNEL_ACTION_NAME = "jmx-toggle-notifications";
-
-    private RequestQueue queue;
-    private AgentInfoDAO agentDAO;
-    private Runnable successAction;
-    private Runnable failureAction;
-    private JmxToggleResponseListenerFactory factory;
-
-    public JmxToggleNotificationRequest(RequestQueue queue, AgentInfoDAO agentDAO,
-            Runnable successAction, Runnable failureAction) {
-        this(queue, agentDAO, successAction, failureAction, new JmxToggleResponseListenerFactory());
-    }
-    
-    JmxToggleNotificationRequest(RequestQueue queue, AgentInfoDAO agentDAO, Runnable successAction, 
-            Runnable failureAction, JmxToggleResponseListenerFactory factory) {
-        this.queue = queue;
-        this.agentDAO = agentDAO;
-        this.successAction = successAction;
-        this.failureAction = failureAction;
-        this.factory = factory;
-    }
-
-    public void sendEnableNotificationsRequestToAgent(VmRef vm, boolean enable) {
-        AgentId targetId = new AgentId(vm.getHostRef().getAgentId());
-
-        InetSocketAddress target = agentDAO.getAgentInformation(targetId).getRequestQueueAddress();
-        Request req = new Request(RequestType.RESPONSE_EXPECTED, target);
-
-        req.setReceiver(JmxCommand.RECEIVER);
-
-        req.setParameter(Request.ACTION, CMD_CHANNEL_ACTION_NAME);
-        req.setParameter(JmxCommand.class.getName(), enable ? JmxCommand.ENABLE_JMX_NOTIFICATIONS.name() : JmxCommand.DISABLE_JMX_NOTIFICATIONS.name());
-        req.setParameter(JmxCommand.VM_PID, String.valueOf(vm.getPid()));
-        req.setParameter(JmxCommand.VM_ID, vm.getVmId());
-
-        JmxToggleResponseListener listener = factory.createListener(successAction, failureAction);
-        req.addListener(listener);
-
-        queue.putRequest(req);
-    }
-    
-    static class JmxToggleResponseListener implements RequestResponseListener {
-        
-        private Runnable successAction;
-        private Runnable failureAction;
-        
-        public JmxToggleResponseListener(Runnable successAction, Runnable failureAction) {
-            this.successAction = successAction;
-            this.failureAction = failureAction;
-        }
-
-        @Override
-        public void fireComplete(Request request, Response response) {
-            switch (response.getType()) {
-            case OK:
-                successAction.run();
-                break;
-            default:
-                failureAction.run();
-                break;
-            }
-        }
-        
-    }
-    
-    static class JmxToggleResponseListenerFactory {
-        
-        JmxToggleResponseListener createListener(Runnable successAction, 
-                Runnable failureAction) {
-            return new JmxToggleResponseListener(successAction, failureAction);
-        }
-        
-    }
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-jmx/client-core/src/test/java/com/redhat/thermostat/vm/jmx/client/core/JmxToggleNotificationRequestTest.java	Fri Oct 28 15:24:30 2016 -0400
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code 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 this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.jmx.client.core;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.net.InetSocketAddress;
+import java.util.Collection;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import com.redhat.thermostat.client.command.RequestQueue;
+import com.redhat.thermostat.common.command.Request;
+import com.redhat.thermostat.common.command.RequestResponseListener;
+import com.redhat.thermostat.common.command.Response;
+import com.redhat.thermostat.common.command.Response.ResponseType;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.HostRef;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.dao.AgentInfoDAO;
+import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.vm.jmx.client.core.JmxToggleNotificationRequest.JmxToggleResponseListener;
+import com.redhat.thermostat.vm.jmx.client.core.JmxToggleNotificationRequest.JmxToggleResponseListenerFactory;
+import com.redhat.thermostat.vm.jmx.common.JmxCommand;
+
+public class JmxToggleNotificationRequestTest {
+
+    private static final String HOST = "example.com";
+    private static final int PORT = 0;
+
+    private RequestQueue queue;
+    private ArgumentCaptor<Request> requestCaptor;
+    private HostRef host;
+    private VmRef vm;
+    private AgentInfoDAO agentDAO;
+    private AgentInformation agentInfo;
+    private JmxToggleResponseListenerFactory factory;
+    private JmxToggleResponseListener listener;
+    private Runnable successAction;
+    private Runnable failureAction;
+    private JmxToggleNotificationRequest toggleReq;
+
+    @Before
+    public void setUp() {
+        queue = mock(RequestQueue.class);
+        requestCaptor = ArgumentCaptor.forClass(Request.class);
+
+        final String AGENT_ID = "some-id";
+        AgentId agentId = new AgentId(AGENT_ID);
+        host = mock(HostRef.class);
+        when(host.getAgentId()).thenReturn(AGENT_ID);
+        vm = mock(VmRef.class);
+        when(vm.getHostRef()).thenReturn(host);
+
+        agentDAO = mock(AgentInfoDAO.class);
+
+        agentInfo = mock(AgentInformation.class);
+        when(agentInfo.getRequestQueueAddress()).thenReturn(new InetSocketAddress(HOST, PORT));
+        when(agentDAO.getAgentInformation(agentId)).thenReturn(agentInfo);
+        
+        factory = mock(JmxToggleResponseListenerFactory.class);
+        listener = mock(JmxToggleResponseListener.class);
+        successAction = mock(Runnable.class);
+        failureAction = mock(Runnable.class);
+        when(factory.createListener(successAction, failureAction)).thenReturn(listener);
+        
+        toggleReq = new JmxToggleNotificationRequest(queue, agentDAO, successAction, failureAction);
+    }
+
+    @Test
+    public void testEnableNotificationsSuccess() {
+        answerSuccess();
+        toggleReq.sendEnableNotificationsRequestToAgent(vm, true);
+
+        verify(queue).putRequest(requestCaptor.capture());
+
+        Request req = requestCaptor.getValue();
+
+        assertEquals(new InetSocketAddress(HOST, PORT), req.getTarget());
+        assertEquals(JmxToggleNotificationRequest.CMD_CHANNEL_ACTION_NAME, req.getParameter(Request.ACTION));
+        assertEquals(JmxCommand.RECEIVER, req.getReceiver());
+        assertEquals(String.valueOf(vm.getPid()), req.getParameter(JmxCommand.VM_PID));
+        assertEquals(vm.getVmId(), req.getParameter(JmxCommand.VM_ID));
+
+        assertEquals(JmxCommand.ENABLE_JMX_NOTIFICATIONS.name(), req.getParameter(JmxCommand.class.getName()));
+        
+        verify(successAction).run();
+        verify(failureAction, never()).run();
+    }
+    
+    @Test
+    public void testEnableNotificationsFailure() {
+        answerFailure();
+        toggleReq.sendEnableNotificationsRequestToAgent(vm, true);
+
+        verify(successAction, never()).run();
+        verify(failureAction).run();
+    }
+
+    @Test
+    public void testDisableNotificationsSuccess() {
+        answerSuccess();
+        toggleReq.sendEnableNotificationsRequestToAgent(vm, false);
+
+        verify(queue).putRequest(requestCaptor.capture());
+
+        Request req = requestCaptor.getValue();
+
+        assertEquals(new InetSocketAddress(HOST, PORT), req.getTarget());
+        assertEquals(JmxToggleNotificationRequest.CMD_CHANNEL_ACTION_NAME, req.getParameter(Request.ACTION));
+        assertEquals(JmxCommand.RECEIVER, req.getReceiver());
+        assertEquals(String.valueOf(vm.getPid()), req.getParameter(JmxCommand.VM_PID));
+        assertEquals(vm.getVmId(), req.getParameter(JmxCommand.VM_ID));
+
+        assertEquals(JmxCommand.DISABLE_JMX_NOTIFICATIONS.name(), req.getParameter(JmxCommand.class.getName()));
+        
+        verify(successAction).run();
+        verify(failureAction, never()).run();
+    }
+    
+    @Test
+    public void testDisableNotificationsFailure() {
+        answerFailure();
+        toggleReq.sendEnableNotificationsRequestToAgent(vm, false);
+
+        verify(successAction, never()).run();
+        verify(failureAction).run();
+    }
+    
+    private void answerSuccess() {
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                // Fire complete OK
+                Request req = (Request) invocation.getArguments()[0];
+                Collection<RequestResponseListener> listeners = req.getListeners();
+                assertEquals(1, listeners.size());
+                RequestResponseListener listener = listeners.iterator().next();
+                listener.fireComplete(req, new Response(ResponseType.OK));
+                return null;
+            }
+        }).when(queue).putRequest(any(Request.class));
+    }
+
+    private void answerFailure() {
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                // Fire complete ERROR
+                Request req = (Request) invocation.getArguments()[0];
+                Collection<RequestResponseListener> listeners = req.getListeners();
+                assertEquals(1, listeners.size());
+                RequestResponseListener listener = listeners.iterator().next();
+                listener.fireComplete(req, new Response(ResponseType.ERROR));
+                return null;
+            }
+        }).when(queue).putRequest(any(Request.class));
+    }
+}
+
--- a/vm-jmx/client-core/src/test/java/com/redhat/thermostat/vm/jmx/client/core/internal/JmxNotificationsViewControllerTest.java	Tue Nov 15 18:18:31 2016 -0500
+++ b/vm-jmx/client-core/src/test/java/com/redhat/thermostat/vm/jmx/client/core/internal/JmxNotificationsViewControllerTest.java	Fri Oct 28 15:24:30 2016 -0400
@@ -38,7 +38,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Matchers.isA;
 import static org.mockito.Mockito.atLeastOnce;
@@ -57,6 +56,7 @@
 import com.redhat.thermostat.storage.dao.VmInfoDAO;
 import com.redhat.thermostat.storage.model.AgentInformation;
 import com.redhat.thermostat.storage.model.VmInfo;
+import com.redhat.thermostat.vm.jmx.client.core.JmxToggleNotificationRequest;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
--- a/vm-jmx/client-core/src/test/java/com/redhat/thermostat/vm/jmx/client/core/internal/JmxToggleNotificationRequestTest.java	Tue Nov 15 18:18:31 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,204 +0,0 @@
-/*
- * Copyright 2012-2016 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code 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 this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-package com.redhat.thermostat.vm.jmx.client.core.internal;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.net.InetSocketAddress;
-import java.util.Collection;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import com.redhat.thermostat.client.command.RequestQueue;
-import com.redhat.thermostat.common.command.Request;
-import com.redhat.thermostat.common.command.RequestResponseListener;
-import com.redhat.thermostat.common.command.Response;
-import com.redhat.thermostat.common.command.Response.ResponseType;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.HostRef;
-import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.dao.AgentInfoDAO;
-import com.redhat.thermostat.storage.model.AgentInformation;
-import com.redhat.thermostat.vm.jmx.client.core.internal.JmxToggleNotificationRequest.JmxToggleResponseListener;
-import com.redhat.thermostat.vm.jmx.client.core.internal.JmxToggleNotificationRequest.JmxToggleResponseListenerFactory;
-import com.redhat.thermostat.vm.jmx.common.JmxCommand;
-
-public class JmxToggleNotificationRequestTest {
-
-    private static final String HOST = "example.com";
-    private static final int PORT = 0;
-
-    private RequestQueue queue;
-    private ArgumentCaptor<Request> requestCaptor;
-    private HostRef host;
-    private VmRef vm;
-    private AgentInfoDAO agentDAO;
-    private AgentInformation agentInfo;
-    private JmxToggleResponseListenerFactory factory;
-    private JmxToggleResponseListener listener;
-    private Runnable successAction;
-    private Runnable failureAction;
-    private JmxToggleNotificationRequest toggleReq;
-
-    @Before
-    public void setUp() {
-        queue = mock(RequestQueue.class);
-        requestCaptor = ArgumentCaptor.forClass(Request.class);
-
-        final String AGENT_ID = "some-id";
-        AgentId agentId = new AgentId(AGENT_ID);
-        host = mock(HostRef.class);
-        when(host.getAgentId()).thenReturn(AGENT_ID);
-        vm = mock(VmRef.class);
-        when(vm.getHostRef()).thenReturn(host);
-
-        agentDAO = mock(AgentInfoDAO.class);
-
-        agentInfo = mock(AgentInformation.class);
-        when(agentInfo.getRequestQueueAddress()).thenReturn(new InetSocketAddress(HOST, PORT));
-        when(agentDAO.getAgentInformation(agentId)).thenReturn(agentInfo);
-        
-        factory = mock(JmxToggleResponseListenerFactory.class);
-        listener = mock(JmxToggleResponseListener.class);
-        successAction = mock(Runnable.class);
-        failureAction = mock(Runnable.class);
-        when(factory.createListener(successAction, failureAction)).thenReturn(listener);
-        
-        toggleReq = new JmxToggleNotificationRequest(queue, agentDAO, successAction, failureAction);
-    }
-
-    @Test
-    public void testEnableNotificationsSuccess() {
-        answerSuccess();
-        toggleReq.sendEnableNotificationsRequestToAgent(vm, true);
-
-        verify(queue).putRequest(requestCaptor.capture());
-
-        Request req = requestCaptor.getValue();
-
-        assertEquals(new InetSocketAddress(HOST, PORT), req.getTarget());
-        assertEquals(JmxToggleNotificationRequest.CMD_CHANNEL_ACTION_NAME, req.getParameter(Request.ACTION));
-        assertEquals(JmxCommand.RECEIVER, req.getReceiver());
-        assertEquals(String.valueOf(vm.getPid()), req.getParameter(JmxCommand.VM_PID));
-        assertEquals(vm.getVmId(), req.getParameter(JmxCommand.VM_ID));
-
-        assertEquals(JmxCommand.ENABLE_JMX_NOTIFICATIONS.name(), req.getParameter(JmxCommand.class.getName()));
-        
-        verify(successAction).run();
-        verify(failureAction, never()).run();
-    }
-    
-    @Test
-    public void testEnableNotificationsFailure() {
-        answerFailure();
-        toggleReq.sendEnableNotificationsRequestToAgent(vm, true);
-
-        verify(successAction, never()).run();
-        verify(failureAction).run();
-    }
-
-    @Test
-    public void testDisableNotificationsSuccess() {
-        answerSuccess();
-        toggleReq.sendEnableNotificationsRequestToAgent(vm, false);
-
-        verify(queue).putRequest(requestCaptor.capture());
-
-        Request req = requestCaptor.getValue();
-
-        assertEquals(new InetSocketAddress(HOST, PORT), req.getTarget());
-        assertEquals(JmxToggleNotificationRequest.CMD_CHANNEL_ACTION_NAME, req.getParameter(Request.ACTION));
-        assertEquals(JmxCommand.RECEIVER, req.getReceiver());
-        assertEquals(String.valueOf(vm.getPid()), req.getParameter(JmxCommand.VM_PID));
-        assertEquals(vm.getVmId(), req.getParameter(JmxCommand.VM_ID));
-
-        assertEquals(JmxCommand.DISABLE_JMX_NOTIFICATIONS.name(), req.getParameter(JmxCommand.class.getName()));
-        
-        verify(successAction).run();
-        verify(failureAction, never()).run();
-    }
-    
-    @Test
-    public void testDisableNotificationsFailure() {
-        answerFailure();
-        toggleReq.sendEnableNotificationsRequestToAgent(vm, false);
-
-        verify(successAction, never()).run();
-        verify(failureAction).run();
-    }
-    
-    private void answerSuccess() {
-        doAnswer(new Answer<Void>() {
-            @Override
-            public Void answer(InvocationOnMock invocation) throws Throwable {
-                // Fire complete OK
-                Request req = (Request) invocation.getArguments()[0];
-                Collection<RequestResponseListener> listeners = req.getListeners();
-                assertEquals(1, listeners.size());
-                RequestResponseListener listener = listeners.iterator().next();
-                listener.fireComplete(req, new Response(ResponseType.OK));
-                return null;
-            }
-        }).when(queue).putRequest(any(Request.class));
-    }
-
-    private void answerFailure() {
-        doAnswer(new Answer<Void>() {
-            @Override
-            public Void answer(InvocationOnMock invocation) throws Throwable {
-                // Fire complete ERROR
-                Request req = (Request) invocation.getArguments()[0];
-                Collection<RequestResponseListener> listeners = req.getListeners();
-                assertEquals(1, listeners.size());
-                RequestResponseListener listener = listeners.iterator().next();
-                listener.fireComplete(req, new Response(ResponseType.ERROR));
-                return null;
-            }
-        }).when(queue).putRequest(any(Request.class));
-    }
-}
-
--- a/vm-jmx/distribution/pom.xml	Tue Nov 15 18:18:31 2016 -0500
+++ b/vm-jmx/distribution/pom.xml	Fri Oct 28 15:24:30 2016 -0400
@@ -92,6 +92,11 @@
     </dependency>
     <dependency>
       <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-vm-jmx-client-cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
       <artifactId>thermostat-vm-jmx-client-core</artifactId>
       <version>${project.version}</version>
     </dependency>
--- a/vm-jmx/distribution/thermostat-plugin.xml	Tue Nov 15 18:18:31 2016 -0500
+++ b/vm-jmx/distribution/thermostat-plugin.xml	Fri Oct 28 15:24:30 2016 -0400
@@ -39,6 +39,72 @@
 <plugin xmlns="http://icedtea.classpath.org/thermostat/plugins/v1.0"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 thermostat-plugin.xsd">
+  <commands>
+    <command>
+      <name>notifications</name>
+      <summary>manage JMX notifications on the given VM</summary>
+      <description>Manage JMX notification monitoring on the given JVM.</description>
+      <subcommands>
+        <subcommand>
+          <name>status</name>
+          <description>Check JMX notification monitoring status</description>
+        </subcommand>
+        <subcommand>
+          <name>enable</name>
+          <description>Enable JMX notification monitoring</description>
+          <options>
+            <option>
+              <long>follow</long>
+              <short>f</short>
+              <required>false</required>
+              <description>Start following JMX events immediately</description>
+            </option>
+          </options>
+        </subcommand>
+        <subcommand>
+          <name>disable</name>
+          <description>Disable JMX notification monitoring</description>
+        </subcommand>
+        <subcommand>
+          <name>follow</name>
+          <description>Follow JMX notification events</description>
+        </subcommand>
+        <subcommand>
+          <name>show</name>
+          <description>Show all JMX events for the JVM</description>
+          <options>
+            <option>
+              <long>since</long>
+              <argument>time</argument>
+              <description>only show events occurring since the given instant. Dates must be expressed 1) in milliseconds
+                since the Unix epoch, or 2) as an integer offset from present in seconds, minutes, hours, or days, of
+                the form "30s" for the last 30 seconds, or "1d" for the last day, for example.</description>
+            </option>
+          </options>
+        </subcommand>
+      </subcommands>
+      <options>
+        <option>
+          <long>vmId</long>
+          <short>v</short>
+          <argument>id</argument>
+          <required>true</required>
+        </option>
+      </options>
+      <environments>
+        <environment>cli</environment>
+        <environment>shell</environment>
+      </environments>
+      <bundles>
+        <bundle><symbolic-name>com.redhat.thermostat.vm.jmx.client.cli</symbolic-name><version>${project.version}</version></bundle>
+        <bundle><symbolic-name>com.redhat.thermostat.vm.jmx.common</symbolic-name><version>${project.version}</version></bundle>
+        <bundle><symbolic-name>com.redhat.thermostat.vm.jmx.client.core</symbolic-name><version>${project.version}</version></bundle>
+        <bundle><symbolic-name>com.redhat.thermostat.web.common</symbolic-name><version>${project.version}</version></bundle>
+        <bundle><symbolic-name>com.redhat.thermostat.web.client</symbolic-name><version>${project.version}</version></bundle>
+      </bundles>
+    </command>
+  </commands>
+
   <extensions>
     <extension>
       <name>gui</name>
--- a/vm-jmx/pom.xml	Tue Nov 15 18:18:31 2016 -0500
+++ b/vm-jmx/pom.xml	Fri Oct 28 15:24:30 2016 -0400
@@ -54,6 +54,7 @@
     <module>agent</module>
     <module>common</module>
     <module>client-core</module>
+    <module>client-cli</module>
     <module>client-swing</module>
     <module>distribution</module>
   </modules>