This is an automated email from the ASF dual-hosted git repository.
shwstppr pushed a commit to branch 4.18
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.18 by this push:
new beebeed5e23 framework/cluster: improve cluster service and integration
API service
beebeed5e23 is described below
commit beebeed5e23ea7dd8391d914d92017cd49cee95a
Author: Abhishek Kumar <[email protected]>
AuthorDate: Tue Jul 2 17:52:52 2024 +0530
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 <[email protected]>
Co-authored-by: Wei Zhou <[email protected]>
Co-authored-by: Rohit Yadav <[email protected]>
Signed-off-by: Abhishek Kumar <[email protected]>
---
.../java/org/apache/cloudstack/ca/CAManager.java | 8 +
.../java/com/cloud/resource/CommandWrapper.java | 27 +++-
.../apache/cloudstack/framework/ca/CAProvider.java | 4 +
.../apache/cloudstack/framework/ca/CAService.java | 3 +
.../java/com/cloud/cluster/ClusterManager.java | 4 +-
.../java/com/cloud/cluster/ClusterManagerImpl.java | 7 +-
.../com/cloud/cluster/ClusterServiceAdapter.java | 2 -
.../cluster/ClusterServiceServletAdapter.java | 18 ++-
.../cluster/ClusterServiceServletContainer.java | 111 ++++++++++----
.../cloud/cluster/ClusterServiceServletImpl.java | 162 ++++++++++++++-------
.../com/cloud/cluster/ClusterManagerImplTest.java} | 30 ++--
.../cluster/ClusterServiceServletAdapterTest.java | 2 +-
.../ClusterServiceServletContainerTest.java | 87 +++++++++++
.../cluster/ClusterServiceServletImplTest.java | 64 ++++++++
packaging/systemd/cloudstack-management.default | 2 +-
.../cloudstack/ca/provider/RootCAProvider.java | 35 ++++-
.../cloudstack/ca/provider/RootCAProviderTest.java | 62 +++++++-
.../LibvirtDeleteVMSnapshotCommandWrapper.java | 17 ++-
.../LibvirtGetVmIpAddressCommandWrapper.java | 50 +++++--
.../LibvirtOvsFetchInterfaceCommandWrapper.java | 77 ++++++++--
.../LibvirtPrepareForMigrationCommandWrapper.java | 5 +-
.../wrapper/LibvirtReadyCommandWrapper.java | 19 ++-
...virtRevokeDirectDownloadCertificateWrapper.java | 31 ++--
...tupDirectDownloadCertificateCommandWrapper.java | 49 +++++--
.../wrapper/LibvirtStopCommandWrapper.java | 16 +-
...LibvirtOvsFetchInterfaceCommandWrapperTest.java | 105 +++++++++++++
...bvirtPrepareForMigrationCommandWrapperTest.java | 45 ++++--
...irectDownloadCertificateCommandWrapperTest.java | 93 ++++++++++++
.../wrapper/LibvirtStopCommandWrapperTest.java | 63 ++++++++
server/src/main/java/com/cloud/api/ApiServer.java | 16 +-
.../org/apache/cloudstack/ca/CAManagerImpl.java | 11 +-
.../src/test/java/com/cloud/api/ApiServerTest.java | 73 ++++++++++
utils/src/main/java/com/cloud/utils/FileUtil.java | 21 ++-
.../main/java/com/cloud/utils/script/Script.java | 126 ++++++++++++----
.../java/com/cloud/utils/script/ScriptTest.java | 81 +++++++++++
35 files changed, 1282 insertions(+), 244 deletions(-)
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 12a9d3d7b41..b0fb1ac73c2 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 @@ public interface CAManager extends CAService, Configurable,
PluggableService {
"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 d9c1ea234e8..ee6aa161e33 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 @@ public abstract class CommandWrapper<T extends Command, A
extends Answer, R exte
* @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 388cae7e007..77b3ee27783 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.math.BigInteger;
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 @@ public interface CAProvider {
/**
* Issues certificate with provided options
+ *
* @param domainNames
* @param ipAddresses
* @param validityDays
@@ -104,4 +106,6 @@ public interface CAProvider {
* @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 facf13a5cb6..721c88bee50 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.io.IOException;
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 @@ public interface CAService {
* @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 1b1406c1cec..54f575830e4 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 @@ public interface ClusterManager extends Manager {
*/
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 289638fe22d..d601c094ca7 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 java.util.concurrent.TimeUnit;
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 @@ public class ClusterManagerImpl extends ManagerBase
implements ClusterManager, C
// 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 @@ public class ClusterManagerImpl extends ManagerBase
implements ClusterManager, C
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 735de5bdac9..e073a28a622 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 interface ClusterServiceAdapter extends Adapter {
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 7451b5f4226..15ee055f9e1 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 java.util.Properties;
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 @@ public class ClusterServiceServletAdapter extends AdapterBase
implements Cluster
@Inject
private ManagementServerHostDao _mshostDao;
@Inject
+ private CAManager caService;
+ @Inject
protected ConfigDepot _configDepot;
private ClusterServiceServletContainer _servletContainer;
@@ -51,7 +54,7 @@ public class ClusterServiceServletAdapter extends AdapterBase
implements Cluster
private int _clusterServicePort = DEFAULT_SERVICE_PORT;
public ClusterServiceServletAdapter() {
- setRunLevel(ComponentLifecycle.RUN_LEVEL_FRAMEWORK);
+ setRunLevel(ComponentLifecycle.RUN_LEVEL_COMPONENT);
}
@Override
@@ -66,12 +69,10 @@ public class ClusterServiceServletAdapter extends
AdapterBase implements Cluster
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 @@ public class ClusterServiceServletAdapter extends AdapterBase
implements Cluster
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 @@ public class ClusterServiceServletAdapter extends
AdapterBase implements Cluster
@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 69cc871dc64..1aa9caae50a 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.ResponseDate;
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 class ClusterServiceServletContainer {
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 @@ public class ClusterServiceServletContainer {
}
}
+ 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;
+ }
+
static class ListenerThread extends Thread {
- private HttpService _httpService = null;
- private volatile ServerSocket _serverSocket = null;
- private HttpParams _params = null;
- private ExecutorService _executor;
+ private HttpService httpService = null;
+ private volatile SSLServerSocket serverSocket = null;
+ private HttpParams params = null;
+ private ExecutorService executor;
+ private CAService caService = null;
- public ListenerThread(HttpRequestHandler requestHandler, int port) {
- _executor = Executors.newCachedThreadPool(new
NamedThreadFactory("Cluster-Listener"));
+ 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 @@ public class ClusterServiceServletContainer {
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 @@ public class ClusterServiceServletContainer {
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 @@ public class ClusterServiceServletContainer {
}
}
- _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 ec8b90866d0..c5b61452169 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;
+
+ private CAService caService;
+
+ private Gson gson = new Gson();
- protected static HttpClient s_client = null;
+ 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 @@ public class ClusterServiceServletImpl implements
ClusterService {
return result;
}
- private HttpClient getHttpClient() {
-
+ private CloseableHttpClient getHttpClient() {
if (s_client == null) {
- final MultiThreadedHttpConnectionManager mgr = new
MultiThreadedHttpConnectionManager();
- mgr.getParams().setDefaultMaxConnectionsPerHost(4);
-
- // TODO make it configurable
- mgr.getParams().setMaxTotalConnections(1000);
+ SSLContext sslContext = null;
+ try {
+ sslContext = Link.initManagementSSLContext(caService);
+ } catch (GeneralSecurityException | IOException e) {
+ throw new RuntimeException(e);
+ }
- s_client = new HttpClient(mgr);
- final HttpClientParams clientParams = new HttpClientParams();
-
clientParams.setSoTimeout(ClusterServiceAdapter.ClusterMessageTimeOut.value() *
1000);
+ int timeout = ClusterServiceAdapter.ClusterMessageTimeOut.value()
* 1000;
+ RequestConfig config = RequestConfig.custom()
+ .setConnectTimeout(timeout)
+ .setConnectionRequestTimeout(timeout)
+ .setSocketTimeout(timeout).build();
- s_client.setParams(clientParams);
+ s_client = HttpClientBuilder.create()
+ .setDefaultRequestConfig(config)
+ .setSSLContext(sslContext)
+ .build();
}
return s_client;
}
diff --git
a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceAdapter.java
b/framework/cluster/src/test/java/com/cloud/cluster/ClusterManagerImplTest.java
similarity index 57%
copy from
framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceAdapter.java
copy to
framework/cluster/src/test/java/com/cloud/cluster/ClusterManagerImplTest.java
index 735de5bdac9..9b1854f7348 100644
---
a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceAdapter.java
+++
b/framework/cluster/src/test/java/com/cloud/cluster/ClusterManagerImplTest.java
@@ -14,21 +14,25 @@
// 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.rmi.RemoteException;
-
-import org.apache.cloudstack.framework.config.ConfigKey;
-import com.cloud.utils.component.Adapter;
-
-public interface ClusterServiceAdapter extends Adapter {
- final ConfigKey<Integer> ClusterMessageTimeOut = new
ConfigKey<Integer>(Integer.class, "cluster.message.timeout.seconds", "Advance",
"300",
- "Time (in seconds) to wait before a inter-management server message
post times out.", true);
+package com.cloud.cluster;
- public ClusterService getPeerService(String strPeer) throws
RemoteException;
+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;
- public String getServiceEndpointName(String strPeer);
+@RunWith(MockitoJUnitRunner.class)
+public class ClusterManagerImplTest {
+ @InjectMocks
+ ClusterManagerImpl clusterManager = new ClusterManagerImpl();
- public int getServicePort();
+ @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 91d8b611a0f..3827236d615 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 @@ public class ClusterServiceServletAdapterTest {
@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 00000000000..baf4e5841bd
--- /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 00000000000..361c77dbeff
--- /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 252fb4b78f6..d0b41b4b564 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 f71274bbc88..821704179fc 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.Security;
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.Configurable;
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 @@ public final class RootCAProvider extends AdapterBase
implements CAProvider, Con
"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 @@ public final class RootCAProvider extends AdapterBase
implements CAProvider, Con
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 @@ public final class RootCAProvider extends AdapterBase
implements CAProvider, Con
@Override
public boolean start() {
+ managementCertificateCustomSAN =
CAManager.CertManagementCustomSubjectAlternativeName.value();
return loadRootCAKeyPair() && loadRootCAKeyPair() &&
loadManagementKeyStore();
}
@@ -456,4 +465,26 @@ public final class RootCAProvider extends AdapterBase
implements CAProvider, Con
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 15514b91c78..8311f4d45ab 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.NoSuchAlgorithmException;
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.ca.Certificate;
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 @@ public class RootCAProviderTest {
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 5b55db24f4d..f95948d73dd 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.KVMPhysicalDisk;
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 @@ public final class LibvirtDeleteVMSnapshotCommandWrapper
extends CommandWrapper<
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 1c27bdd958f..da2839d9cee 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.CommandWrapper;
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 final class LibvirtGetVmIpAddressCommandWrapper
extends CommandWrapper<Ge
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 @@ public final class LibvirtGetVmIpAddressCommandWrapper
extends CommandWrapper<Ge
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 5c79de5f4bf..117a832a68b 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.agent.api.OvsFetchInterfaceCommand;
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 3f281e54bba..769482fa0d7 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.CommandWrapper;
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 @@ public final class LibvirtPrepareForMigrationCommandWrapper
extends CommandWrapp
} 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 fc57cd412f0..0803fc2b956 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.CommandWrapper;
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 @@ public final class LibvirtReadyCommandWrapper extends
CommandWrapper<ReadyComman
}
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 6c83c4d9f06..f230d3f4973 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.resource.ResourceWrapper;
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 @@ public class LibvirtRevokeDirectDownloadCertificateWrapper
extends CommandWrappe
}
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 fff8da7c4ea..0774d306b8a 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 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 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;
-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;
@ResourceWrapper(handles = SetupDirectDownloadCertificateCommand.class)
public class LibvirtSetupDirectDownloadCertificateCommandWrapper extends
CommandWrapper<SetupDirectDownloadCertificateCommand, Answer,
LibvirtComputingResource> {
@@ -79,9 +84,10 @@ public class
LibvirtSetupDirectDownloadCertificateCommandWrapper extends Command
*/
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 @@ public class
LibvirtSetupDirectDownloadCertificateCommandWrapper extends Command
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 @@ public class
LibvirtSetupDirectDownloadCertificateCommandWrapper extends Command
/**
* 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 7ee6ccddf66..518ee2b6f0a 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.io.File;
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 @@ public final class LibvirtStopCommandWrapper extends
CommandWrapper<StopCommand,
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 00000000000..fbc9c2bcb4b
--- /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 5530819c2e4..e534f815e3d 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.mockito.Spy;
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 @@ public class LibvirtPrepareForMigrationCommandWrapperTest {
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 00000000000..cd78733a237
--- /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 00000000000..c701946edaa
--- /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 4a7259c6d33..cf0e689ed27 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 @@ public class ApiServer extends ManagerBase implements
HttpRequestHandler, ApiSer
}
}
+ 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 @@ public class ApiServer extends ManagerBase implements
HttpRequestHandler, ApiSer
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 facad1add16..609c4d512a9 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.KeyStore;
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 @@ public class CAManagerImpl extends ManagerBase implements
CAManager {
@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 00000000000..2c7eebddc49
--- /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 d9bf0817687..fbb04e134dc 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 @@ package com.cloud.utils;
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 @@ public class FileUtil {
}
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 31585534cbd..27ec65a18a4 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.ArrayList;
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 class Script implements Callable<String> {
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 @@ public class Script implements Callable<String> {
_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 @@ public class Script implements Callable<String> {
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 @@ public class Script implements Callable<String> {
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 class Script implements Callable<String> {
}
}
+ 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 00000000000..cc6047959da
--- /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));
+ }
+}