You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2018/07/13 11:28:49 UTC

[cloudstack] branch master updated: diagnostics: new diagnostics admin API for system VMs (#2721)

This is an automated email from the ASF dual-hosted git repository.

rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/master by this push:
     new 40af32b  diagnostics: new diagnostics admin API for system VMs (#2721)
40af32b is described below

commit 40af32b1b98e5ab6229531a151381b1f4f1f2910
Author: Dingane Hlaluku <di...@shapeblue.com>
AuthorDate: Fri Jul 13 13:28:45 2018 +0200

    diagnostics: new diagnostics admin API for system VMs (#2721)
    
    This is a new feature for CS that allows Admin users improved
    troubleshooting of network issues in CloudStack hosted networks.
    
    Description: For troubleshooting purposes, CloudStack administrators may wish to execute network utility commands remotely on system VMs, or request system VMs to ping/traceroute/arping to specific addresses over specific interfaces. An API command to provide such functionalities is being developed without altering any existing APIs. The targeted system VMs for this feature are the Virtual Router (VR), Secondary Storage VM (SSVM) and the Console Proxy VM (CPVM).
    
    FS:
    https://cwiki.apache.org/confluence/display/CLOUDSTACK/CloudStack+Remote+Diagnostics+API
    ML discussion:
    https://markmail.org/message/xt7owmb2c6iw7tva
---
 .travis.yml                                        |   3 +-
 .../org/apache/cloudstack/api/ApiConstants.java    |   5 +
 .../admin/diagnostics/RunDiagnosticsCmd.java       | 136 ++++++
 .../api/response/RunDiagnosticsResponse.java       |  67 +++
 .../cloudstack/diagnostics/DiagnosticsService.java |  29 ++
 .../cloudstack/diagnostics/DiagnosticsType.java    |  42 ++
 .../agent/resource/virtualnetwork/VRScripts.java   |   1 +
 .../virtualnetwork/VirtualRoutingResource.java     |  18 +-
 .../cloudstack/diagnostics/DiagnosticsAnswer.java  |  54 +++
 .../cloudstack/diagnostics/DiagnosticsCommand.java |  44 ++
 .../com/cloud/agent/manager/MockAgentManager.java  |   3 +
 .../cloud/agent/manager/MockAgentManagerImpl.java  |  56 ++-
 .../cloud/agent/manager/SimulatorManagerImpl.java  |   5 +-
 .../diagnostics/DiagnosticsServiceImpl.java        | 131 +++++
 .../core/spring-server-core-managers-context.xml   |   2 +
 .../diagnostics/DiagnosticsServiceImplTest.java    | 196 ++++++++
 systemvm/debian/opt/cloud/bin/diagnostics.py       |  71 +++
 test/integration/smoke/test_diagnostics.py         | 539 +++++++++++++++++++++
 tools/apidoc/gen_toc.py                            |   3 +-
 19 files changed, 1377 insertions(+), 28 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 2d32324..130e907 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -38,7 +38,7 @@ env:
   matrix:
     # Keep the TESTS sorted by name and grouped by type
     - TESTS="smoke/test_certauthority_root"
-    
+
     - TESTS="smoke/test_accounts
              smoke/test_affinity_groups
              smoke/test_affinity_groups_projects
@@ -47,6 +47,7 @@ env:
              smoke/test_deploy_vm_root_resize
              smoke/test_deploy_vm_with_userdata
              smoke/test_deploy_vms_with_varied_deploymentplanners
+             smoke/test_diagnostics
              smoke/test_disk_offerings
              smoke/test_dynamicroles
              smoke/test_global_settings
diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
index 504b214..1a57313 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -719,6 +719,11 @@ public class ApiConstants {
     public static final String LAST_ANNOTATED = "lastannotated";
     public static final String LDAP_DOMAIN = "ldapdomain";
 
+    public static final String STDOUT = "stdout";
+    public static final String STDERR = "stderr";
+    public static final String EXITCODE = "exitcode";
+    public static final String TARGET_ID = "targetid";
+
     public enum HostDetails {
         all, capacity, events, stats, min;
     }
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/RunDiagnosticsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/RunDiagnosticsCmd.java
new file mode 100644
index 0000000..bb1ddf5
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/RunDiagnosticsCmd.java
@@ -0,0 +1,136 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.api.command.admin.diagnostics;
+
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.user.Account;
+import com.cloud.vm.VirtualMachine;
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiArgValidator;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.RunDiagnosticsResponse;
+import org.apache.cloudstack.api.response.SystemVmResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.diagnostics.DiagnosticsService;
+import org.apache.cloudstack.diagnostics.DiagnosticsType;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.log4j.Logger;
+
+import javax.inject.Inject;
+import java.util.Collections;
+import java.util.Map;
+
+@APICommand(name = RunDiagnosticsCmd.APINAME, responseObject = RunDiagnosticsResponse.class, entityType = {VirtualMachine.class},
+        responseHasSensitiveInfo = false,
+        requestHasSensitiveInfo = false,
+        description = "Execute network-utility command (ping/arping/tracert) on system VMs remotely",
+        authorized = {RoleType.Admin},
+        since = "4.12.0.0")
+public class RunDiagnosticsCmd extends BaseCmd {
+    private static final Logger LOGGER = Logger.getLogger(RunDiagnosticsCmd.class);
+    public static final String APINAME = "runDiagnostics";
+
+    @Inject
+    private DiagnosticsService diagnosticsService;
+
+    /////////////////////////////////////////////////////
+    //////////////// API parameters /////////////////////
+    /////////////////////////////////////////////////////
+    @Parameter(name = ApiConstants.TARGET_ID, type = CommandType.UUID, required = true, entityType = SystemVmResponse.class,
+            validations = {ApiArgValidator.PositiveNumber},
+            description = "The ID of the system VM instance to diagnose")
+    private Long id;
+
+    @Parameter(name = ApiConstants.IP_ADDRESS, type = CommandType.STRING, required = true,
+            validations = {ApiArgValidator.NotNullOrEmpty},
+            description = "The IP/Domain address to test connection to")
+    private String address;
+
+    @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true,
+            validations = {ApiArgValidator.NotNullOrEmpty},
+            description = "The system VM diagnostics type  valid options are: ping, traceroute, arping")
+    private String type;
+
+    @Parameter(name = ApiConstants.PARAMS, type = CommandType.STRING,
+            authorized = {RoleType.Admin},
+            description = "Additional command line options that apply for each command")
+    private String optionalArguments;
+
+    /////////////////////////////////////////////////////
+    /////////////////// Accessors ///////////////////////
+    /////////////////////////////////////////////////////
+    public Long getId() {
+        return id;
+    }
+
+    public String getAddress() {
+        return address;
+    }
+
+    public DiagnosticsType getType() {
+        DiagnosticsType diagnosticsType = DiagnosticsType.getCommand(type);
+        if (diagnosticsType == null) {
+            throw new IllegalArgumentException(type + " Is not a valid diagnostics command type. ");
+        }
+        return diagnosticsType;
+    }
+
+    public String getOptionalArguments() {
+        return optionalArguments;
+    }
+
+    /////////////////////////////////////////////////////
+    /////////////////// Implementation //////////////////
+    /////////////////////////////////////////////////////
+    @Override
+    public String getCommandName() {
+        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+    }
+
+    @Override
+    public long getEntityOwnerId() {
+        Account account = CallContext.current().getCallingAccount();
+        if (account != null) {
+            return account.getId();
+        }
+        return Account.ACCOUNT_ID_SYSTEM;
+    }
+
+    @Override
+    public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException {
+        RunDiagnosticsResponse response = new RunDiagnosticsResponse();
+        try {
+            final Map<String, String> answerMap = diagnosticsService.runDiagnosticsCommand(this);
+            if (CollectionUtils.isNotEmpty(Collections.singleton(answerMap))) {
+                response.setStdout(answerMap.get(ApiConstants.STDOUT));
+                response.setStderr(answerMap.get(ApiConstants.STDERR));
+                response.setExitCode(answerMap.get(ApiConstants.EXITCODE));
+                response.setObjectName("diagnostics");
+                response.setResponseName(getCommandName());
+                this.setResponseObject(response);
+            }
+        } catch (final ServerApiException e) {
+            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
+        }
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/RunDiagnosticsResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/RunDiagnosticsResponse.java
new file mode 100644
index 0000000..4c8a923
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/RunDiagnosticsResponse.java
@@ -0,0 +1,67 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+package org.apache.cloudstack.api.response;
+
+import com.cloud.serializer.Param;
+import com.cloud.vm.VirtualMachine;
+import com.google.gson.annotations.SerializedName;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+
+@EntityReference(value = VirtualMachine.class)
+public class RunDiagnosticsResponse extends BaseResponse {
+    @SerializedName(ApiConstants.STDOUT)
+    @Param(description = "the standard output from the command execution")
+    private String stdout;
+
+    @SerializedName(ApiConstants.STDERR)
+    @Param(description = "the standard error output from the command execution")
+    private String stderr;
+
+    @SerializedName(ApiConstants.EXITCODE)
+    @Param(description = "the command execution return code")
+    private String exitCode;
+
+    public String getStdout() {
+        return stdout;
+    }
+
+    public void setStdout(String stdout) {
+        this.stdout = stdout;
+    }
+
+    public String getStderr() {
+        return stderr;
+    }
+
+    public void setStderr(String stderr) {
+        this.stderr = stderr;
+    }
+
+    public String getExitCode() {
+        return exitCode;
+    }
+
+    public void setExitCode(String exitCode) {
+        this.exitCode = exitCode;
+    }
+
+}
diff --git a/api/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsService.java b/api/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsService.java
new file mode 100644
index 0000000..a9177af
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsService.java
@@ -0,0 +1,29 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+package org.apache.cloudstack.diagnostics;
+
+import org.apache.cloudstack.api.command.admin.diagnostics.RunDiagnosticsCmd;
+
+import java.util.Map;
+
+public interface DiagnosticsService {
+
+    Map<String, String> runDiagnosticsCommand(RunDiagnosticsCmd cmd);
+
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsType.java b/api/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsType.java
new file mode 100644
index 0000000..0e3a1da
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsType.java
@@ -0,0 +1,42 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+package org.apache.cloudstack.diagnostics;
+
+public enum DiagnosticsType {
+    PING("ping"), TRACEROUTE("traceroute"), ARPING("arping");
+
+    private String value;
+
+    DiagnosticsType(String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public static DiagnosticsType getCommand(String cmd) {
+        for (DiagnosticsType type : DiagnosticsType.values()) {
+            if (type.value.equalsIgnoreCase(cmd)) {
+                return type;
+            }
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java
index 838f087..2c75a78 100644
--- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java
+++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java
@@ -69,4 +69,5 @@ public class VRScripts {
 
     public static final String VR_CFG = "vr_cfg.sh";
 
+    public static final String DIAGNOSTICS = "diagnostics.py";
 }
\ No newline at end of file
diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java
index 0ffe8cc..112d920 100644
--- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java
+++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java
@@ -22,6 +22,9 @@ package com.cloud.agent.resource.virtualnetwork;
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.nio.channels.SocketChannel;
+
+import org.apache.cloudstack.diagnostics.DiagnosticsAnswer;
+import org.apache.cloudstack.diagnostics.DiagnosticsCommand;
 import org.joda.time.Duration;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -189,9 +192,11 @@ public class VirtualRoutingResource {
         } else if (cmd instanceof GetDomRVersionCmd) {
             return execute((GetDomRVersionCmd)cmd);
         } else if (cmd instanceof CheckS2SVpnConnectionsCommand) {
-            return execute((CheckS2SVpnConnectionsCommand) cmd);
+            return execute((CheckS2SVpnConnectionsCommand)cmd);
         } else if (cmd instanceof GetRouterAlertsCommand) {
             return execute((GetRouterAlertsCommand)cmd);
+        } else if (cmd instanceof DiagnosticsCommand) {
+            return execute((DiagnosticsCommand)cmd);
         } else {
             s_logger.error("Unknown query command in VirtualRoutingResource!");
             return Answer.createUnsupportedCommandAnswer(cmd);
@@ -292,6 +297,15 @@ public class VirtualRoutingResource {
         return new CheckRouterAnswer(cmd, result.getDetails(), true);
     }
 
+    private Answer execute(DiagnosticsCommand cmd) {
+        _eachTimeout = Duration.standardSeconds(NumbersUtil.parseInt("60", 60));
+        final ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), VRScripts.DIAGNOSTICS, cmd.getSrciptArguments(), _eachTimeout);
+        if (!result.isSuccess()) {
+            return new DiagnosticsAnswer(cmd, false, result.getDetails());
+        }
+        return new DiagnosticsAnswer(cmd, result.isSuccess(), result.getDetails());
+    }
+
     private Answer execute(GetDomRVersionCmd cmd) {
         final ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), VRScripts.VERSION, null);
         if (!result.isSuccess()) {
@@ -454,6 +468,6 @@ public class VirtualRoutingResource {
                 _vrAggregateCommandsSet.remove(routerName);
             }
         }
-        return new Answer(cmd, false, "Fail to recongize aggregation action " + action.toString());
+        return new Answer(cmd, false, "Fail to recognize aggregation action " + action.toString());
     }
 }
diff --git a/core/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsAnswer.java b/core/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsAnswer.java
new file mode 100644
index 0000000..006f043
--- /dev/null
+++ b/core/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsAnswer.java
@@ -0,0 +1,54 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.diagnostics;
+
+import com.cloud.agent.api.Answer;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.google.common.base.Strings;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.log4j.Logger;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class DiagnosticsAnswer extends Answer {
+    public static final Logger LOGGER = Logger.getLogger(DiagnosticsAnswer.class);
+
+    public DiagnosticsAnswer(DiagnosticsCommand cmd, boolean result, String details) {
+        super(cmd, result, details);
+    }
+
+    public Map<String, String> getExecutionDetails() {
+        final Map<String, String> executionDetailsMap = new HashMap<>();
+        if (result == true && !Strings.isNullOrEmpty(details)) {
+            final String[] parseDetails = details.split("&&");
+            if (parseDetails.length >= 3) {
+                executionDetailsMap.put(ApiConstants.STDOUT, parseDetails[0].trim());
+                executionDetailsMap.put(ApiConstants.STDERR, parseDetails[1].trim());
+                executionDetailsMap.put(ApiConstants.EXITCODE, String.valueOf(parseDetails[2]).trim());
+            } else {
+                throw new CloudRuntimeException("Unsupported diagnostics command type supplied");
+            }
+        } else {
+            executionDetailsMap.put(ApiConstants.STDOUT, "");
+            executionDetailsMap.put(ApiConstants.STDERR, details);
+            executionDetailsMap.put(ApiConstants.EXITCODE, "-1");
+        }
+        return executionDetailsMap;
+    }
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsCommand.java b/core/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsCommand.java
new file mode 100644
index 0000000..14d9da9
--- /dev/null
+++ b/core/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsCommand.java
@@ -0,0 +1,44 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.diagnostics;
+
+import com.cloud.agent.api.routing.NetworkElementCommand;
+
+public class DiagnosticsCommand extends NetworkElementCommand {
+
+    private final String scriptArguments;
+    private final boolean executeInSequence;
+
+    public DiagnosticsCommand(String scriptArguments, boolean executeInSequence) {
+        this.scriptArguments = scriptArguments;
+        this.executeInSequence = executeInSequence;
+    }
+
+    public String getSrciptArguments() {
+        return scriptArguments;
+    }
+
+    @Override
+    public boolean isQuery() {
+        return true;
+    }
+
+    @Override
+    public boolean executeInSequence() {
+        return this.executeInSequence;
+    }
+}
\ No newline at end of file
diff --git a/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManager.java b/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManager.java
index 3a31550..6a9e707 100644
--- a/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManager.java
+++ b/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManager.java
@@ -33,6 +33,7 @@ import com.cloud.agent.api.PingTestCommand;
 import com.cloud.resource.AgentResourceBase;
 import com.cloud.simulator.MockHost;
 import com.cloud.utils.component.Manager;
+import org.apache.cloudstack.diagnostics.DiagnosticsCommand;
 
 public interface MockAgentManager extends Manager {
     public static final long DEFAULT_HOST_MEM_SIZE = 8 * 1024 * 1024 * 1024L; // 8G, unit of Mbytes
@@ -64,4 +65,6 @@ public interface MockAgentManager extends Manager {
     Answer maintain(MaintainCommand cmd);
 
     Answer checkNetworkCommand(CheckNetworkCommand cmd);
+
+    Answer runDiagnostics(DiagnosticsCommand cmd);
 }
diff --git a/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManagerImpl.java b/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManagerImpl.java
index 9d1e407..7af2827 100644
--- a/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManagerImpl.java
+++ b/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManagerImpl.java
@@ -16,29 +16,6 @@
 // under the License.
 package com.cloud.agent.manager;
 
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.PatternSyntaxException;
-
-import javax.inject.Inject;
-import javax.naming.ConfigurationException;
-
-import org.apache.cloudstack.ca.SetupCertificateAnswer;
-import org.apache.cloudstack.ca.SetupCertificateCommand;
-import org.apache.cloudstack.ca.SetupKeyStoreCommand;
-import org.apache.cloudstack.ca.SetupKeystoreAnswer;
-import org.apache.cloudstack.context.CallContext;
-import org.apache.log4j.Logger;
-import org.springframework.stereotype.Component;
-
 import com.cloud.agent.AgentManager;
 import com.cloud.agent.api.Answer;
 import com.cloud.agent.api.CheckHealthCommand;
@@ -49,6 +26,7 @@ import com.cloud.agent.api.GetHostStatsCommand;
 import com.cloud.agent.api.HostStatsEntry;
 import com.cloud.agent.api.MaintainAnswer;
 import com.cloud.agent.api.PingTestCommand;
+import com.cloud.agent.api.routing.NetworkElementCommand;
 import com.cloud.api.commands.SimulatorAddSecondaryAgent;
 import com.cloud.dc.dao.HostPodDao;
 import com.cloud.exception.DiscoveryException;
@@ -73,6 +51,29 @@ import com.cloud.utils.db.DB;
 import com.cloud.utils.db.TransactionLegacy;
 import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.utils.net.NetUtils;
+import org.apache.cloudstack.ca.SetupCertificateAnswer;
+import org.apache.cloudstack.ca.SetupCertificateCommand;
+import org.apache.cloudstack.ca.SetupKeyStoreCommand;
+import org.apache.cloudstack.ca.SetupKeystoreAnswer;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.diagnostics.DiagnosticsAnswer;
+import org.apache.cloudstack.diagnostics.DiagnosticsCommand;
+import org.apache.log4j.Logger;
+import org.springframework.stereotype.Component;
+
+import javax.inject.Inject;
+import javax.naming.ConfigurationException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.PatternSyntaxException;
 
 @Component
 public class MockAgentManagerImpl extends ManagerBase implements MockAgentManager {
@@ -481,6 +482,7 @@ public class MockAgentManagerImpl extends ManagerBase implements MockAgentManage
         return new SetupCertificateAnswer(true);
     }
 
+
     @Override
     public boolean start() {
         for (Discoverer discoverer : discoverers) {
@@ -520,6 +522,14 @@ public class MockAgentManagerImpl extends ManagerBase implements MockAgentManage
         return new CheckNetworkAnswer(cmd, true, "Network Setup check by names is done");
     }
 
+    @Override
+    public Answer runDiagnostics(final DiagnosticsCommand cmd) {
+        final String vmInstance = cmd.getAccessDetail(NetworkElementCommand.ROUTER_NAME);
+        final String[] args = cmd.getSrciptArguments().split(" ");
+        final String mockAnswer = String.format("%s %s executed in %s &&  && 0", args[0].toUpperCase(), args[1], vmInstance);
+        return new DiagnosticsAnswer(cmd, true, mockAnswer);
+    }
+
     public List<Discoverer> getDiscoverers() {
         return discoverers;
     }
diff --git a/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/SimulatorManagerImpl.java b/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/SimulatorManagerImpl.java
index 7297773..29ad3cc 100644
--- a/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/SimulatorManagerImpl.java
+++ b/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/SimulatorManagerImpl.java
@@ -26,6 +26,7 @@ import java.util.Map;
 import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
+import org.apache.cloudstack.diagnostics.DiagnosticsCommand;
 import org.apache.log4j.Logger;
 import org.springframework.stereotype.Component;
 
@@ -293,7 +294,9 @@ public class SimulatorManagerImpl extends ManagerBase implements SimulatorManage
                 } else if (cmd instanceof PingTestCommand) {
                     answer = _mockAgentMgr.pingTest((PingTestCommand)cmd);
                 } else if (cmd instanceof SetupKeyStoreCommand) {
-                    answer = _mockAgentMgr.setupKeyStore((SetupKeyStoreCommand)cmd);
+                    answer = _mockAgentMgr.setupKeyStore((SetupKeyStoreCommand) cmd);
+                }else if (cmd instanceof DiagnosticsCommand) {
+                    answer = _mockAgentMgr.runDiagnostics((DiagnosticsCommand)cmd);
                 } else if (cmd instanceof SetupCertificateCommand) {
                     answer = _mockAgentMgr.setupCertificate((SetupCertificateCommand)cmd);
                 } else if (cmd instanceof PrepareForMigrationCommand) {
diff --git a/server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImpl.java b/server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImpl.java
new file mode 100644
index 0000000..a06b5bb
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImpl.java
@@ -0,0 +1,131 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.diagnostics;
+
+import com.cloud.agent.AgentManager;
+import com.cloud.agent.api.Answer;
+import com.cloud.agent.api.routing.NetworkElementCommand;
+import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.hypervisor.Hypervisor;
+import com.cloud.utils.component.ManagerBase;
+import com.cloud.utils.component.PluggableService;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.vm.VMInstanceVO;
+import com.cloud.vm.VirtualMachine;
+import com.cloud.vm.VirtualMachineManager;
+import com.cloud.vm.dao.VMInstanceDao;
+import com.google.common.base.Strings;
+import org.apache.cloudstack.api.command.admin.diagnostics.RunDiagnosticsCmd;
+import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
+import org.apache.log4j.Logger;
+
+import javax.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+public class DiagnosticsServiceImpl extends ManagerBase implements PluggableService, DiagnosticsService {
+    private static final Logger LOGGER = Logger.getLogger(DiagnosticsServiceImpl.class);
+
+    @Inject
+    private AgentManager agentManager;
+    @Inject
+    private VMInstanceDao instanceDao;
+    @Inject
+    private VirtualMachineManager vmManager;
+    @Inject
+    private NetworkOrchestrationService networkManager;
+
+    @Override
+    public Map<String, String> runDiagnosticsCommand(final RunDiagnosticsCmd cmd) {
+        final Long vmId = cmd.getId();
+        final String cmdType = cmd.getType().getValue();
+        final String ipAddress = cmd.getAddress();
+        final String optionalArguments = cmd.getOptionalArguments();
+        final VMInstanceVO vmInstance = instanceDao.findByIdTypes(vmId, VirtualMachine.Type.ConsoleProxy, VirtualMachine.Type.DomainRouter, VirtualMachine.Type.SecondaryStorageVm);
+
+        if (vmInstance == null) {
+            throw new InvalidParameterValueException("Unable to find a system vm with id " + vmId);
+        }
+        final Long hostId = vmInstance.getHostId();
+
+        if (hostId == null) {
+            throw new CloudRuntimeException("Unable to find host for virtual machine instance: " + vmInstance.getInstanceName());
+        }
+
+        final String shellCmd = prepareShellCmd(cmdType, ipAddress, optionalArguments);
+
+        if (Strings.isNullOrEmpty(shellCmd)) {
+            throw new IllegalArgumentException("Optional parameters contain unwanted characters: " + optionalArguments);
+        }
+
+        final Hypervisor.HypervisorType hypervisorType = vmInstance.getHypervisorType();
+
+        final DiagnosticsCommand command = new DiagnosticsCommand(shellCmd, vmManager.getExecuteInSequence(hypervisorType));
+        final Map<String, String> accessDetails = networkManager.getSystemVMAccessDetails(vmInstance);
+
+        if (Strings.isNullOrEmpty(accessDetails.get(NetworkElementCommand.ROUTER_IP))) {
+            throw new CloudRuntimeException("Unable to set system vm ControlIP for system vm with ID: " + vmId);
+        }
+
+        command.setAccessDetail(accessDetails);
+
+        Map<String, String> detailsMap;
+
+        final Answer answer = agentManager.easySend(hostId, command);
+
+        if (answer != null && (answer instanceof DiagnosticsAnswer)) {
+            detailsMap = ((DiagnosticsAnswer) answer).getExecutionDetails();
+            return detailsMap;
+        } else {
+            throw new CloudRuntimeException("Failed to execute diagnostics command on remote host: " + answer.getDetails());
+        }
+    }
+
+    protected boolean hasValidChars(String optionalArgs) {
+        if (Strings.isNullOrEmpty(optionalArgs)) {
+            return true;
+        } else {
+            final String regex = "^[\\w\\-\\s.]+$";
+            final Pattern pattern = Pattern.compile(regex);
+            return pattern.matcher(optionalArgs).find();
+        }
+
+    }
+
+    protected String prepareShellCmd(String cmdType, String ipAddress, String optionalParams) {
+        final String CMD_TEMPLATE = String.format("%s %s", cmdType, ipAddress);
+        if (Strings.isNullOrEmpty(optionalParams)) {
+            return CMD_TEMPLATE;
+        } else {
+            if (hasValidChars(optionalParams)) {
+                return String.format("%s %s", CMD_TEMPLATE, optionalParams);
+            } else {
+                return null;
+            }
+        }
+    }
+
+    @Override
+    public List<Class<?>> getCommands() {
+        List<Class<?>> cmdList = new ArrayList<>();
+        cmdList.add(RunDiagnosticsCmd.class);
+        return cmdList;
+    }
+}
\ No newline at end of file
diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml
index c7715a8..2f67c42 100644
--- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml
+++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml
@@ -298,4 +298,6 @@
     <bean id="indirectAgentLBService" class="org.apache.cloudstack.agent.lb.IndirectAgentLBServiceImpl" />
 
     <bean id="directDownloadManager" class="org.apache.cloudstack.direct.download.DirectDownloadManagerImpl" />
+
+    <bean id="DiagnosticsService" class="org.apache.cloudstack.diagnostics.DiagnosticsServiceImpl" />
 </beans>
diff --git a/server/src/test/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImplTest.java
new file mode 100644
index 0000000..d85c543
--- /dev/null
+++ b/server/src/test/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImplTest.java
@@ -0,0 +1,196 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+package org.apache.cloudstack.diagnostics;
+
+import com.cloud.agent.AgentManager;
+import com.cloud.agent.api.routing.NetworkElementCommand;
+import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.vm.VMInstanceVO;
+import com.cloud.vm.VirtualMachine;
+import com.cloud.vm.VirtualMachineManager;
+import com.cloud.vm.dao.VMInstanceDao;
+import junit.framework.TestCase;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.command.admin.diagnostics.RunDiagnosticsCmd;
+import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DiagnosticsServiceImplTest extends TestCase {
+
+    @Mock
+    private AgentManager agentManager;
+    @Mock
+    private VMInstanceDao instanceDao;
+    @Mock
+    private RunDiagnosticsCmd diagnosticsCmd;
+    @Mock
+    private DiagnosticsCommand command;
+    @Mock
+    private VMInstanceVO instanceVO;
+    @Mock
+    private VirtualMachineManager vmManager;
+    @Mock
+    private NetworkOrchestrationService networkManager;
+
+    @InjectMocks
+    private DiagnosticsServiceImpl diagnosticsService = new DiagnosticsServiceImpl();
+
+    @Before
+    public void setUp() throws Exception {
+        Mockito.when(diagnosticsCmd.getId()).thenReturn(1L);
+        Mockito.when(diagnosticsCmd.getType()).thenReturn(DiagnosticsType.PING);
+        Mockito.when(instanceDao.findByIdTypes(Mockito.anyLong(), Mockito.any(VirtualMachine.Type.class),
+                Mockito.any(VirtualMachine.Type.class), Mockito.any(VirtualMachine.Type.class))).thenReturn(instanceVO);
+
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        Mockito.reset(diagnosticsCmd);
+        Mockito.reset(agentManager);
+        Mockito.reset(instanceDao);
+        Mockito.reset(instanceVO);
+        Mockito.reset(command);
+    }
+
+    @Test
+    public void testRunDiagnosticsCommandTrue() throws Exception {
+        Mockito.when(diagnosticsCmd.getAddress()).thenReturn("8.8.8.8");
+        Map<String, String> accessDetailsMap = new HashMap<>();
+        accessDetailsMap.put(NetworkElementCommand.ROUTER_IP, "169.20.175.10");
+        Mockito.when(networkManager.getSystemVMAccessDetails(Mockito.any(VMInstanceVO.class))).thenReturn(accessDetailsMap);
+        final String details = "PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n" +
+                "64 bytes from 8.8.8.8: icmp_seq=1 ttl=125 time=7.88 ms\n" +
+                "64 bytes from 8.8.8.8: icmp_seq=2 ttl=125 time=251 ms\n" +
+                "64 bytes from 8.8.8.8: icmp_seq=3 ttl=125 time=64.9 ms\n" +
+                "64 bytes from 8.8.8.8: icmp_seq=4 ttl=125 time=50.7 ms\n" +
+                "64 bytes from 8.8.8.8: icmp_seq=5 ttl=125 time=67.9 ms\n" +
+                "\n" +
+                "--- 8.8.8.8 ping statistics ---\n" +
+                "5 packets transmitted, 5 received, 0% packet loss, time 4003ms\n" +
+                "rtt min/avg/max/mdev = 7.881/88.587/251.410/84.191 ms&&\n" +
+                "&&\n" +
+                "0\n";
+
+        Mockito.when(agentManager.easySend(Mockito.anyLong(), Mockito.any(DiagnosticsCommand.class))).thenReturn(new DiagnosticsAnswer(command, true, details));
+
+        Map<String, String> detailsMap = diagnosticsService.runDiagnosticsCommand(diagnosticsCmd);
+
+        String stdout = "PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n" +
+                "64 bytes from 8.8.8.8: icmp_seq=1 ttl=125 time=7.88 ms\n" +
+                "64 bytes from 8.8.8.8: icmp_seq=2 ttl=125 time=251 ms\n" +
+                "64 bytes from 8.8.8.8: icmp_seq=3 ttl=125 time=64.9 ms\n" +
+                "64 bytes from 8.8.8.8: icmp_seq=4 ttl=125 time=50.7 ms\n" +
+                "64 bytes from 8.8.8.8: icmp_seq=5 ttl=125 time=67.9 ms\n" +
+                "\n" +
+                "--- 8.8.8.8 ping statistics ---\n" +
+                "5 packets transmitted, 5 received, 0% packet loss, time 4003ms\n" +
+                "rtt min/avg/max/mdev = 7.881/88.587/251.410/84.191 ms";
+
+        assertEquals(3, detailsMap.size());
+        assertEquals("Mismatch between actual and expected STDERR", "", detailsMap.get(ApiConstants.STDERR));
+        assertEquals("Mismatch between actual and expected EXITCODE", "0", detailsMap.get(ApiConstants.EXITCODE));
+        assertEquals("Mismatch between actual and expected STDOUT", stdout, detailsMap.get(ApiConstants.STDOUT));
+    }
+
+    @Test
+    public void testRunDiagnosticsCommandFalse() throws Exception {
+        Mockito.when(diagnosticsCmd.getAddress()).thenReturn("192.0.2.2");
+
+        Map<String, String> accessDetailsMap = new HashMap<>();
+        accessDetailsMap.put(NetworkElementCommand.ROUTER_IP, "169.20.175.10");
+        Mockito.when(networkManager.getSystemVMAccessDetails(Mockito.any(VMInstanceVO.class))).thenReturn(accessDetailsMap);
+
+        String details = "PING 192.0.2.2 (192.0.2.2): 56 data bytes\n" +
+                "76 bytes from 213.130.48.253: Destination Net Unreachable\n" +
+                "--- 192.0.2.2 ping statistics ---\n" +
+                "4 packets transmitted, 0 packets received, 100% packet loss&&\n" +
+                "&&\n" +
+                "1\n";
+        String stdout = "PING 192.0.2.2 (192.0.2.2): 56 data bytes\n" +
+                "76 bytes from 213.130.48.253: Destination Net Unreachable\n" +
+                "--- 192.0.2.2 ping statistics ---\n" +
+                "4 packets transmitted, 0 packets received, 100% packet loss";
+        Mockito.when(agentManager.easySend(Mockito.anyLong(), Mockito.any(DiagnosticsCommand.class))).thenReturn(new DiagnosticsAnswer(command, true, details));
+
+        Map<String, String> detailsMap = diagnosticsService.runDiagnosticsCommand(diagnosticsCmd);
+
+        assertEquals(3, detailsMap.size());
+        assertEquals("Mismatch between actual and expected STDERR", "", detailsMap.get(ApiConstants.STDERR));
+        assertTrue("Mismatch between actual and expected EXITCODE", !detailsMap.get(ApiConstants.EXITCODE).equalsIgnoreCase("0"));
+        assertEquals("Mismatch between actual and expected STDOUT", stdout, detailsMap.get(ApiConstants.STDOUT));
+    }
+
+    @Test(expected = InvalidParameterValueException.class)
+    public void testRunDiagnosticsThrowsInvalidParamException() throws Exception {
+        Mockito.when(diagnosticsCmd.getAddress()).thenReturn("");
+        Mockito.when(instanceDao.findByIdTypes(Mockito.anyLong(), Mockito.any(VirtualMachine.Type.class),
+                Mockito.any(VirtualMachine.Type.class), Mockito.any(VirtualMachine.Type.class))).thenReturn(null);
+
+        diagnosticsService.runDiagnosticsCommand(diagnosticsCmd);
+    }
+
+    @Test(expected = CloudRuntimeException.class)
+    public void testVMControlIPisNull() throws Exception {
+        Mockito.when(diagnosticsCmd.getAddress()).thenReturn("0.42.42.42");
+
+        Map<String, String> accessDetailsMap = new HashMap<>();
+        accessDetailsMap.put(NetworkElementCommand.ROUTER_IP, null);
+        Mockito.when(networkManager.getSystemVMAccessDetails(Mockito.any(VMInstanceVO.class))).thenReturn(accessDetailsMap);
+
+        diagnosticsService.runDiagnosticsCommand(diagnosticsCmd);
+    }
+
+    @Test
+    public void testInvalidCharsInParams() throws Exception {
+        assertFalse(diagnosticsService.hasValidChars("'\\''"));
+        assertFalse(diagnosticsService.hasValidChars("-I eth0 &"));
+        assertFalse(diagnosticsService.hasValidChars("-I eth0 ;"));
+        assertFalse(diagnosticsService.hasValidChars(" &2 > "));
+        assertFalse(diagnosticsService.hasValidChars(" &2 >> "));
+        assertFalse(diagnosticsService.hasValidChars(" | "));
+        assertFalse(diagnosticsService.hasValidChars("|"));
+        assertFalse(diagnosticsService.hasValidChars(","));
+    }
+
+    @Test
+    public void testValidCharsInParams() throws Exception {
+        assertTrue(diagnosticsService.hasValidChars(""));
+        assertTrue(diagnosticsService.hasValidChars("."));
+        assertTrue(diagnosticsService.hasValidChars(" "));
+        assertTrue(diagnosticsService.hasValidChars("-I eth0 www.google.com"));
+        assertTrue(diagnosticsService.hasValidChars(" "));
+        assertTrue(diagnosticsService.hasValidChars(" -I cloudbr0 --sport "));
+        assertTrue(diagnosticsService.hasValidChars(" --back -m20 "));
+        assertTrue(diagnosticsService.hasValidChars("-c 5 -4"));
+        assertTrue(diagnosticsService.hasValidChars("-c 5 -4 -AbDfhqUV"));
+    }
+}
\ No newline at end of file
diff --git a/systemvm/debian/opt/cloud/bin/diagnostics.py b/systemvm/debian/opt/cloud/bin/diagnostics.py
new file mode 100755
index 0000000..477f99d
--- /dev/null
+++ b/systemvm/debian/opt/cloud/bin/diagnostics.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import shlex
+import subprocess
+import sys
+
+
+def run_cmd(command):
+    if command is not None:
+        try:
+            p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+            stdout, stderr = p.communicate()
+            return_code = p.returncode
+
+        except OSError as e:
+            stdout = "Check your command type"
+            stderr = "Exception occurred: %s" % e
+            return_code = 1
+
+        finally:
+            print('%s&&' % stdout.strip())
+            print('%s&&' % stderr.strip())
+            print('%s' % return_code)
+
+
+def get_command():
+    arguments = sys.argv
+    cmd = " ".join(arguments[1:])
+    cmd_type = sys.argv[1]
+
+    if cmd_type == 'ping':
+        if '-c' in arguments:
+            return cmd
+        else:
+            return cmd + " -c 4"
+
+    elif cmd_type == 'traceroute':
+        if '-m' in arguments:
+            return cmd
+        else:
+            return cmd + " -m 20"
+
+    elif cmd_type == 'arping':
+        if '-c' in arguments:
+            return cmd
+        else:
+            return cmd + " -c 4"
+
+    else:
+        return None
+
+
+if __name__ == "__main__":
+    command = get_command()
+    run_cmd(command)
diff --git a/test/integration/smoke/test_diagnostics.py b/test/integration/smoke/test_diagnostics.py
new file mode 100644
index 0000000..6364d83
--- /dev/null
+++ b/test/integration/smoke/test_diagnostics.py
@@ -0,0 +1,539 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+""" BVT tests for remote diagnostics of system VMs
+"""
+# Import Local Modules
+from marvin.codes import FAILED
+from marvin.cloudstackTestCase import cloudstackTestCase
+from marvin.cloudstackAPI import runDiagnostics
+from marvin.lib.utils import (cleanup_resources)
+from marvin.lib.base import (Account,
+                             ServiceOffering,
+                             VirtualMachine)
+from marvin.lib.common import (get_domain,
+                               get_zone,
+                               get_test_template,
+                               list_ssvms,
+                               list_routers)
+
+from nose.plugins.attrib import attr
+
+
+class TestRemoteDiagnostics(cloudstackTestCase):
+    """
+    Test remote diagnostics with system VMs and VR as root admin
+    """
+
+    @classmethod
+    def setUpClass(cls):
+
+        testClient = super(TestRemoteDiagnostics, cls).getClsTestClient()
+        cls.apiclient = testClient.getApiClient()
+        cls.services = testClient.getParsedTestDataConfig()
+
+        # Get Zone, Domain and templates
+        cls.domain = get_domain(cls.apiclient)
+        cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests())
+        cls.hypervisor = testClient.getHypervisorInfo()
+        cls.services['mode'] = cls.zone.networktype
+        template = get_test_template(
+            cls.apiclient,
+            cls.zone.id,
+            cls.hypervisor
+        )
+        if template == FAILED:
+            cls.fail("get_test_template() failed to return template")
+
+        cls.services["virtual_machine"]["zoneid"] = cls.zone.id
+
+        # Create an account, network, VM and IP addresses
+        cls.account = Account.create(
+            cls.apiclient,
+            cls.services["account"],
+            domainid=cls.domain.id
+        )
+        cls.service_offering = ServiceOffering.create(
+            cls.apiclient,
+            cls.services["service_offerings"]["tiny"]
+        )
+        cls.vm_1 = VirtualMachine.create(
+            cls.apiclient,
+            cls.services["virtual_machine"],
+            templateid=template.id,
+            accountid=cls.account.name,
+            domainid=cls.account.domainid,
+            serviceofferingid=cls.service_offering.id
+        )
+        cls.cleanup = [
+            cls.account,
+            cls.service_offering
+        ]
+
+    @classmethod
+    def tearDownClass(cls):
+        try:
+            cls.apiclient = super(
+                TestRemoteDiagnostics,
+                cls
+            ).getClsTestClient().getApiClient()
+            # Clean up, terminate the created templates
+            cleanup_resources(cls.apiclient, cls.cleanup)
+
+        except Exception as e:
+            raise Exception("Warning: Exception during cleanup : %s" % e)
+
+    def setUp(self):
+        self.apiclient = self.testClient.getApiClient()
+        self.hypervisor = self.testClient.getHypervisorInfo()
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_01_ping_in_vr_success(self):
+        '''
+        Test Ping command execution in VR
+        '''
+
+        # Validate the following:
+        # 1. Ping command is executed remotely on VR
+
+        list_router_response = list_routers(
+            self.apiclient,
+            account=self.account.name,
+            domainid=self.account.domainid
+        )
+        self.assertEqual(
+            isinstance(list_router_response, list),
+            True,
+            "Check list response returns a valid list"
+        )
+        router = list_router_response[0]
+        self.debug('Starting the router with ID: %s' % router.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = router.id
+        cmd.ipaddress = '8.8.8.8'
+        cmd.type = 'ping'
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertEqual(
+            '0',
+            cmd_response.exitcode,
+            'Failed to run remote Ping in VR')
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_02_ping_in_vr_failure(self):
+        '''
+        Test Ping command execution in VR
+        '''
+
+        # Validate the following:
+        # 1. Ping command is executed remotely on VR
+        # 2. Validate Ping command execution with a non-existent/pingable IP address
+
+        if self.hypervisor.lower() == 'simulator':
+            raise self.skipTest("Skipping negative test case for Simulator hypervisor")
+
+        list_router_response = list_routers(
+            self.apiclient,
+            account=self.account.name,
+            domainid=self.account.domainid
+        )
+        self.assertEqual(
+            isinstance(list_router_response, list),
+            True,
+            "Check list response returns a valid list"
+        )
+        router = list_router_response[0]
+        self.debug('Starting the router with ID: %s' % router.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = router.id
+        cmd.ipaddress = '192.0.2.2'
+        cmd.type = 'ping'
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertNotEqual(
+            '0',
+            cmd_response.exitcode,
+            'Check diagnostics command returns a non-zero exit code')
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_03_ping_in_ssvm_success(self):
+        '''
+        Test Ping command execution in SSVM
+        '''
+
+        # Validate the following:
+        # 1. Ping command is executed remotely on SSVM
+
+        list_ssvm_response = list_ssvms(
+            self.apiclient,
+            systemvmtype='secondarystoragevm',
+            state='Running',
+        )
+
+        self.assertEqual(
+            isinstance(list_ssvm_response, list),
+            True,
+            'Check list response returns a valid list'
+        )
+        ssvm = list_ssvm_response[0]
+
+        self.debug('Setting up SSVM with ID %s' % ssvm.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = ssvm.id
+        cmd.ipaddress = '8.8.8.8'
+        cmd.type = 'ping'
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertEqual(
+            '0',
+            cmd_response.exitcode,
+            'Failed to run remote Ping in SSVM'
+        )
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_04_ping_in_ssvm_failure(self):
+        '''
+        Test Ping command execution in SSVM
+        '''
+
+        # Validate the following:
+        # 1. Ping command is executed remotely on SSVM
+        # 2. Validate Ping command execution with a non-existent/pingable IP address
+
+        if self.hypervisor.lower() == 'simulator':
+            raise self.skipTest("Skipping negative test case for Simulator hypervisor")
+
+        list_ssvm_response = list_ssvms(
+            self.apiclient,
+            systemvmtype='secondarystoragevm',
+            state='Running',
+        )
+
+        self.assertEqual(
+            isinstance(list_ssvm_response, list),
+            True,
+            'Check list response returns a valid list'
+        )
+        ssvm = list_ssvm_response[0]
+
+        self.debug('Setting up SSVM with ID %s' % ssvm.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = ssvm.id
+        cmd.ipaddress = '192.0.2.2'
+        cmd.type = 'ping'
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertNotEqual(
+            '0',
+            cmd_response.exitcode,
+            'Failed to run remote Ping in SSVM'
+        )
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_05_ping_in_cpvm_success(self):
+        '''
+        Test Ping command execution in CPVM
+        '''
+
+        # Validate the following:
+        # 1. Ping command is executed remotely on CPVM
+
+        list_ssvm_response = list_ssvms(
+            self.apiclient,
+            systemvmtype='consoleproxy',
+            state='Running',
+        )
+
+        self.assertEqual(
+            isinstance(list_ssvm_response, list),
+            True,
+            'Check list response returns a valid list'
+        )
+        cpvm = list_ssvm_response[0]
+
+        self.debug('Setting up CPVM with ID %s' % cpvm.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = cpvm.id
+        cmd.ipaddress = '8.8.8.8'
+        cmd.type = 'ping'
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertEqual(
+            '0',
+            cmd_response.exitcode,
+            'Failed to run remote Ping in CPVM'
+        )
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_06_ping_in_cpvm_failure(self):
+        '''
+        Test Ping command execution in CPVM
+        '''
+
+        # Validate the following:
+        # 1. Ping command is executed remotely on CPVM
+        # 2. Validate Ping command execution with a non-existent/pingable IP address
+
+        if self.hypervisor.lower() == 'simulator':
+            raise self.skipTest("Skipping negative test case for Simulator hypervisor")
+
+        list_ssvm_response = list_ssvms(
+            self.apiclient,
+            systemvmtype='consoleproxy',
+            state='Running',
+        )
+
+        self.assertEqual(
+            isinstance(list_ssvm_response, list),
+            True,
+            'Check list response returns a valid list'
+        )
+        cpvm = list_ssvm_response[0]
+
+        self.debug('Setting up CPVM with ID %s' % cpvm.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = cpvm.id
+        cmd.ipaddress = '192.0.2.2'
+        cmd.type = 'ping'
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertNotEqual(
+            '0',
+            cmd_response.exitcode,
+            'Check diagnostics command returns a non-zero exit code'
+        )
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_07_arping_in_vr(self):
+        '''
+        Test Arping command execution in VR
+        '''
+
+        # Validate the following:
+        # 1. Arping command is executed remotely on VR
+
+        list_router_response = list_routers(
+            self.apiclient,
+            account=self.account.name,
+            domainid=self.account.domainid
+        )
+        self.assertEqual(
+            isinstance(list_router_response, list),
+            True,
+            "Check list response returns a valid list"
+        )
+        router = list_router_response[0]
+        self.debug('Starting the router with ID: %s' % router.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = router.id
+        cmd.ipaddress = router.gateway
+        cmd.type = 'arping'
+        cmd.params = "-I eth2"
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertEqual(
+            '0',
+            cmd_response.exitcode,
+            'Failed to run remote Arping in VR')
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_08_arping_in_ssvm(self):
+        '''
+        Test Arping command execution in SSVM
+        '''
+
+        # Validate the following:
+        # 1. Arping command is executed remotely on SSVM
+
+        list_ssvm_response = list_ssvms(
+            self.apiclient,
+            systemvmtype='secondarystoragevm',
+            state='Running',
+        )
+
+        self.assertEqual(
+            isinstance(list_ssvm_response, list),
+            True,
+            'Check list response returns a valid list'
+        )
+        ssvm = list_ssvm_response[0]
+
+        self.debug('Setting up SSVM with ID %s' % ssvm.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = ssvm.id
+        cmd.ipaddress = ssvm.gateway
+        cmd.type = 'arping'
+        cmd.params = '-I eth2'
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertEqual(
+            '0',
+            cmd_response.exitcode,
+            'Failed to run remote Arping in SSVM'
+        )
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_09_arping_in_cpvm(self):
+        '''
+        Test Arping command execution in CPVM
+        '''
+
+        # Validate the following:
+        # 1. Arping command is executed remotely on CPVM
+
+        list_cpvm_response = list_ssvms(
+            self.apiclient,
+            systemvmtype='secondarystoragevm',
+            state='Running',
+        )
+
+        self.assertEqual(
+            isinstance(list_cpvm_response, list),
+            True,
+            'Check list response returns a valid list'
+        )
+        cpvm = list_cpvm_response[0]
+
+        self.debug('Setting up CPVM with ID %s' % cpvm.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = cpvm.id
+        cmd.ipaddress = cpvm.gateway
+        cmd.type = 'arping'
+        cmd.params = '-I eth2'
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertEqual(
+            '0',
+            cmd_response.exitcode,
+            'Failed to run remote Arping in CPVM'
+        )
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_10_traceroute_in_vr(self):
+        '''
+        Test Arping command execution in VR
+        '''
+
+        # Validate the following:
+        # 1. Arping command is executed remotely on VR
+
+        list_router_response = list_routers(
+            self.apiclient,
+            account=self.account.name,
+            domainid=self.account.domainid
+        )
+        self.assertEqual(
+            isinstance(list_router_response, list),
+            True,
+            "Check list response returns a valid list"
+        )
+        router = list_router_response[0]
+        self.debug('Starting the router with ID: %s' % router.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = router.id
+        cmd.ipaddress = '8.8.4.4'
+        cmd.type = 'traceroute'
+        cmd.params = "-m 10"
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertEqual(
+            '0',
+            cmd_response.exitcode,
+            'Failed to run remote Arping in VR')
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_11_traceroute_in_ssvm(self):
+        '''
+        Test Traceroute command execution in SSVM
+        '''
+
+        # Validate the following:
+        # 1. Traceroute command is executed remotely on SSVM
+
+        list_ssvm_response = list_ssvms(
+            self.apiclient,
+            systemvmtype='secondarystoragevm',
+            state='Running',
+        )
+
+        self.assertEqual(
+            isinstance(list_ssvm_response, list),
+            True,
+            'Check list response returns a valid list'
+        )
+        ssvm = list_ssvm_response[0]
+
+        self.debug('Setting up SSVM with ID %s' % ssvm.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = ssvm.id
+        cmd.ipaddress = '8.8.4.4'
+        cmd.type = 'traceroute'
+        cmd.params = '-m 10'
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertEqual(
+            '0',
+            cmd_response.exitcode,
+            'Failed to run remote Traceroute in SSVM'
+        )
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true")
+    def test_12_traceroute_in_cpvm(self):
+        '''
+        Test Traceroute command execution in CPVMM
+        '''
+
+        # Validate the following:
+        # 1. Traceroute command is executed remotely on CPVM
+
+        list_cpvm_response = list_ssvms(
+            self.apiclient,
+            systemvmtype='consoleproxy',
+            state='Running',
+        )
+
+        self.assertEqual(
+            isinstance(list_cpvm_response, list),
+            True,
+            'Check list response returns a valid list'
+        )
+        cpvm = list_cpvm_response[0]
+
+        self.debug('Setting up CPVMM with ID %s' % cpvm.id)
+
+        cmd = runDiagnostics.runDiagnosticsCmd()
+        cmd.targetid = cpvm.id
+        cmd.ipaddress = '8.8.4.4'
+        cmd.type = 'traceroute'
+        cmd.params = '-m 10'
+        cmd_response = self.apiclient.runDiagnostics(cmd)
+
+        self.assertEqual(
+            '0',
+            cmd_response.exitcode,
+            'Failed to run remote Traceroute in CPVM'
+        )
diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py
index a025efe..f45f030 100644
--- a/tools/apidoc/gen_toc.py
+++ b/tools/apidoc/gen_toc.py
@@ -190,7 +190,8 @@ known_categories = {
     'CA': 'Certificate',
     'listElastistorInterface': 'Misc',
     'cloudian': 'Cloudian',
-    'Sioc' : 'Sioc'
+    'Sioc' : 'Sioc',
+    'Diagnostics': 'Diagnostics'
     }