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 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 { protected Logger logger = Logger.getLogger(getClass()); @@ -32,4 +35,26 @@ public abstract class CommandWrapper*?![]{}~".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 { } } - static class ListenerThread extends Thread { - private HttpService _httpService = null; - private volatile ServerSocket _serverSocket = null; - private HttpParams _params = null; - private ExecutorService _executor; + protected static SSLServerSocket getSecuredServerSocket(SSLContext sslContext, String ip, int port) + throws IOException { + SSLServerSocketFactory sslFactory = sslContext.getServerSocketFactory(); + SSLServerSocket serverSocket = null; + if (StringUtils.isNotEmpty(ip)) { + serverSocket = (SSLServerSocket) sslFactory.createServerSocket(port, 0, + InetAddress.getByName(ip)); + } else { + serverSocket = (SSLServerSocket) sslFactory.createServerSocket(port); + } + serverSocket.setNeedClientAuth(true); + return serverSocket; + } - public ListenerThread(HttpRequestHandler requestHandler, int port) { - _executor = Executors.newCachedThreadPool(new NamedThreadFactory("Cluster-Listener")); + static class ListenerThread extends Thread { + private HttpService httpService = null; + private volatile SSLServerSocket serverSocket = null; + private HttpParams params = null; + private ExecutorService executor; + private CAService caService = null; + + public ListenerThread(HttpRequestHandler requestHandler, String ip, int port, + CAService caService) { + this.executor = Executors.newCachedThreadPool(new NamedThreadFactory("Cluster-Listener")); + this.caService = caService; try { - _serverSocket = new ServerSocket(port); - } catch (IOException ioex) { - s_logger.error("error initializing cluster service servlet container", ioex); + SSLContext sslContext = Link.initManagementSSLContext(caService); + serverSocket = getSecuredServerSocket(sslContext, ip, port); + } catch (IOException | GeneralSecurityException e) { + s_logger.error("Error initializing cluster service servlet container for secure connection", + e); return; } - _params = new BasicHttpParams(); - _params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000) + params = new BasicHttpParams(); + params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000) .setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024) .setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false) .setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true) @@ -104,35 +135,55 @@ 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; - protected static HttpClient s_client = null; + private CAService caService; + + private Gson gson = new Gson(); + + protected static CloseableHttpClient s_client = null; + + private void logPostParametersForFailedEncoding(List 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 getClusterServicePduPostParameters(final ClusterServicePdu pdu) { + List 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 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 getPingPostParameters(final String callingPeer) { + List 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 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); + SSLContext sslContext = null; + try { + sslContext = Link.initManagementSSLContext(caService); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException(e); + } - // TODO make it configurable - mgr.getParams().setMaxTotalConnections(1000); + int timeout = ClusterServiceAdapter.ClusterMessageTimeOut.value() * 1000; + RequestConfig config = RequestConfig.custom() + .setConnectTimeout(timeout) + .setConnectionRequestTimeout(timeout) + .setSocketTimeout(timeout).build(); - s_client = new HttpClient(mgr); - final HttpClientParams clientParams = new HttpClientParams(); - clientParams.setSoTimeout(ClusterServiceAdapter.ClusterMessageTimeOut.value() * 1000); - - s_client.setParams(clientParams); + s_client = HttpClientBuilder.create() + .setDefaultRequestConfig(config) + .setSSLContext(sslContext) + .build(); } return s_client; } diff --git a/framework/cluster/src/test/java/com/cloud/cluster/ClusterManagerImplTest.java b/framework/cluster/src/test/java/com/cloud/cluster/ClusterManagerImplTest.java new file mode 100644 index 00000000000..9b1854f7348 --- /dev/null +++ b/framework/cluster/src/test/java/com/cloud/cluster/ClusterManagerImplTest.java @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.cluster; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +@RunWith(MockitoJUnitRunner.class) +public class ClusterManagerImplTest { + @InjectMocks + ClusterManagerImpl clusterManager = new ClusterManagerImpl(); + + @Test + public void testGetSelfNodeIP() { + String ip = "1.2.3.4"; + ReflectionTestUtils.setField(clusterManager, "_clusterNodeIP", ip); + Assert.assertEquals(ip, clusterManager.getSelfNodeIP()); + } +} diff --git a/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletAdapterTest.java b/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletAdapterTest.java index 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 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) 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 parameters = + clusterServiceServlet.getClusterServicePduPostParameters(Mockito.mock(ClusterServicePdu.class)); + Assert.assertTrue(CollectionUtils.isNotEmpty(parameters)); + Optional 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 parameters = + clusterServiceServlet.getPingPostParameters(peer); + Assert.assertTrue(CollectionUtils.isNotEmpty(parameters)); + Optional 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 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> 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> altNames = new ArrayList<>(); + altNames.add(List.of(GeneralName.dNSName, UUID.randomUUID().toString())); + altNames.add(List.of(GeneralName.dNSName, UUID.randomUUID().toString())); + Collection> 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> altNames = new ArrayList<>(); + altNames.add(List.of(GeneralName.dNSName, customSAN)); + altNames.add(List.of(GeneralName.dNSName, UUID.randomUUID().toString())); + Collection> 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 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 { @@ -37,31 +41,51 @@ public final class LibvirtGetVmIpAddressCommandWrapper extends CommandWrapper 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\\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 { 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 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 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 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 { @@ -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 { @@ -50,12 +52,17 @@ public final class LibvirtReadyCommandWrapper extends CommandWrapper 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 { @@ -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 com.cloud.agent.api.Answer; -import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; -import com.cloud.resource.CommandWrapper; -import com.cloud.resource.ResourceWrapper; -import com.cloud.utils.PropertiesUtil; -import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.utils.script.Script; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + import org.apache.cloudstack.agent.directdownload.SetupDirectDownloadCertificateCommand; import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.FileUtil; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.Script; @ResourceWrapper(handles = SetupDirectDownloadCertificateCommand.class) public class LibvirtSetupDirectDownloadCertificateCommandWrapper extends CommandWrapper { @@ -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 { @@ -121,10 +120,7 @@ public final class LibvirtStopCommandWrapper extends CommandWrapper 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 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 addresses = networkInterface.getInetAddresses(); + while(addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (addr instanceof Inet4Address) { + ipAddress = addr.getHostAddress(); + break; + }; + } + } + } + } catch (SocketException ignored) {} + Ternary 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 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 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 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 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 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 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 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 { private static final Logger s_logger = Logger.getLogger(Script.class); @@ -52,7 +58,7 @@ public class Script implements Callable { 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 { _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 { 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 { 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 executePipedCommands(List commands, long timeout) { + if (timeout <= 0) { + timeout = DEFAULT_TIMEOUT; + } + Callable> commandRunner = () -> { + List builders = commands.stream().map(ProcessBuilder::new).collect(Collectors.toList()); + List 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> future = s_executors.submit(commandRunner); + Pair 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 { } } + 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 commands = new ArrayList<>(); + commands.add(new String[]{"echo", keyword}); + Pair 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 commands = Arrays.asList( + new String[]{"echo", String.format("%s\n World", keyword)}, + new String[]{"grep", keyword} + ); + Pair 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 commands = new ArrayList<>(); + commands.add(new String[]{"sh", "-c", "sleep 10"}); + Pair 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)); + } +}