framework/cluster: improve cluster service and integration API service
- mTLS implementation for cluster service communication
- Listen only on the specified cluster node IP address instead of all interfaces
- Validate incoming cluster service requests are from peer management servers based on the server's certificate dns name which can be through global config - ca.framework.cert.management.custom.san
- Hardening of KVM command wrapper script execution
- Improve API server integration port check
- cloudstack-management.default: don't have JMX configuration if not needed. JMX is used for instrumentation; users who need to use it should enable it explicitly
Co-authored-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
Co-authored-by: Wei Zhou <weizhou@apache.org>
Co-authored-by: Rohit Yadav <rohit.yadav@shapeblue.com>
Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
diff --git a/api/src/main/java/org/apache/cloudstack/ca/CAManager.java b/api/src/main/java/org/apache/cloudstack/ca/CAManager.java
index 12a9d3d..b0fb1ac 100644
--- a/api/src/main/java/org/apache/cloudstack/ca/CAManager.java
+++ b/api/src/main/java/org/apache/cloudstack/ca/CAManager.java
@@ -77,6 +77,14 @@
"15",
"The number of days before expiry of a client certificate, the validations are checked. Admins are alerted when auto-renewal is not allowed, otherwise auto-renewal is attempted.", true, ConfigKey.Scope.Cluster);
+
+ ConfigKey<String> CertManagementCustomSubjectAlternativeName = new ConfigKey<>("Advanced", String.class,
+ "ca.framework.cert.management.custom.san",
+ "cloudstack.internal",
+ "The custom Subject Alternative Name that will be added to the management server certificate. " +
+ "The actual implementation will depend on the configured CA provider.",
+ false);
+
/**
* Returns a list of available CA provider plugins
* @return returns list of CAProvider
diff --git a/core/src/main/java/com/cloud/resource/CommandWrapper.java b/core/src/main/java/com/cloud/resource/CommandWrapper.java
index d9c1ea2..ee6aa16 100644
--- a/core/src/main/java/com/cloud/resource/CommandWrapper.java
+++ b/core/src/main/java/com/cloud/resource/CommandWrapper.java
@@ -19,9 +19,12 @@
package com.cloud.resource;
+import org.apache.log4j.Logger;
+
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
-import org.apache.log4j.Logger;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.utils.script.Script;
public abstract class CommandWrapper<T extends Command, A extends Answer, R extends ServerResource> {
protected Logger logger = Logger.getLogger(getClass());
@@ -32,4 +35,26 @@
* @return A and the Answer from the command.
*/
public abstract A execute(T command, R serverResource);
+
+ protected String sanitizeBashCommandArgument(String input) {
+ StringBuilder sanitized = new StringBuilder();
+ for (char c : input.toCharArray()) {
+ if ("\\\"'`$|&;()<>*?![]{}~".indexOf(c) != -1) {
+ sanitized.append('\\');
+ }
+ sanitized.append(c);
+ }
+ return sanitized.toString();
+ }
+
+ public void removeDpdkPort(String portToRemove) {
+ logger.debug("Removing DPDK port: " + portToRemove);
+ int port;
+ try {
+ port = Integer.valueOf(portToRemove);
+ } catch (NumberFormatException nfe) {
+ throw new CloudRuntimeException(String.format("Invalid DPDK port specified: '%s'", portToRemove));
+ }
+ Script.executeCommand("ovs-vsctl", "del-port", String.valueOf(port));
+ }
}
diff --git a/framework/ca/src/main/java/org/apache/cloudstack/framework/ca/CAProvider.java b/framework/ca/src/main/java/org/apache/cloudstack/framework/ca/CAProvider.java
index 388cae7..77b3ee2 100644
--- a/framework/ca/src/main/java/org/apache/cloudstack/framework/ca/CAProvider.java
+++ b/framework/ca/src/main/java/org/apache/cloudstack/framework/ca/CAProvider.java
@@ -22,6 +22,7 @@
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
+import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
@@ -45,6 +46,7 @@
/**
* Issues certificate with provided options
+ *
* @param domainNames
* @param ipAddresses
* @param validityDays
@@ -104,4 +106,6 @@
* @return returns description
*/
String getDescription();
+
+ boolean isManagementCertificate(java.security.cert.Certificate certificate) throws CertificateParsingException;
}
diff --git a/framework/ca/src/main/java/org/apache/cloudstack/framework/ca/CAService.java b/framework/ca/src/main/java/org/apache/cloudstack/framework/ca/CAService.java
index facf13a..721c88b 100644
--- a/framework/ca/src/main/java/org/apache/cloudstack/framework/ca/CAService.java
+++ b/framework/ca/src/main/java/org/apache/cloudstack/framework/ca/CAService.java
@@ -21,6 +21,7 @@
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
+import java.security.cert.CertificateParsingException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
@@ -47,4 +48,6 @@
* @return returns char[] passphrase
*/
char[] getKeyStorePassphrase();
+
+ boolean isManagementCertificate(java.security.cert.Certificate certificate) throws CertificateParsingException;
}
diff --git a/framework/cluster/src/main/java/com/cloud/cluster/ClusterManager.java b/framework/cluster/src/main/java/com/cloud/cluster/ClusterManager.java
index 1b1406c..54f5758 100644
--- a/framework/cluster/src/main/java/com/cloud/cluster/ClusterManager.java
+++ b/framework/cluster/src/main/java/com/cloud/cluster/ClusterManager.java
@@ -16,8 +16,8 @@
// under the License.
package com.cloud.cluster;
-import org.apache.cloudstack.management.ManagementServerHost;
import org.apache.cloudstack.framework.config.ConfigKey;
+import org.apache.cloudstack.management.ManagementServerHost;
import com.cloud.utils.component.Manager;
@@ -77,6 +77,8 @@
*/
String getSelfPeerName();
+ String getSelfNodeIP();
+
long getManagementNodeId();
/**
diff --git a/framework/cluster/src/main/java/com/cloud/cluster/ClusterManagerImpl.java b/framework/cluster/src/main/java/com/cloud/cluster/ClusterManagerImpl.java
index 289638f..d601c09 100644
--- a/framework/cluster/src/main/java/com/cloud/cluster/ClusterManagerImpl.java
+++ b/framework/cluster/src/main/java/com/cloud/cluster/ClusterManagerImpl.java
@@ -40,17 +40,17 @@
import javax.inject.Inject;
import javax.naming.ConfigurationException;
-import com.cloud.cluster.dao.ManagementServerStatusDao;
-import org.apache.cloudstack.management.ManagementServerHost;
import org.apache.cloudstack.framework.config.ConfigDepot;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
+import org.apache.cloudstack.management.ManagementServerHost;
import org.apache.cloudstack.utils.identity.ManagementServerNode;
import org.apache.log4j.Logger;
import com.cloud.cluster.dao.ManagementServerHostDao;
import com.cloud.cluster.dao.ManagementServerHostPeerDao;
+import com.cloud.cluster.dao.ManagementServerStatusDao;
import com.cloud.utils.DateUtil;
import com.cloud.utils.Profiler;
import com.cloud.utils.component.ComponentLifecycle;
@@ -130,7 +130,7 @@
// recursive remote calls between nodes
//
_executor = Executors.newCachedThreadPool(new NamedThreadFactory("Cluster-Worker"));
- setRunLevel(ComponentLifecycle.RUN_LEVEL_FRAMEWORK);
+ setRunLevel(ComponentLifecycle.RUN_LEVEL_COMPONENT);
}
private void registerRequestPdu(final ClusterServiceRequestPdu pdu) {
@@ -475,6 +475,7 @@
return Long.toString(_msId);
}
+ @Override
public String getSelfNodeIP() {
return _clusterNodeIP;
}
diff --git a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceAdapter.java b/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceAdapter.java
index 735de5b..e073a28 100644
--- a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceAdapter.java
+++ b/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceAdapter.java
@@ -28,7 +28,5 @@
public ClusterService getPeerService(String strPeer) throws RemoteException;
- public String getServiceEndpointName(String strPeer);
-
public int getServicePort();
}
diff --git a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletAdapter.java b/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletAdapter.java
index 7451b5f..15ee055 100644
--- a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletAdapter.java
+++ b/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletAdapter.java
@@ -23,8 +23,9 @@
import javax.inject.Inject;
import javax.naming.ConfigurationException;
-import org.apache.log4j.Logger;
+import org.apache.cloudstack.ca.CAManager;
import org.apache.cloudstack.framework.config.ConfigDepot;
+import org.apache.log4j.Logger;
import com.cloud.cluster.dao.ManagementServerHostDao;
import com.cloud.utils.NumbersUtil;
@@ -44,6 +45,8 @@
@Inject
private ManagementServerHostDao _mshostDao;
@Inject
+ private CAManager caService;
+ @Inject
protected ConfigDepot _configDepot;
private ClusterServiceServletContainer _servletContainer;
@@ -51,7 +54,7 @@
private int _clusterServicePort = DEFAULT_SERVICE_PORT;
public ClusterServiceServletAdapter() {
- setRunLevel(ComponentLifecycle.RUN_LEVEL_FRAMEWORK);
+ setRunLevel(ComponentLifecycle.RUN_LEVEL_COMPONENT);
}
@Override
@@ -66,12 +69,10 @@
String serviceUrl = getServiceEndpointName(strPeer);
if (serviceUrl == null)
return null;
-
- return new ClusterServiceServletImpl(serviceUrl);
+ return new ClusterServiceServletImpl(serviceUrl, caService);
}
- @Override
- public String getServiceEndpointName(String strPeer) {
+ protected String getServiceEndpointName(String strPeer) {
try {
init();
} catch (ConfigurationException e) {
@@ -95,7 +96,7 @@
private String composeEndpointName(String nodeIP, int port) {
StringBuffer sb = new StringBuffer();
- sb.append("http://").append(nodeIP).append(":").append(port).append("/clusterservice");
+ sb.append("https://").append(nodeIP).append(":").append(port).append("/clusterservice");
return sb.toString();
}
@@ -108,7 +109,8 @@
@Override
public boolean start() {
_servletContainer = new ClusterServiceServletContainer();
- _servletContainer.start(new ClusterServiceServletHttpHandler(_manager), _clusterServicePort);
+ _servletContainer.start(new ClusterServiceServletHttpHandler(_manager), _manager.getSelfNodeIP(),
+ _clusterServicePort, caService);
return true;
}
diff --git a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletContainer.java b/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletContainer.java
index 69cc871..1aa9caa 100644
--- a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletContainer.java
+++ b/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletContainer.java
@@ -17,11 +17,23 @@
package com.cloud.cluster;
import java.io.IOException;
-import java.net.ServerSocket;
+import java.net.InetAddress;
import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateParsingException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+import org.apache.cloudstack.framework.ca.CAService;
+import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.http.ConnectionClosedException;
import org.apache.http.HttpException;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
@@ -43,9 +55,9 @@
import org.apache.http.protocol.ResponseServer;
import org.apache.log4j.Logger;
-import org.apache.cloudstack.managed.context.ManagedContextRunnable;
-
+import com.cloud.utils.StringUtils;
import com.cloud.utils.concurrency.NamedThreadFactory;
+import com.cloud.utils.nio.Link;
public class ClusterServiceServletContainer {
private static final Logger s_logger = Logger.getLogger(ClusterServiceServletContainer.class);
@@ -55,9 +67,9 @@
public ClusterServiceServletContainer() {
}
- public boolean start(HttpRequestHandler requestHandler, int port) {
+ public boolean start(HttpRequestHandler requestHandler, String ip, int port, CAService caService) {
- listenerThread = new ListenerThread(requestHandler, port);
+ listenerThread = new ListenerThread(requestHandler, ip, port, caService);
listenerThread.start();
return true;
@@ -69,24 +81,43 @@
}
}
- static class ListenerThread extends Thread {
- private HttpService _httpService = null;
- private volatile ServerSocket _serverSocket = null;
- private HttpParams _params = null;
- private ExecutorService _executor;
+ protected static SSLServerSocket getSecuredServerSocket(SSLContext sslContext, String ip, int port)
+ throws IOException {
+ SSLServerSocketFactory sslFactory = sslContext.getServerSocketFactory();
+ SSLServerSocket serverSocket = null;
+ if (StringUtils.isNotEmpty(ip)) {
+ serverSocket = (SSLServerSocket) sslFactory.createServerSocket(port, 0,
+ InetAddress.getByName(ip));
+ } else {
+ serverSocket = (SSLServerSocket) sslFactory.createServerSocket(port);
+ }
+ serverSocket.setNeedClientAuth(true);
+ return serverSocket;
+ }
- public ListenerThread(HttpRequestHandler requestHandler, int port) {
- _executor = Executors.newCachedThreadPool(new NamedThreadFactory("Cluster-Listener"));
+ static class ListenerThread extends Thread {
+ private HttpService httpService = null;
+ private volatile SSLServerSocket serverSocket = null;
+ private HttpParams params = null;
+ private ExecutorService executor;
+ private CAService caService = null;
+
+ public ListenerThread(HttpRequestHandler requestHandler, String ip, int port,
+ CAService caService) {
+ this.executor = Executors.newCachedThreadPool(new NamedThreadFactory("Cluster-Listener"));
+ this.caService = caService;
try {
- _serverSocket = new ServerSocket(port);
- } catch (IOException ioex) {
- s_logger.error("error initializing cluster service servlet container", ioex);
+ SSLContext sslContext = Link.initManagementSSLContext(caService);
+ serverSocket = getSecuredServerSocket(sslContext, ip, port);
+ } catch (IOException | GeneralSecurityException e) {
+ s_logger.error("Error initializing cluster service servlet container for secure connection",
+ e);
return;
}
- _params = new BasicHttpParams();
- _params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000)
+ params = new BasicHttpParams();
+ params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000)
.setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024)
.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false)
.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true)
@@ -104,35 +135,55 @@
reqistry.register("/clusterservice", requestHandler);
// Set up the HTTP service
- _httpService = new HttpService(httpproc, new DefaultConnectionReuseStrategy(), new DefaultHttpResponseFactory());
- _httpService.setParams(_params);
- _httpService.setHandlerResolver(reqistry);
+ httpService = new HttpService(httpproc, new DefaultConnectionReuseStrategy(), new DefaultHttpResponseFactory());
+ httpService.setParams(params);
+ httpService.setHandlerResolver(reqistry);
}
public void stopRunning() {
- if (_serverSocket != null) {
+ if (serverSocket != null) {
try {
- _serverSocket.close();
+ serverSocket.close();
} catch (IOException e) {
s_logger.info("[ignored] error on closing server socket", e);
}
- _serverSocket = null;
+ serverSocket = null;
}
}
+ protected boolean isValidPeerConnection(Socket socket) throws SSLPeerUnverifiedException,
+ CertificateParsingException {
+ SSLSocket sslSocket = (SSLSocket) socket;
+ SSLSession session = sslSocket.getSession();
+ if (session == null || !session.isValid()) {
+ return false;
+ }
+ Certificate[] certs = session.getPeerCertificates();
+ if (certs == null || certs.length < 1) {
+ return false;
+ }
+ return caService.isManagementCertificate(certs[0]);
+ }
+
@Override
public void run() {
if (s_logger.isInfoEnabled())
- s_logger.info("Cluster service servlet container listening on port " + _serverSocket.getLocalPort());
+ s_logger.info(String.format("Cluster service servlet container listening on host: %s and port %d",
+ serverSocket.getInetAddress().getHostAddress(), serverSocket.getLocalPort()));
- while (_serverSocket != null) {
+ while (serverSocket != null) {
try {
// Set up HTTP connection
- Socket socket = _serverSocket.accept();
+ Socket socket = serverSocket.accept();
final DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
- conn.bind(socket, _params);
-
- _executor.execute(new ManagedContextRunnable() {
+ conn.bind(socket, params);
+ if (!isValidPeerConnection(socket)) {
+ s_logger.warn(String.format("Failure during validating cluster request from %s",
+ socket.getInetAddress().getHostAddress()));
+ conn.shutdown();
+ continue;
+ }
+ executor.execute(new ManagedContextRunnable() {
@Override
protected void runInContext() {
HttpContext context = new BasicHttpContext(null);
@@ -141,7 +192,7 @@
if (s_logger.isTraceEnabled())
s_logger.trace("dispatching cluster request from " + conn.getRemoteAddress().toString());
- _httpService.handleRequest(conn, context);
+ httpService.handleRequest(conn, context);
if (s_logger.isTraceEnabled())
s_logger.trace("Cluster request from " + conn.getRemoteAddress().toString() + " is processed");
@@ -176,7 +227,7 @@
}
}
- _executor.shutdown();
+ executor.shutdown();
if (s_logger.isInfoEnabled())
s_logger.info("Cluster service servlet container shutdown");
}
diff --git a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletImpl.java b/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletImpl.java
index ec8b908..c5b6145 100644
--- a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletImpl.java
+++ b/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletImpl.java
@@ -17,98 +17,143 @@
package com.cloud.cluster;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
import java.rmi.RemoteException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
-import org.apache.commons.httpclient.HttpClient;
-import org.apache.commons.httpclient.HttpException;
+import javax.net.ssl.SSLContext;
+
+import org.apache.cloudstack.framework.ca.CAService;
import org.apache.commons.httpclient.HttpStatus;
-import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
-import org.apache.commons.httpclient.methods.PostMethod;
-import org.apache.commons.httpclient.params.HttpClientParams;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
+import com.cloud.utils.HttpUtils;
import com.cloud.utils.Profiler;
+import com.cloud.utils.nio.Link;
+import com.google.gson.Gson;
public class ClusterServiceServletImpl implements ClusterService {
private static final long serialVersionUID = 4574025200012566153L;
private static final Logger s_logger = Logger.getLogger(ClusterServiceServletImpl.class);
- private String _serviceUrl;
+ private String serviceUrl;
- protected static HttpClient s_client = null;
+ private CAService caService;
+
+ private Gson gson = new Gson();
+
+ protected static CloseableHttpClient s_client = null;
+
+ private void logPostParametersForFailedEncoding(List<NameValuePair> parameters) {
+ if (s_logger.isTraceEnabled()) {
+ s_logger.trace(String.format("%s encoding failed for POST parameters: %s", HttpUtils.UTF_8,
+ gson.toJson(parameters)));
+ }
+ }
public ClusterServiceServletImpl() {
}
- public ClusterServiceServletImpl(final String serviceUrl) {
- s_logger.info("Setup cluster service servlet. service url: " + serviceUrl + ", request timeout: " + ClusterServiceAdapter.ClusterMessageTimeOut.value() +
- " seconds");
+ public ClusterServiceServletImpl(final String serviceUrl, final CAService caService) {
+ s_logger.info(String.format("Setup cluster service servlet. service url: %s, request timeout: %d seconds", serviceUrl,
+ ClusterServiceAdapter.ClusterMessageTimeOut.value()));
+ this.serviceUrl = serviceUrl;
+ this.caService = caService;
+ }
- _serviceUrl = serviceUrl;
+ protected List<NameValuePair> getClusterServicePduPostParameters(final ClusterServicePdu pdu) {
+ List<NameValuePair> postParameters = new ArrayList<>();
+ postParameters.add(new BasicNameValuePair("method", Integer.toString(RemoteMethodConstants.METHOD_DELIVER_PDU)));
+ postParameters.add(new BasicNameValuePair("sourcePeer", pdu.getSourcePeer()));
+ postParameters.add(new BasicNameValuePair("destPeer", pdu.getDestPeer()));
+ postParameters.add(new BasicNameValuePair("pduSeq", Long.toString(pdu.getSequenceId())));
+ postParameters.add(new BasicNameValuePair("pduAckSeq", Long.toString(pdu.getAckSequenceId())));
+ postParameters.add(new BasicNameValuePair("agentId", Long.toString(pdu.getAgentId())));
+ postParameters.add(new BasicNameValuePair("gsonPackage", pdu.getJsonPackage()));
+ postParameters.add(new BasicNameValuePair("stopOnError", pdu.isStopOnError() ? "1" : "0"));
+ postParameters.add(new BasicNameValuePair("pduType", Integer.toString(pdu.getPduType())));
+ return postParameters;
}
@Override
public String execute(final ClusterServicePdu pdu) throws RemoteException {
-
- final HttpClient client = getHttpClient();
- final PostMethod method = new PostMethod(_serviceUrl);
-
- method.addParameter("method", Integer.toString(RemoteMethodConstants.METHOD_DELIVER_PDU));
- method.addParameter("sourcePeer", pdu.getSourcePeer());
- method.addParameter("destPeer", pdu.getDestPeer());
- method.addParameter("pduSeq", Long.toString(pdu.getSequenceId()));
- method.addParameter("pduAckSeq", Long.toString(pdu.getAckSequenceId()));
- method.addParameter("agentId", Long.toString(pdu.getAgentId()));
- method.addParameter("gsonPackage", pdu.getJsonPackage());
- method.addParameter("stopOnError", pdu.isStopOnError() ? "1" : "0");
- method.addParameter("pduType", Integer.toString(pdu.getPduType()));
+ if (s_logger.isDebugEnabled()) {
+ s_logger.debug(String.format("Executing ClusterServicePdu with service URL: %s", serviceUrl));
+ }
+ final CloseableHttpClient client = getHttpClient();
+ final HttpPost method = new HttpPost(serviceUrl);
+ final List<NameValuePair> postParameters = getClusterServicePduPostParameters(pdu);
+ try {
+ method.setEntity(new UrlEncodedFormEntity(postParameters, HttpUtils.UTF_8));
+ } catch (UnsupportedEncodingException e) {
+ s_logger.error("Failed to encode request POST parameters", e);
+ logPostParametersForFailedEncoding(postParameters);
+ throw new RemoteException("Failed to encode request POST parameters", e);
+ }
return executePostMethod(client, method);
}
+ protected List<NameValuePair> getPingPostParameters(final String callingPeer) {
+ List<NameValuePair> postParameters = new ArrayList<>();
+ postParameters.add(new BasicNameValuePair("method", Integer.toString(RemoteMethodConstants.METHOD_PING)));
+ postParameters.add(new BasicNameValuePair("callingPeer", callingPeer));
+ return postParameters;
+ }
+
@Override
public boolean ping(final String callingPeer) throws RemoteException {
if (s_logger.isDebugEnabled()) {
- s_logger.debug("Ping at " + _serviceUrl);
+ s_logger.debug("Ping at " + serviceUrl);
}
- final HttpClient client = getHttpClient();
- final PostMethod method = new PostMethod(_serviceUrl);
+ final CloseableHttpClient client = getHttpClient();
+ final HttpPost method = new HttpPost(serviceUrl);
- method.addParameter("method", Integer.toString(RemoteMethodConstants.METHOD_PING));
- method.addParameter("callingPeer", callingPeer);
+ List<NameValuePair> postParameters = getPingPostParameters(callingPeer);
+ try {
+ method.setEntity(new UrlEncodedFormEntity(postParameters, HttpUtils.UTF_8));
+ } catch (UnsupportedEncodingException e) {
+ s_logger.error("Failed to encode ping request POST parameters", e);
+ logPostParametersForFailedEncoding(postParameters);
+ throw new RemoteException("Failed to encode ping request POST parameters", e);
+ }
final String returnVal = executePostMethod(client, method);
- if ("true".equalsIgnoreCase(returnVal)) {
- return true;
- }
- return false;
+ return Boolean.TRUE.toString().equalsIgnoreCase(returnVal);
}
- private String executePostMethod(final HttpClient client, final PostMethod method) {
- int response = 0;
+ private String executePostMethod(final CloseableHttpClient client, final HttpPost method) {
String result = null;
try {
final Profiler profiler = new Profiler();
profiler.start();
- response = client.executeMethod(method);
+ CloseableHttpResponse httpResponse = client.execute(method);
+ int response = httpResponse.getStatusLine().getStatusCode();
if (response == HttpStatus.SC_OK) {
- result = method.getResponseBodyAsString();
+ result = EntityUtils.toString(httpResponse.getEntity());
profiler.stop();
if (s_logger.isDebugEnabled()) {
- s_logger.debug("POST " + _serviceUrl + " response :" + result + ", responding time: " + profiler.getDurationInMillis() + " ms");
+ s_logger.debug("POST " + serviceUrl + " response :" + result + ", responding time: " + profiler.getDurationInMillis() + " ms");
}
} else {
profiler.stop();
- s_logger.error("Invalid response code : " + response + ", from : " + _serviceUrl + ", method : " + method.getParameter("method") + " responding time: " +
+ s_logger.error("Invalid response code : " + response + ", from : " + serviceUrl + ", method : " + method.getParams().getParameter("method") + " responding time: " +
profiler.getDurationInMillis());
}
- } catch (final HttpException e) {
- s_logger.error("HttpException from : " + _serviceUrl + ", method : " + method.getParameter("method"));
- } catch (final IOException e) {
- s_logger.error("IOException from : " + _serviceUrl + ", method : " + method.getParameter("method"));
- } catch (final Throwable e) {
- s_logger.error("Exception from : " + _serviceUrl + ", method : " + method.getParameter("method") + ", exception :", e);
+ } catch (IOException e) {
+ s_logger.error("Exception from : " + serviceUrl + ", method : " + method.getParams().getParameter("method") + ", exception :", e);
} finally {
method.releaseConnection();
}
@@ -116,20 +161,25 @@
return result;
}
- private HttpClient getHttpClient() {
-
+ private CloseableHttpClient getHttpClient() {
if (s_client == null) {
- final MultiThreadedHttpConnectionManager mgr = new MultiThreadedHttpConnectionManager();
- mgr.getParams().setDefaultMaxConnectionsPerHost(4);
+ SSLContext sslContext = null;
+ try {
+ sslContext = Link.initManagementSSLContext(caService);
+ } catch (GeneralSecurityException | IOException e) {
+ throw new RuntimeException(e);
+ }
- // TODO make it configurable
- mgr.getParams().setMaxTotalConnections(1000);
+ int timeout = ClusterServiceAdapter.ClusterMessageTimeOut.value() * 1000;
+ RequestConfig config = RequestConfig.custom()
+ .setConnectTimeout(timeout)
+ .setConnectionRequestTimeout(timeout)
+ .setSocketTimeout(timeout).build();
- s_client = new HttpClient(mgr);
- final HttpClientParams clientParams = new HttpClientParams();
- clientParams.setSoTimeout(ClusterServiceAdapter.ClusterMessageTimeOut.value() * 1000);
-
- s_client.setParams(clientParams);
+ s_client = HttpClientBuilder.create()
+ .setDefaultRequestConfig(config)
+ .setSSLContext(sslContext)
+ .build();
}
return s_client;
}
diff --git a/framework/cluster/src/test/java/com/cloud/cluster/ClusterManagerImplTest.java b/framework/cluster/src/test/java/com/cloud/cluster/ClusterManagerImplTest.java
new file mode 100644
index 0000000..9b1854f
--- /dev/null
+++ b/framework/cluster/src/test/java/com/cloud/cluster/ClusterManagerImplTest.java
@@ -0,0 +1,38 @@
+// 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 com.cloud.cluster;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.test.util.ReflectionTestUtils;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ClusterManagerImplTest {
+ @InjectMocks
+ ClusterManagerImpl clusterManager = new ClusterManagerImpl();
+
+ @Test
+ public void testGetSelfNodeIP() {
+ String ip = "1.2.3.4";
+ ReflectionTestUtils.setField(clusterManager, "_clusterNodeIP", ip);
+ Assert.assertEquals(ip, clusterManager.getSelfNodeIP());
+ }
+}
diff --git a/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletAdapterTest.java b/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletAdapterTest.java
index 91d8b61..3827236 100644
--- a/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletAdapterTest.java
+++ b/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletAdapterTest.java
@@ -50,7 +50,7 @@
@Test
public void testRunLevel() {
int runLevel = clusterServiceServletAdapter.getRunLevel();
- assertTrue(runLevel == ComponentLifecycle.RUN_LEVEL_FRAMEWORK);
+ assertTrue(runLevel == ComponentLifecycle.RUN_LEVEL_COMPONENT);
assertTrue(runLevel == clusterManagerImpl.getRunLevel());
}
}
diff --git a/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletContainerTest.java b/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletContainerTest.java
new file mode 100644
index 0000000..baf4e58
--- /dev/null
+++ b/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletContainerTest.java
@@ -0,0 +1,87 @@
+// 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 com.cloud.cluster;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLContextSpi;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.cloud.utils.StringUtils;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ClusterServiceServletContainerTest {
+
+ private void runGetSecuredServerSocket(String ip) {
+ SSLContext sslContext = Mockito.mock(SSLContext.class);
+ SSLContextSpi sslContextSpi = Mockito.mock(SSLContextSpi.class);
+ ReflectionTestUtils.setField(sslContext, "contextSpi", sslContextSpi);
+ SSLServerSocketFactory factory = Mockito.mock(SSLServerSocketFactory.class);
+ Mockito.when(sslContext.getServerSocketFactory()).thenReturn(factory);
+ int port = 9090;
+ final List<Boolean> socketNeedClientAuth = new ArrayList<>();
+ try {
+ SSLServerSocket socketMock = Mockito.mock(SSLServerSocket.class);
+ if (StringUtils.isBlank(ip)) {
+ Mockito.when(factory.createServerSocket(port)).thenReturn(socketMock);
+ } else {
+ Mockito.when(factory.createServerSocket(Mockito.anyInt(), Mockito.anyInt(),
+ Mockito.any(InetAddress.class))).thenReturn(socketMock);
+ }
+ Mockito.doAnswer((Answer<Void>) invocationOnMock -> {
+ boolean needClientAuth = (boolean) invocationOnMock.getArguments()[0];
+ socketNeedClientAuth.add(needClientAuth);
+ return null;
+ }).when(socketMock).setNeedClientAuth(Mockito.anyBoolean());
+ SSLServerSocket socket = ClusterServiceServletContainer.getSecuredServerSocket(sslContext, ip, 9090);
+ if (StringUtils.isBlank(ip)) {
+ Mockito.verify(factory, Mockito.times(1)).createServerSocket(port);
+ } else {
+ Mockito.verify(factory, Mockito.times(1)).createServerSocket(port, 0, InetAddress.getByName(ip));
+ }
+ Mockito.verify(socket, Mockito.times(1)).setNeedClientAuth(Mockito.anyBoolean());
+ Assert.assertTrue(CollectionUtils.isNotEmpty(socketNeedClientAuth));
+ Assert.assertTrue(socketNeedClientAuth.get(0));
+ } catch (IOException e) {
+ Assert.fail("Exception occurred: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testGetSecuredServerSocketNoIp() {
+ runGetSecuredServerSocket("");
+ }
+
+ @Test
+ public void testGetSecuredServerSocketIp() {
+ runGetSecuredServerSocket("1.2.3.4");
+ }
+}
diff --git a/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletImplTest.java b/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletImplTest.java
new file mode 100644
index 0000000..361c77d
--- /dev/null
+++ b/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletImplTest.java
@@ -0,0 +1,64 @@
+// 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 com.cloud.cluster;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.http.NameValuePair;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ClusterServiceServletImplTest {
+
+ @InjectMocks
+ ClusterServiceServletImpl clusterServiceServlet = new ClusterServiceServletImpl();
+
+ @Test
+ public void testClusterServicePduPostParameters() {
+ List<NameValuePair> parameters =
+ clusterServiceServlet.getClusterServicePduPostParameters(Mockito.mock(ClusterServicePdu.class));
+ Assert.assertTrue(CollectionUtils.isNotEmpty(parameters));
+ Optional<NameValuePair> opt = parameters.stream().filter(x -> x.getName().equals("method")).findFirst();
+ Assert.assertTrue(opt.isPresent());
+ NameValuePair val = opt.get();
+ Assert.assertEquals(Integer.toString(RemoteMethodConstants.METHOD_DELIVER_PDU), val.getValue());
+ }
+
+ @Test
+ public void testPingPostParameters() {
+ String peer = "1.2.3.4";
+ List<NameValuePair> parameters =
+ clusterServiceServlet.getPingPostParameters(peer);
+ Assert.assertTrue(CollectionUtils.isNotEmpty(parameters));
+ Optional<NameValuePair> opt = parameters.stream().filter(x -> x.getName().equals("method")).findFirst();
+ Assert.assertTrue(opt.isPresent());
+ NameValuePair val = opt.get();
+ Assert.assertEquals(Integer.toString(RemoteMethodConstants.METHOD_PING), val.getValue());
+ opt = parameters.stream().filter(x -> x.getName().equals("callingPeer")).findFirst();
+ Assert.assertTrue(opt.isPresent());
+ val = opt.get();
+ Assert.assertEquals(peer, val.getValue());
+ }
+}
diff --git a/packaging/systemd/cloudstack-management.default b/packaging/systemd/cloudstack-management.default
index 252fb4b..d0b41b4 100644
--- a/packaging/systemd/cloudstack-management.default
+++ b/packaging/systemd/cloudstack-management.default
@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-JAVA_OPTS="-Djava.security.properties=/etc/cloudstack/management/java.security.ciphers -Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2G -XX:+UseParallelGC -XX:MaxGCPauseMillis=500 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:ErrorFile=/var/log/cloudstack/management/cloudstack-management.err "
+JAVA_OPTS="-Djava.security.properties=/etc/cloudstack/management/java.security.ciphers -Djava.awt.headless=true -Xmx2G -XX:+UseParallelGC -XX:MaxGCPauseMillis=500 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:ErrorFile=/var/log/cloudstack/management/cloudstack-management.err "
CLASSPATH="/usr/share/cloudstack-management/lib/*:/etc/cloudstack/management:/usr/share/cloudstack-common:/usr/share/cloudstack-management/setup:/usr/share/cloudstack-management:/usr/share/java/mysql-connector-java.jar:/usr/share/cloudstack-mysql-ha/lib/*"
diff --git a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java
index f71274b..8217041 100644
--- a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java
+++ b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java
@@ -33,9 +33,11 @@
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
+import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -57,6 +59,7 @@
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.utils.security.CertUtils;
import org.apache.cloudstack.utils.security.KeyStoreUtils;
+import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.pkcs.Attribute;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
@@ -127,6 +130,8 @@
"true",
"When set to true, it will allow expired client certificate during SSL handshake.", true);
+ private static String managementCertificateCustomSAN;
+
///////////////////////////////////////////////////////////
/////////////// Root CA Private Methods ///////////////////
@@ -365,8 +370,11 @@
if (managementKeyStore != null) {
return true;
}
- final Certificate serverCertificate = issueCertificate(Collections.singletonList(NetUtils.getHostName()),
- NetUtils.getAllDefaultNicIps(), getCaValidityDays());
+ List<String> domainNames = new ArrayList<>();
+ domainNames.add(NetUtils.getHostName());
+ domainNames.add(CAManager.CertManagementCustomSubjectAlternativeName.value());
+ final Certificate serverCertificate = issueCertificate(
+ domainNames, NetUtils.getAllDefaultNicIps(), getCaValidityDays());
if (serverCertificate == null || serverCertificate.getPrivateKey() == null) {
throw new CloudRuntimeException("Failed to generate management server certificate and load management server keystore");
}
@@ -402,6 +410,7 @@
@Override
public boolean start() {
+ managementCertificateCustomSAN = CAManager.CertManagementCustomSubjectAlternativeName.value();
return loadRootCAKeyPair() && loadRootCAKeyPair() && loadManagementKeyStore();
}
@@ -456,4 +465,26 @@
public String getDescription() {
return "CloudStack's Root CA provider plugin";
}
+
+ @Override
+ public boolean isManagementCertificate(java.security.cert.Certificate certificate) throws CertificateParsingException {
+ if (!(certificate instanceof X509Certificate)) {
+ return false;
+ }
+ X509Certificate x509Certificate = (X509Certificate) certificate;
+
+ // Check for alternative names
+ Collection<List<?>> altNames = x509Certificate.getSubjectAlternativeNames();
+ if (CollectionUtils.isEmpty(altNames)) {
+ return false;
+ }
+ for (List<?> altName : altNames) {
+ int type = (Integer) altName.get(0);
+ String name = (String) altName.get(1);
+ if (type == GeneralName.dNSName && managementCertificateCustomSAN.equals(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java
index 15514b9..8311f4d 100644
--- a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java
+++ b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java
@@ -26,8 +26,13 @@
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
+import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
import javax.net.ssl.SSLEngine;
@@ -35,15 +40,16 @@
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.utils.security.CertUtils;
import org.apache.cloudstack.utils.security.SSLUtils;
+import org.bouncycastle.asn1.x509.GeneralName;
import org.joda.time.DateTime;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-
-import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.test.util.ReflectionTestUtils;
@RunWith(MockitoJUnitRunner.class)
@@ -150,4 +156,56 @@
Assert.assertEquals(provider.getProviderName(), "root");
}
+ @Test
+ public void testIsManagementCertificateNotX509() {
+ try {
+ Assert.assertFalse(provider.isManagementCertificate(Mockito.mock(java.security.cert.Certificate.class)));
+ } catch (CertificateParsingException e) {
+ Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
+ }
+ }
+
+ @Test
+ public void testIsManagementCertificateNoAltNames() {
+ try {
+ X509Certificate certificate = Mockito.mock(X509Certificate.class);
+ Mockito.when(certificate.getSubjectAlternativeNames()).thenReturn(new ArrayList<>());
+ Assert.assertFalse(provider.isManagementCertificate(certificate));
+ } catch (CertificateParsingException e) {
+ Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
+ }
+ }
+
+ @Test
+ public void testIsManagementCertificateNoMatch() {
+ ReflectionTestUtils.setField(provider, "managementCertificateCustomSAN", "cloudstack");
+ try {
+ X509Certificate certificate = Mockito.mock(X509Certificate.class);
+ List<List<?>> altNames = new ArrayList<>();
+ altNames.add(List.of(GeneralName.dNSName, UUID.randomUUID().toString()));
+ altNames.add(List.of(GeneralName.dNSName, UUID.randomUUID().toString()));
+ Collection<List<?>> collection = new ArrayList<>(altNames);
+ Mockito.when(certificate.getSubjectAlternativeNames()).thenReturn(collection);
+ Assert.assertFalse(provider.isManagementCertificate(certificate));
+ } catch (CertificateParsingException e) {
+ Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
+ }
+ }
+
+ @Test
+ public void testIsManagementCertificateMatch() {
+ String customSAN = "cloudstack";
+ ReflectionTestUtils.setField(provider, "managementCertificateCustomSAN", customSAN);
+ try {
+ X509Certificate certificate = Mockito.mock(X509Certificate.class);
+ List<List<?>> altNames = new ArrayList<>();
+ altNames.add(List.of(GeneralName.dNSName, customSAN));
+ altNames.add(List.of(GeneralName.dNSName, UUID.randomUUID().toString()));
+ Collection<List<?>> collection = new ArrayList<>(altNames);
+ Mockito.when(certificate.getSubjectAlternativeNames()).thenReturn(collection);
+ Assert.assertTrue(provider.isManagementCertificate(certificate));
+ } catch (CertificateParsingException e) {
+ Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
+ }
+ }
}
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVMSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVMSnapshotCommandWrapper.java
index 5b55db2..f95948d 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVMSnapshotCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVMSnapshotCommandWrapper.java
@@ -19,6 +19,9 @@
package com.cloud.hypervisor.kvm.resource.wrapper;
+import java.util.ArrayList;
+import java.util.List;
+
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
import org.apache.log4j.Logger;
@@ -36,8 +39,8 @@
import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
-import com.cloud.storage.Volume;
import com.cloud.storage.Storage.ImageFormat;
+import com.cloud.storage.Volume;
import com.cloud.utils.script.Script;
@ResourceWrapper(handles = DeleteVMSnapshotCommand.class)
@@ -96,12 +99,20 @@
PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO) rootVolume.getDataStore();
KVMPhysicalDisk rootDisk = storagePoolMgr.getPhysicalDisk(primaryStore.getPoolType(),
primaryStore.getUuid(), rootVolume.getPath());
- String qemu_img_snapshot = Script.runSimpleBashScript("qemu-img snapshot -l " + rootDisk.getPath() + " | tail -n +3 | awk -F ' ' '{print $2}' | grep ^" + cmd.getTarget().getSnapshotName() + "$");
+ String qemuImgPath = Script.getExecutableAbsolutePath("qemu-img");
+ List<String[]> commands = new ArrayList<>();
+ commands.add(new String[]{qemuImgPath, "snapshot", "-l", sanitizeBashCommandArgument(rootDisk.getPath())});
+ commands.add(new String[]{Script.getExecutableAbsolutePath("tail"), "-n", "+3"});
+ commands.add(new String[]{Script.getExecutableAbsolutePath("awk"), "-F", " ", "{print $2}"});
+ commands.add(new String[]{Script.getExecutableAbsolutePath("grep"), "^" + sanitizeBashCommandArgument(cmd.getTarget().getSnapshotName()) + "$"});
+ String qemu_img_snapshot = Script.executePipedCommands(commands, 0).second();
if (qemu_img_snapshot == null) {
s_logger.info("Cannot find snapshot " + cmd.getTarget().getSnapshotName() + " in file " + rootDisk.getPath() + ", return true");
return new DeleteVMSnapshotAnswer(cmd, cmd.getVolumeTOs());
}
- int result = Script.runSimpleBashScriptForExitValue("qemu-img snapshot -d " + cmd.getTarget().getSnapshotName() + " " + rootDisk.getPath());
+ int result = Script.executeCommandForExitValue(qemuImgPath, "snapshot", "-d",
+ sanitizeBashCommandArgument(cmd.getTarget().getSnapshotName()),
+ sanitizeBashCommandArgument(rootDisk.getPath()));
if (result != 0) {
return new DeleteVMSnapshotAnswer(cmd, false,
"Delete VM Snapshot Failed due to can not remove snapshot from image file " + rootDisk.getPath() + " : " + result);
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java
index 1c27bdd..da2839d 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java
@@ -19,6 +19,11 @@
package com.cloud.hypervisor.kvm.resource.wrapper;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.GetVmIpAddressCommand;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
@@ -26,7 +31,6 @@
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.net.NetUtils;
import com.cloud.utils.script.Script;
-import org.apache.log4j.Logger;
@ResourceWrapper(handles = GetVmIpAddressCommand.class)
public final class LibvirtGetVmIpAddressCommandWrapper extends CommandWrapper<GetVmIpAddressCommand, Answer, LibvirtComputingResource> {
@@ -37,31 +41,51 @@
public Answer execute(final GetVmIpAddressCommand command, final LibvirtComputingResource libvirtComputingResource) {
String ip = null;
boolean result = false;
+ String vmName = command.getVmName();
+ String sanitizedVmName = sanitizeBashCommandArgument(vmName);
String networkCidr = command.getVmNetworkCidr();
+ List<String[]> commands = new ArrayList<>();
+ final String virt_ls_path = Script.getExecutableAbsolutePath("virt-ls");
+ final String virt_cat_path = Script.getExecutableAbsolutePath("virt-cat");
+ final String virt_win_reg_path = Script.getExecutableAbsolutePath("virt-win-reg");
+ final String tail_path = Script.getExecutableAbsolutePath("tail");
+ final String grep_path = Script.getExecutableAbsolutePath("grep");
+ final String awk_path = Script.getExecutableAbsolutePath("awk");
+ final String sed_path = Script.getExecutableAbsolutePath("sed");
if(!command.isWindows()) {
//List all dhcp lease files inside guestVm
- String leasesList = Script.runSimpleBashScript(new StringBuilder().append("virt-ls ").append(command.getVmName())
- .append(" /var/lib/dhclient/ | grep .*\\*.leases").toString());
+ commands.add(new String[]{virt_ls_path, sanitizedVmName, "/var/lib/dhclient/"});
+ commands.add(new String[]{grep_path, ".*\\*.leases"});
+ String leasesList = Script.executePipedCommands(commands, 0).second();
if(leasesList != null) {
String[] leasesFiles = leasesList.split("\n");
for(String leaseFile : leasesFiles){
- //Read from each dhclient lease file inside guest Vm using virt-cat libguestfs ulitiy
- String ipAddr = Script.runSimpleBashScript(new StringBuilder().append("virt-cat ").append(command.getVmName())
- .append(" /var/lib/dhclient/" + leaseFile + " | tail -16 | grep 'fixed-address' | awk '{print $2}' | sed -e 's/;//'").toString());
+ //Read from each dhclient lease file inside guest Vm using virt-cat libguestfs utility
+ commands = new ArrayList<>();
+ commands.add(new String[]{virt_cat_path, sanitizedVmName, "/var/lib/dhclient/" + leaseFile});
+ commands.add(new String[]{tail_path, "-16"});
+ commands.add(new String[]{grep_path, "fixed-address"});
+ commands.add(new String[]{awk_path, "{print $2}"});
+ commands.add(new String[]{sed_path, "-e", "s/;//"});
+ String ipAddr = Script.executePipedCommands(commands, 0).second();
// Check if the IP belongs to the network
- if((ipAddr != null) && NetUtils.isIpWithInCidrRange(ipAddr, networkCidr)){
+ if((ipAddr != null) && NetUtils.isIpWithInCidrRange(ipAddr, networkCidr)) {
ip = ipAddr;
break;
}
- s_logger.debug("GetVmIp: "+command.getVmName()+ " Ip: "+ipAddr+" does not belong to network "+networkCidr);
+ s_logger.debug("GetVmIp: "+ vmName + " Ip: "+ipAddr+" does not belong to network "+networkCidr);
}
}
} else {
// For windows, read from guest Vm registry using virt-win-reg libguestfs ulitiy. Registry Path: HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\<service>\DhcpIPAddress
- String ipList = Script.runSimpleBashScript(new StringBuilder().append("virt-win-reg --unsafe-printable-strings ").append(command.getVmName())
- .append(" 'HKEY_LOCAL_MACHINE\\SYSTEM\\ControlSet001\\Services\\Tcpip\\Parameters\\Interfaces' | grep DhcpIPAddress | awk -F : '{print $2}' | sed -e 's/^\"//' -e 's/\"$//'").toString());
+ commands = new ArrayList<>();
+ commands.add(new String[]{virt_win_reg_path, "--unsafe-printable-strings", sanitizedVmName, "HKEY_LOCAL_MACHINE\\SYSTEM\\ControlSet001\\Services\\Tcpip\\Parameters\\Interfaces"});
+ commands.add(new String[]{grep_path, "DhcpIPAddress"});
+ commands.add(new String[]{awk_path, "-F", ":", "{print $2}"});
+ commands.add(new String[]{sed_path, "-e", "s/^\"//", "-e", "s/\"$//"});
+ String ipList = Script.executePipedCommands(commands, 0).second();
if(ipList != null) {
- s_logger.debug("GetVmIp: "+command.getVmName()+ "Ips: "+ipList);
+ s_logger.debug("GetVmIp: "+ vmName + "Ips: "+ipList);
String[] ips = ipList.split("\n");
for (String ipAddr : ips){
// Check if the IP belongs to the network
@@ -69,13 +93,13 @@
ip = ipAddr;
break;
}
- s_logger.debug("GetVmIp: "+command.getVmName()+ " Ip: "+ipAddr+" does not belong to network "+networkCidr);
+ s_logger.debug("GetVmIp: "+ vmName + " Ip: "+ipAddr+" does not belong to network "+networkCidr);
}
}
}
if(ip != null){
result = true;
- s_logger.debug("GetVmIp: "+command.getVmName()+ " Found Ip: "+ip);
+ s_logger.debug("GetVmIp: "+ vmName + " Found Ip: "+ip);
}
return new Answer(command, result, ip);
}
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtOvsFetchInterfaceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtOvsFetchInterfaceCommandWrapper.java
index 5c79de5..117a832 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtOvsFetchInterfaceCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtOvsFetchInterfaceCommandWrapper.java
@@ -19,7 +19,12 @@
package com.cloud.hypervisor.kvm.resource.wrapper;
-import org.apache.commons.lang3.StringUtils;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+
import org.apache.log4j.Logger;
import com.cloud.agent.api.Answer;
@@ -28,33 +33,73 @@
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
-import com.cloud.utils.script.Script;
+import com.cloud.utils.Ternary;
@ResourceWrapper(handles = OvsFetchInterfaceCommand.class)
public final class LibvirtOvsFetchInterfaceCommandWrapper extends CommandWrapper<OvsFetchInterfaceCommand, Answer, LibvirtComputingResource> {
private static final Logger s_logger = Logger.getLogger(LibvirtOvsFetchInterfaceCommandWrapper.class);
+ private String getSubnetMaskForAddress(NetworkInterface networkInterface, InetAddress inetAddress) {
+ for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) {
+ if (!inetAddress.equals(address.getAddress())) {
+ continue;
+ }
+ int prefixLength = address.getNetworkPrefixLength();
+ int mask = 0xffffffff << (32 - prefixLength);
+ return String.format("%d.%d.%d.%d",
+ (mask >>> 24) & 0xff,
+ (mask >>> 16) & 0xff,
+ (mask >>> 8) & 0xff,
+ mask & 0xff);
+ }
+ return "";
+ }
+
+ private String getMacAddress(NetworkInterface networkInterface) throws SocketException {
+ byte[] macBytes = networkInterface.getHardwareAddress();
+ if (macBytes == null) {
+ return "";
+ }
+ StringBuilder macAddress = new StringBuilder();
+ for (byte b : macBytes) {
+ macAddress.append(String.format("%02X:", b));
+ }
+ if (macAddress.length() > 0) {
+ macAddress.deleteCharAt(macAddress.length() - 1); // Remove trailing colon
+ }
+ return macAddress.toString();
+ }
+
+ public Ternary<String, String, String> getInterfaceDetails(String interfaceName) throws SocketException {
+ NetworkInterface networkInterface = NetworkInterface.getByName(interfaceName);
+ if (networkInterface == null) {
+ logger.warn(String.format("Network interface: '%s' not found", interfaceName));
+ return new Ternary<>(null, null, null);
+ }
+ Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
+ while (inetAddresses.hasMoreElements()) {
+ InetAddress inetAddress = inetAddresses.nextElement();
+ if (inetAddress instanceof java.net.Inet4Address) {
+ String ipAddress = inetAddress.getHostAddress();
+ String subnetMask = getSubnetMaskForAddress(networkInterface, inetAddress);
+ String macAddress = getMacAddress(networkInterface);
+ return new Ternary<>(ipAddress, subnetMask, macAddress);
+ }
+ }
+ return new Ternary<>(null, null, null);
+ }
+
@Override
public Answer execute(final OvsFetchInterfaceCommand command, final LibvirtComputingResource libvirtComputingResource) {
- final String label = command.getLabel();
+ final String label = "'" + command.getLabel() + "'";
s_logger.debug("Will look for network with name-label:" + label);
try {
- String ipadd = Script.runSimpleBashScript("ifconfig " + label + " | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'");
- if (StringUtils.isEmpty(ipadd)) {
- ipadd = Script.runSimpleBashScript("ifconfig " + label + " | grep ' inet ' | awk '{ print $2}'");
- }
- String mask = Script.runSimpleBashScript("ifconfig " + label + " | grep 'inet addr:' | cut -d: -f4");
- if (StringUtils.isEmpty(mask)) {
- mask = Script.runSimpleBashScript("ifconfig " + label + " | grep ' inet ' | awk '{ print $4}'");
- }
- String mac = Script.runSimpleBashScript("ifconfig " + label + " | grep HWaddr | awk -F \" \" '{print $5}'");
- if (StringUtils.isEmpty(mac)) {
- mac = Script.runSimpleBashScript("ifconfig " + label + " | grep ' ether ' | awk '{ print $2}'");
- }
+ Ternary<String, String, String> interfaceDetails = getInterfaceDetails(label);
return new OvsFetchInterfaceAnswer(command, true, "Interface " + label
- + " retrieved successfully", ipadd, mask, mac);
+ + " retrieved successfully", interfaceDetails.first(), interfaceDetails.second(),
+ interfaceDetails.third());
} catch (final Exception e) {
s_logger.warn("Caught execption when fetching interface", e);
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java
index 3f281e5..769482f 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java
@@ -47,7 +47,6 @@
import com.cloud.resource.ResourceWrapper;
import com.cloud.storage.Volume;
import com.cloud.utils.exception.CloudRuntimeException;
-import com.cloud.utils.script.Script;
@ResourceWrapper(handles = PrepareForMigrationCommand.class)
public final class LibvirtPrepareForMigrationCommandWrapper extends CommandWrapper<PrepareForMigrationCommand, Answer, LibvirtComputingResource> {
@@ -126,9 +125,7 @@
} catch (final LibvirtException | CloudRuntimeException | InternalErrorException | URISyntaxException e) {
if (MapUtils.isNotEmpty(dpdkInterfaceMapping)) {
for (DpdkTO to : dpdkInterfaceMapping.values()) {
- String cmd = String.format("ovs-vsctl del-port %s", to.getPort());
- s_logger.debug("Removing DPDK port: " + to.getPort());
- Script.runSimpleBashScript(cmd);
+ removeDpdkPort(to.getPort());
}
}
return new PrepareForMigrationAnswer(command, e.toString());
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java
index fc57cd4..0803fc2 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java
@@ -19,9 +19,13 @@
package com.cloud.hypervisor.kvm.resource.wrapper;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import org.apache.log4j.Logger;
+
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.ReadyAnswer;
import com.cloud.agent.api.ReadyCommand;
@@ -31,8 +35,6 @@
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.script.Script;
-import org.apache.log4j.Logger;
-
@ResourceWrapper(handles = ReadyCommand.class)
public final class LibvirtReadyCommandWrapper extends CommandWrapper<ReadyCommand, Answer, LibvirtComputingResource> {
@@ -50,12 +52,17 @@
}
private boolean hostSupportsUefi(boolean isUbuntuHost) {
- String cmd = "rpm -qa | grep -i ovmf";
+ int result;
if (isUbuntuHost) {
- cmd = "dpkg -l ovmf";
+ s_logger.debug("Running command : dpkg -l ovmf");
+ result = Script.executeCommandForExitValue(Script.getExecutableAbsolutePath("dpkg"), "-l", "ovmf");
+ } else {
+ s_logger.debug("Running command : rpm -qa | grep -i ovmf");
+ List<String[]> commands = new ArrayList<>();
+ commands.add(new String[]{Script.getExecutableAbsolutePath("rpm"), "-qa"});
+ commands.add(new String[]{Script.getExecutableAbsolutePath("grep"), "-i", "ovmf"});
+ result = Script.executePipedCommands(commands, 0).first();
}
- s_logger.debug("Running command : " + cmd);
- int result = Script.runSimpleBashScriptForExitValue(cmd);
s_logger.debug("Got result : " + result);
return result == 0;
}
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevokeDirectDownloadCertificateWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevokeDirectDownloadCertificateWrapper.java
index 6c83c4d..f230d3f 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevokeDirectDownloadCertificateWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevokeDirectDownloadCertificateWrapper.java
@@ -19,6 +19,15 @@
package com.cloud.hypervisor.kvm.resource.wrapper;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import org.apache.cloudstack.agent.directdownload.RevokeDirectDownloadCertificateCommand;
+import org.apache.cloudstack.utils.security.KeyStoreUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.log4j.Logger;
+
import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
@@ -26,14 +35,6 @@
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;
-import org.apache.cloudstack.agent.directdownload.RevokeDirectDownloadCertificateCommand;
-import org.apache.cloudstack.utils.security.KeyStoreUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.log4j.Logger;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
@ResourceWrapper(handles = RevokeDirectDownloadCertificateCommand.class)
public class LibvirtRevokeDirectDownloadCertificateWrapper extends CommandWrapper<RevokeDirectDownloadCertificateCommand, Answer, LibvirtComputingResource> {
@@ -84,17 +85,17 @@
}
final String keyStoreFile = getKeyStoreFilePath(agentFile);
-
- String checkCmd = String.format("keytool -list -alias %s -keystore %s -storepass %s",
- certificateAlias, keyStoreFile, privatePassword);
- int existsCmdResult = Script.runSimpleBashScriptForExitValue(checkCmd);
+ String keyToolPath = Script.getExecutableAbsolutePath("keytool");
+ int existsCmdResult = Script.executeCommandForExitValue(keyToolPath, "-list", "-alias",
+ sanitizeBashCommandArgument(certificateAlias), "-keystore", keyStoreFile, "-storepass",
+ privatePassword);
if (existsCmdResult == 1) {
s_logger.error("Certificate alias " + certificateAlias + " does not exist, no need to revoke it");
} else {
- String revokeCmd = String.format("keytool -delete -alias %s -keystore %s -storepass %s",
- certificateAlias, keyStoreFile, privatePassword);
s_logger.debug("Revoking certificate alias " + certificateAlias + " from keystore " + keyStoreFile);
- Script.runSimpleBashScriptForExitValue(revokeCmd);
+ Script.executeCommandForExitValue(keyToolPath, "-delete", "-alias",
+ sanitizeBashCommandArgument(certificateAlias), "-keystore", keyStoreFile, "-storepass",
+ privatePassword);;
}
} catch (FileNotFoundException | CloudRuntimeException e) {
s_logger.error("Error while setting up certificate " + certificateAlias, e);
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java
index fff8da7..0774d30 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapper.java
@@ -18,21 +18,26 @@
//
package com.cloud.hypervisor.kvm.resource.wrapper;
-import com.cloud.agent.api.Answer;
-import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
-import com.cloud.resource.CommandWrapper;
-import com.cloud.resource.ResourceWrapper;
-import com.cloud.utils.PropertiesUtil;
-import com.cloud.utils.exception.CloudRuntimeException;
-import com.cloud.utils.script.Script;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
import org.apache.cloudstack.agent.directdownload.SetupDirectDownloadCertificateCommand;
import org.apache.cloudstack.utils.security.KeyStoreUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
+import com.cloud.agent.api.Answer;
+import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import com.cloud.resource.CommandWrapper;
+import com.cloud.resource.ResourceWrapper;
+import com.cloud.utils.FileUtil;
+import com.cloud.utils.PropertiesUtil;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.utils.script.Script;
@ResourceWrapper(handles = SetupDirectDownloadCertificateCommand.class)
public class LibvirtSetupDirectDownloadCertificateCommandWrapper extends CommandWrapper<SetupDirectDownloadCertificateCommand, Answer, LibvirtComputingResource> {
@@ -79,9 +84,10 @@
*/
private void importCertificate(String tempCerFilePath, String keyStoreFile, String certificateName, String privatePassword) {
s_logger.debug("Importing certificate from temporary file to keystore");
- String importCommandFormat = "keytool -importcert -file %s -keystore %s -alias '%s' -storepass '%s' -noprompt";
- String importCmd = String.format(importCommandFormat, tempCerFilePath, keyStoreFile, certificateName, privatePassword);
- int result = Script.runSimpleBashScriptForExitValue(importCmd);
+ String keyToolPath = Script.getExecutableAbsolutePath("keytool");
+ int result = Script.executeCommandForExitValue(keyToolPath, "-importcert", "file", tempCerFilePath,
+ "-keystore", keyStoreFile, "-alias", sanitizeBashCommandArgument(certificateName), "-storepass",
+ privatePassword, "-noprompt");
if (result != 0) {
s_logger.debug("Certificate " + certificateName + " not imported as it already exist on keystore");
}
@@ -94,8 +100,7 @@
String tempCerFilePath = String.format("%s/%s-%s",
agentFile.getParent(), temporaryCertFilePrefix, certificateName);
s_logger.debug("Creating temporary certificate file into: " + tempCerFilePath);
- int result = Script.runSimpleBashScriptForExitValue(String.format("echo '%s' > %s", certificate, tempCerFilePath));
- if (result != 0) {
+ if (!FileUtil.writeToFile(tempCerFilePath, certificate)) {
throw new CloudRuntimeException("Could not create the certificate file on path: " + tempCerFilePath);
}
return tempCerFilePath;
@@ -104,9 +109,23 @@
/**
* Remove temporary file
*/
- private void cleanupTemporaryFile(String temporaryFile) {
+ protected void cleanupTemporaryFile(String temporaryFile) {
s_logger.debug("Cleaning up temporary certificate file");
- Script.runSimpleBashScript("rm -f " + temporaryFile);
+ if (StringUtils.isBlank(temporaryFile)) {
+ s_logger.debug("Provided temporary certificate file path is empty");
+ return;
+ }
+ try {
+ Path filePath = Paths.get(temporaryFile);
+ if (!Files.exists(filePath)) {
+ s_logger.debug("Temporary certificate file does not exist: " + temporaryFile);
+ return;
+ }
+ Files.delete(filePath);
+ } catch (IOException e) {
+ s_logger.warn(String.format("Error while cleaning up temporary file: %s", temporaryFile));
+ s_logger.debug(String.format("Error while cleaning up temporary file: %s", temporaryFile), e);
+ }
}
@Override
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopCommandWrapper.java
index 7ee6ccd..518ee2b 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopCommandWrapper.java
@@ -23,28 +23,27 @@
import java.util.List;
import java.util.Map;
-import com.cloud.agent.api.to.DpdkTO;
-import com.cloud.hypervisor.kvm.resource.LibvirtKvmAgentHook;
-import com.cloud.utils.Pair;
-import com.cloud.utils.script.Script;
-import com.cloud.utils.ssh.SshHelper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.log4j.Logger;
import org.libvirt.Connect;
import org.libvirt.Domain;
import org.libvirt.DomainInfo.DomainState;
+import org.libvirt.LibvirtException;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.StopAnswer;
import com.cloud.agent.api.StopCommand;
+import com.cloud.agent.api.to.DpdkTO;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import com.cloud.hypervisor.kvm.resource.LibvirtKvmAgentHook;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef;
import com.cloud.hypervisor.kvm.resource.VifDriver;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
-import org.libvirt.LibvirtException;
+import com.cloud.utils.Pair;
+import com.cloud.utils.ssh.SshHelper;
@ResourceWrapper(handles = StopCommand.class)
public final class LibvirtStopCommandWrapper extends CommandWrapper<StopCommand, Answer, LibvirtComputingResource> {
@@ -121,10 +120,7 @@
Map<String, DpdkTO> dpdkInterfaceMapping = command.getDpdkInterfaceMapping();
if (MapUtils.isNotEmpty(dpdkInterfaceMapping)) {
for (DpdkTO to : dpdkInterfaceMapping.values()) {
- String portToRemove = to.getPort();
- String cmd = String.format("ovs-vsctl del-port %s", portToRemove);
- s_logger.debug("Removing DPDK port: " + portToRemove);
- Script.runSimpleBashScript(cmd);
+ removeDpdkPort(to.getPort());
}
}
} else {
diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtOvsFetchInterfaceCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtOvsFetchInterfaceCommandWrapperTest.java
new file mode 100644
index 0000000..fbc9c2b
--- /dev/null
+++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtOvsFetchInterfaceCommandWrapperTest.java
@@ -0,0 +1,105 @@
+// 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 com.cloud.hypervisor.kvm.resource.wrapper;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Spy;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import com.cloud.utils.StringUtils;
+import com.cloud.utils.Ternary;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(value = {LibvirtOvsFetchInterfaceCommandWrapper.class})
+public class LibvirtOvsFetchInterfaceCommandWrapperTest {
+
+ @Spy
+ LibvirtOvsFetchInterfaceCommandWrapper wrapper = new LibvirtOvsFetchInterfaceCommandWrapper();
+
+ @Test
+ public void testGetInterfaceDetailsValidValid() {
+ String interfaceName = null;
+ String ipAddress = null;
+ try {
+ Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+ while(interfaces.hasMoreElements()) {
+ NetworkInterface networkInterface = interfaces.nextElement();
+ if (networkInterface.getInetAddresses().hasMoreElements() &&
+ (networkInterface.getName().startsWith("eth") ||
+ networkInterface.getName().startsWith("wl"))) {
+ interfaceName = networkInterface.getName();
+ Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
+ while(addresses.hasMoreElements()) {
+ InetAddress addr = addresses.nextElement();
+ if (addr instanceof Inet4Address) {
+ ipAddress = addr.getHostAddress();
+ break;
+ };
+ }
+ }
+ }
+ } catch (SocketException ignored) {}
+ Ternary<String, String, String> result = null;
+ try {
+ result = wrapper.getInterfaceDetails(interfaceName);
+ } catch (SocketException e) {
+ Assert.fail("Exception occurred: " + e.getMessage());
+ }
+ Assert.assertNotNull(result);
+ Assert.assertEquals(ipAddress, result.first().trim());
+ }
+
+ private String getTempFilepath() {
+ return String.format("%s/%s.txt", System.getProperty("java.io.tmpdir"), UUID.randomUUID());
+ }
+
+ private void runTestGetInterfaceDetailsForRandomInterfaceName(String arg) {
+ try {
+ Ternary<String, String, String> result = wrapper.getInterfaceDetails(arg);
+ Assert.assertTrue(StringUtils.isAllEmpty(result.first(), result.second(), result.third()));
+ } catch (SocketException e) {
+ Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
+ }
+ }
+
+ @Test
+ public void testGetInterfaceDetailsForRandomInterfaceName() {
+ List<String> commandVariants = List.of(
+ "';touch %s'",
+ ";touch %s",
+ "&& touch %s",
+ "|| touch %s",
+ UUID.randomUUID().toString());
+ for (String cmd : commandVariants) {
+ String filePath = getTempFilepath();
+ String arg = String.format(cmd, filePath);
+ runTestGetInterfaceDetailsForRandomInterfaceName(arg);
+ }
+ }
+}
diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapperTest.java
index 5530819..e534f81 100644
--- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapperTest.java
+++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapperTest.java
@@ -1,4 +1,3 @@
-//
// 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
@@ -15,15 +14,14 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-//
package com.cloud.hypervisor.kvm.resource.wrapper;
-import com.cloud.agent.api.PrepareForMigrationAnswer;
-import com.cloud.agent.api.PrepareForMigrationCommand;
-import com.cloud.agent.api.to.DpdkTO;
-import com.cloud.agent.api.to.VirtualMachineTO;
-import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -33,8 +31,12 @@
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
-import java.util.HashMap;
-import java.util.Map;
+import com.cloud.agent.api.PrepareForMigrationAnswer;
+import com.cloud.agent.api.PrepareForMigrationCommand;
+import com.cloud.agent.api.to.DpdkTO;
+import com.cloud.agent.api.to.VirtualMachineTO;
+import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import com.cloud.utils.exception.CloudRuntimeException;
@RunWith(PowerMockRunner.class)
@PrepareForTest(value = {LibvirtPrepareForMigrationCommandWrapper.class})
@@ -72,4 +74,29 @@
Assert.assertEquals(cpuShares, prepareForMigrationAnswer.getNewVmCpuShares().intValue());
}
+
+ private String getTempFilepath() {
+ return String.format("%s/%s.txt", System.getProperty("java.io.tmpdir"), UUID.randomUUID());
+ }
+
+ private void runTestRemoveDpdkPortForCommandInjection(String portWithCommand) {
+ try {
+ libvirtPrepareForMigrationCommandWrapperSpy.removeDpdkPort(portWithCommand);
+ Assert.fail(String.format("Command injection working for portWithCommand: %s", portWithCommand));
+ } catch (CloudRuntimeException ignored) {}
+ }
+
+ @Test
+ public void testRemoveDpdkPortForCommandInjection() {
+ List<String> commandVariants = List.of(
+ "';touch %s'",
+ ";touch %s",
+ "&& touch %s",
+ "|| touch %s",
+ UUID.randomUUID().toString());
+ for (String cmd : commandVariants) {
+ String portWithCommand = String.format(cmd, getTempFilepath());
+ runTestRemoveDpdkPortForCommandInjection(portWithCommand);
+ }
+ }
}
diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapperTest.java
new file mode 100644
index 0000000..cd78733
--- /dev/null
+++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtSetupDirectDownloadCertificateCommandWrapperTest.java
@@ -0,0 +1,93 @@
+//
+// 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 com.cloud.hypervisor.kvm.resource.wrapper;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Spy;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(value = {LibvirtSetupDirectDownloadCertificateCommandWrapper.class})
+public class LibvirtSetupDirectDownloadCertificateCommandWrapperTest {
+
+ @Spy
+ LibvirtSetupDirectDownloadCertificateCommandWrapper wrapper = new LibvirtSetupDirectDownloadCertificateCommandWrapper();
+
+ private String getTempFilepath() {
+ return String.format("%s/%s.txt", System.getProperty("java.io.tmpdir"), UUID.randomUUID());
+ }
+
+ private void runTestCleanupTemporaryFileForRandomFileNames(String fileWithCommand, String filePath) {
+ wrapper.cleanupTemporaryFile(fileWithCommand);
+ File f = new File(filePath);
+ if(f.exists() && !f.isDirectory()) {
+ Assert.fail(String.format("Command injection working for fileWithCommand: %s", fileWithCommand));
+ }
+ }
+
+ @Test
+ public void testCleanupTemporaryFileForRandomFileNames() {
+ List<String> commandVariants = List.of(
+ "';touch %s'",
+ ";touch %s",
+ "&& touch %s",
+ "|| touch %s",
+ "%s");
+ for (String cmd : commandVariants) {
+ String filePath = getTempFilepath();
+ String arg = String.format(cmd, filePath);
+ runTestCleanupTemporaryFileForRandomFileNames(arg, filePath);
+ }
+ }
+
+ private String createTempFile() {
+ String filePath = getTempFilepath();
+ Path path = Paths.get(getTempFilepath());
+ try {
+ if (Files.notExists(path)) {
+ Files.createFile(path);
+ }
+ } catch (IOException e) {
+ Assert.fail(String.format("Error while creating file: %s due to %s", filePath, e.getMessage()));
+ }
+ return filePath;
+ }
+
+ @Test
+ public void testCleanupTemporaryFileValid() {
+ String filePath = createTempFile();
+ wrapper.cleanupTemporaryFile(filePath);
+ File f = new File(filePath);
+ if(f.exists() && !f.isDirectory()) {
+ Assert.fail(String.format("Command injection working for fileWithCommand: %s", filePath));
+ }
+ }
+}
diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopCommandWrapperTest.java
new file mode 100644
index 0000000..c701946
--- /dev/null
+++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopCommandWrapperTest.java
@@ -0,0 +1,63 @@
+// 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 com.cloud.hypervisor.kvm.resource.wrapper;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Spy;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import com.cloud.utils.exception.CloudRuntimeException;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(value = {LibvirtStopCommandWrapper.class})
+public class LibvirtStopCommandWrapperTest {
+
+ @Spy
+ LibvirtStopCommandWrapper wrapper = new LibvirtStopCommandWrapper();
+
+ private String getTempFilepath() {
+ return String.format("%s/%s.txt", System.getProperty("java.io.tmpdir"), UUID.randomUUID());
+ }
+
+ private void runTestRemoveDpdkPortForCommandInjection(String portWithCommand) {
+ try {
+ wrapper.removeDpdkPort(portWithCommand);
+ Assert.fail(String.format("Command injection working for portWithCommand: %s", portWithCommand));
+ } catch (CloudRuntimeException ignored) {}
+ }
+
+ @Test
+ public void testRemoveDpdkPortForCommandInjection() {
+ List<String> commandVariants = List.of(
+ "';touch %s'",
+ ";touch %s",
+ "&& touch %s",
+ "|| touch %s",
+ UUID.randomUUID().toString());
+ for (String cmd : commandVariants) {
+ String portWithCommand = String.format(cmd, getTempFilepath());
+ runTestRemoveDpdkPortForCommandInjection(portWithCommand);
+ }
+ }
+}
diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java
index 4a7259c..cf0e689 100644
--- a/server/src/main/java/com/cloud/api/ApiServer.java
+++ b/server/src/main/java/com/cloud/api/ApiServer.java
@@ -399,6 +399,17 @@
}
}
+ protected void setupIntegrationPortListener(Integer apiPort) {
+ if (apiPort == null || apiPort <= 0) {
+ s_logger.trace(String.format("Skipping setting up listener for integration port as %s is set to %d",
+ IntegrationAPIPort.key(), apiPort));
+ return;
+ }
+ s_logger.debug(String.format("Setting up integration API service listener on port: %d", apiPort));
+ final ListenerThread listenerThread = new ListenerThread(this, apiPort);
+ listenerThread.start();
+ }
+
@Override
public boolean start() {
Security.addProvider(new BouncyCastleProvider());
@@ -444,10 +455,7 @@
setEncodeApiResponse(EncodeApiResponse.value());
- if (apiPort != null) {
- final ListenerThread listenerThread = new ListenerThread(this, apiPort);
- listenerThread.start();
- }
+ setupIntegrationPortListener(apiPort);
return true;
}
diff --git a/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java b/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java
index facad1a..609c4d5 100644
--- a/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java
@@ -24,6 +24,7 @@
import java.security.KeyStoreException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
@@ -432,6 +433,14 @@
@Override
public ConfigKey<?>[] getConfigKeys() {
- return new ConfigKey<?>[] {CAProviderPlugin, CertKeySize, CertSignatureAlgorithm, CertValidityPeriod, AutomaticCertRenewal, AllowHostIPInSysVMAgentCert, CABackgroundJobDelay, CertExpiryAlertPeriod};
+ return new ConfigKey<?>[] {CAProviderPlugin, CertKeySize, CertSignatureAlgorithm, CertValidityPeriod,
+ AutomaticCertRenewal, AllowHostIPInSysVMAgentCert, CABackgroundJobDelay, CertExpiryAlertPeriod,
+ CertManagementCustomSubjectAlternativeName
+ };
+ }
+
+ @Override
+ public boolean isManagementCertificate(java.security.cert.Certificate certificate) throws CertificateParsingException {
+ return getConfiguredCaProvider().isManagementCertificate(certificate);
}
}
diff --git a/server/src/test/java/com/cloud/api/ApiServerTest.java b/server/src/test/java/com/cloud/api/ApiServerTest.java
new file mode 100644
index 0000000..2c7eebd
--- /dev/null
+++ b/server/src/test/java/com/cloud/api/ApiServerTest.java
@@ -0,0 +1,73 @@
+// 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 com.cloud.api;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(ApiServer.class)
+public class ApiServerTest {
+
+ @InjectMocks
+ ApiServer apiServer = new ApiServer();
+
+ private List<ApiServer.ListenerThread> createdListeners;
+
+ private void runTestSetupIntegrationPortListenerInvalidPorts(Integer port) {
+ try {
+ ApiServer.ListenerThread mocked = Mockito.mock(ApiServer.ListenerThread.class);
+ PowerMockito.whenNew(ApiServer.ListenerThread.class).withAnyArguments().thenReturn(mocked);
+ apiServer.setupIntegrationPortListener(port);
+ Mockito.verify(mocked, Mockito.never()).start();
+ } catch (Exception e) {
+ Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
+ }
+ }
+
+ @Test
+ public void testSetupIntegrationPortListenerInvalidPorts() {
+ List<Integer> ports = new ArrayList<>(List.of(-1, -10, 0));
+ ports.add(null);
+ for (Integer port : ports) {
+ runTestSetupIntegrationPortListenerInvalidPorts(port);
+ }
+ }
+
+ @Test
+ public void testSetupIntegrationPortListenerValidPort() {
+ Integer validPort = 8080;
+ try {
+ ApiServer.ListenerThread mocked = Mockito.mock(ApiServer.ListenerThread.class);
+ PowerMockito.whenNew(ApiServer.ListenerThread.class).withAnyArguments().thenReturn(mocked);
+ apiServer.setupIntegrationPortListener(validPort);
+ PowerMockito.verifyNew(ApiServer.ListenerThread.class).withArguments(apiServer, validPort);
+ Mockito.verify(mocked).start();
+ } catch (Exception e) {
+ Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
+ }
+ }
+}
diff --git a/utils/src/main/java/com/cloud/utils/FileUtil.java b/utils/src/main/java/com/cloud/utils/FileUtil.java
index d9bf081..fbb04e1 100644
--- a/utils/src/main/java/com/cloud/utils/FileUtil.java
+++ b/utils/src/main/java/com/cloud/utils/FileUtil.java
@@ -21,15 +21,20 @@
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
-import com.cloud.utils.exception.CloudRuntimeException;
-import com.cloud.utils.ssh.SshHelper;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.utils.ssh.SshHelper;
+
public class FileUtil {
private static final Logger s_logger = Logger.getLogger(FileUtil.class);
@@ -57,4 +62,16 @@
}
throw new CloudRuntimeException(finalErrMsg);
}
+
+ public static boolean writeToFile(String fileName, String content) {
+ Path filePath = Paths.get(fileName);
+ try {
+ Files.write(filePath, content.getBytes(StandardCharsets.UTF_8));
+ s_logger.debug(String.format("Successfully wrote to the file: %s", fileName));
+ return true;
+ } catch (IOException e) {
+ s_logger.error(String.format("Error writing to the file: %s", fileName), e);
+ }
+ return false;
+ }
}
diff --git a/utils/src/main/java/com/cloud/utils/script/Script.java b/utils/src/main/java/com/cloud/utils/script/Script.java
index 3158553..27ec65a 100644
--- a/utils/src/main/java/com/cloud/utils/script/Script.java
+++ b/utils/src/main/java/com/cloud/utils/script/Script.java
@@ -19,14 +19,6 @@
package com.cloud.utils.script;
-import com.cloud.utils.PropertiesUtil;
-import com.cloud.utils.concurrency.NamedThreadFactory;
-import com.cloud.utils.script.OutputInterpreter.TimedOutLogger;
-import org.apache.cloudstack.utils.security.KeyStoreUtils;
-import org.apache.commons.io.IOUtils;
-import org.apache.log4j.Logger;
-import org.joda.time.Duration;
-
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
@@ -40,10 +32,24 @@
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+import org.apache.cloudstack.utils.security.KeyStoreUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.log4j.Logger;
+import org.joda.time.Duration;
+
+import com.cloud.utils.Pair;
+import com.cloud.utils.PropertiesUtil;
+import com.cloud.utils.concurrency.NamedThreadFactory;
+import com.cloud.utils.script.OutputInterpreter.TimedOutLogger;
public class Script implements Callable<String> {
private static final Logger s_logger = Logger.getLogger(Script.class);
@@ -52,7 +58,7 @@
public static final String ERR_EXECUTE = "execute.error";
public static final String ERR_TIMEOUT = "timeout";
- private int _defaultTimeout = 3600 * 1000; /* 1 hour */
+ private static final int DEFAULT_TIMEOUT = 3600 * 1000; /* 1 hour */
private volatile boolean _isTimeOut = false;
private boolean _passwordCommand = false;
@@ -84,7 +90,7 @@
_timeout = timeout;
if (_timeout == 0) {
/* always using default timeout 1 hour to avoid thread hang */
- _timeout = _defaultTimeout;
+ _timeout = DEFAULT_TIMEOUT;
}
_process = null;
_logger = logger != null ? logger : s_logger;
@@ -493,16 +499,7 @@
return null;
}
- public static String runSimpleBashScript(String command) {
- return Script.runSimpleBashScript(command, 0);
- }
-
- public static String runSimpleBashScript(String command, int timeout) {
-
- Script s = new Script("/bin/bash", timeout);
- s.add("-c");
- s.add(command);
-
+ private static String runScript(Script s) {
OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser();
if (s.execute(parser) != null)
return null;
@@ -514,16 +511,83 @@
return result.trim();
}
- public static int runSimpleBashScriptForExitValue(String command) {
- return runSimpleBashScriptForExitValue(command, 0);
- }
-
- public static int runSimpleBashScriptForExitValue(String command, int timeout) {
-
+ public static String runSimpleBashScript(String command, int timeout) {
Script s = new Script("/bin/bash", timeout);
s.add("-c");
s.add(command);
+ return runScript(s);
+ }
+ public static String runSimpleBashScript(String command) {
+ return Script.runSimpleBashScript(command, 0);
+ }
+
+ public static String getExecutableAbsolutePath(String executable) {
+ for (String dirName : System.getenv("PATH").split(File.pathSeparator)) {
+ File file = new File(dirName, executable);
+ if (file.isFile() && file.canExecute()) {
+ return file.getAbsolutePath();
+ }
+ }
+ return executable;
+ }
+
+ private static Script getScriptForCommandRun(String... command) {
+ Script s = new Script(command[0], 0);
+ if (command.length > 1) {
+ for (int i = 1; i < command.length; ++i) {
+ s.add(command[i]);
+ }
+ }
+ return s;
+ }
+
+ public static String executeCommand(String... command) {
+ return runScript(getScriptForCommandRun(command));
+ }
+
+ public static int executeCommandForExitValue(String... command) {
+ return runScriptForExitValue(getScriptForCommandRun(command));
+ }
+
+ public static Pair<Integer, String> executePipedCommands(List<String[]> commands, long timeout) {
+ if (timeout <= 0) {
+ timeout = DEFAULT_TIMEOUT;
+ }
+ Callable<Pair<Integer, String>> commandRunner = () -> {
+ List<ProcessBuilder> builders = commands.stream().map(ProcessBuilder::new).collect(Collectors.toList());
+ List<Process> processes = ProcessBuilder.startPipeline(builders);
+ Process last = processes.get(processes.size()-1);
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(last.getInputStream()))) {
+ String line;
+ StringBuilder output = new StringBuilder();
+ while ((line = reader.readLine()) != null) {
+ output.append(line).append(System.lineSeparator());
+ }
+ last.waitFor();
+ s_logger.debug("Piped commands executed successfully");
+ return new Pair<>(last.exitValue(), output.toString());
+ } catch (IOException | InterruptedException e) {
+ s_logger.error("Error executing piped commands", e);
+ return new Pair<>(-1, stackTraceAsString(e));
+ }
+ };
+
+ Future<Pair<Integer, String>> future = s_executors.submit(commandRunner);
+ Pair<Integer, String> result = new Pair<>(-1, ERR_EXECUTE);
+ try {
+ result = future.get(timeout, TimeUnit.MILLISECONDS);
+ } catch (TimeoutException e) {
+ s_logger.error("Piped command execution timed out, attempting to terminate the processes.");
+ future.cancel(true);
+ result.second(ERR_TIMEOUT);
+ } catch (InterruptedException | ExecutionException e) {
+ s_logger.error("Error executing piped commands", e);
+ }
+ return result;
+ }
+
+ private static int runScriptForExitValue(Script s) {
String result = s.execute(null);
if (result == null || result.trim().isEmpty())
return -1;
@@ -536,4 +600,14 @@
}
}
+ public static int runSimpleBashScriptForExitValue(String command) {
+ return runSimpleBashScriptForExitValue(command, 0);
+ }
+
+ public static int runSimpleBashScriptForExitValue(String command, int timeout) {
+ Script s = new Script("/bin/bash", timeout);
+ s.add("-c");
+ s.add(command);
+ return runScriptForExitValue(s);
+ }
}
diff --git a/utils/src/test/java/com/cloud/utils/script/ScriptTest.java b/utils/src/test/java/com/cloud/utils/script/ScriptTest.java
new file mode 100644
index 0000000..cc60479
--- /dev/null
+++ b/utils/src/test/java/com/cloud/utils/script/ScriptTest.java
@@ -0,0 +1,81 @@
+// 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 com.cloud.utils.script;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import com.cloud.utils.Pair;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ScriptTest {
+
+ @Test
+ public void testExecutePipedCommandsSingle() {
+ String keyword = "Hello World!";
+ List<String[]> commands = new ArrayList<>();
+ commands.add(new String[]{"echo", keyword});
+ Pair<Integer, String> result = Script.executePipedCommands(commands, 0);
+ Assert.assertNotNull("Result should not be null", result);
+ Assert.assertEquals(0, result.first().intValue());
+ String output = result.second().trim();
+ Assert.assertTrue(StringUtils.isNotEmpty(output));
+ Assert.assertEquals(keyword, output);
+ }
+
+ @Test
+ public void testExecutePipedCommandsMultiple() {
+ String keyword = "Hello";
+ List<String[]> commands = Arrays.asList(
+ new String[]{"echo", String.format("%s\n World", keyword)},
+ new String[]{"grep", keyword}
+ );
+ Pair<Integer, String> result = Script.executePipedCommands(commands, 0);
+ Assert.assertNotNull("Result should not be null", result);
+ Assert.assertEquals(0, result.first().intValue());
+ String output = result.second().trim();
+ Assert.assertTrue(StringUtils.isNotEmpty(output));
+ Assert.assertEquals(keyword, output);
+ }
+
+ @Test
+ public void testExecutePipedCommandsTimeout() {
+ List<String[]> commands = new ArrayList<>();
+ commands.add(new String[]{"sh", "-c", "sleep 10"});
+ Pair<Integer, String> result = Script.executePipedCommands(commands, TimeUnit.SECONDS.toMillis(1));
+ Assert.assertNotNull("Result should not be null", result);
+ Assert.assertEquals(-1, result.first().intValue());
+ Assert.assertEquals(Script.ERR_TIMEOUT, result.second());
+ }
+
+ @Test
+ public void testGetExecutableAbsolutePath() {
+ if (System.getProperty("os.name").startsWith("Windows")) {
+ return;
+ }
+ String result = Script.getExecutableAbsolutePath("ls");
+ Assert.assertTrue(List.of("/usr/bin/ls", "/bin/ls").contains(result));
+ }
+}