framework/cluster: improve cluster service and integration API service

- mTLS implementation for cluster service communication
- Listen only on the specified cluster node IP address instead of all interfaces
- Validate incoming cluster service requests are from peer management servers based on the server's certificate dns name which can be through global config - ca.framework.cert.management.custom.san
- Hardening of KVM command wrapper script execution
- Improve API server integration port check
- cloudstack-management.default: don't have JMX configuration if not needed. JMX is used for instrumentation; users who need to use it should enable it explicitly

Co-authored-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
Co-authored-by: Wei Zhou <weizhou@apache.org>
Co-authored-by: Rohit Yadav <rohit.yadav@shapeblue.com>

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2024-07-02 17:52:52 +05:30
parent eb1a0ef7f2
commit beebeed5e2
35 changed files with 1306 additions and 234 deletions

View File

@ -77,6 +77,14 @@ public interface CAManager extends CAService, Configurable, PluggableService {
"15",
"The number of days before expiry of a client certificate, the validations are checked. Admins are alerted when auto-renewal is not allowed, otherwise auto-renewal is attempted.", true, ConfigKey.Scope.Cluster);
ConfigKey<String> CertManagementCustomSubjectAlternativeName = new ConfigKey<>("Advanced", String.class,
"ca.framework.cert.management.custom.san",
"cloudstack.internal",
"The custom Subject Alternative Name that will be added to the management server certificate. " +
"The actual implementation will depend on the configured CA provider.",
false);
/**
* Returns a list of available CA provider plugins
* @return returns list of CAProvider

View File

@ -19,9 +19,12 @@
package com.cloud.resource;
import org.apache.log4j.Logger;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import org.apache.log4j.Logger;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;
public abstract class CommandWrapper<T extends Command, A extends Answer, R extends ServerResource> {
protected Logger logger = Logger.getLogger(getClass());
@ -32,4 +35,26 @@ public abstract class CommandWrapper<T extends Command, A extends Answer, R exte
* @return A and the Answer from the command.
*/
public abstract A execute(T command, R serverResource);
protected String sanitizeBashCommandArgument(String input) {
StringBuilder sanitized = new StringBuilder();
for (char c : input.toCharArray()) {
if ("\\\"'`$|&;()<>*?![]{}~".indexOf(c) != -1) {
sanitized.append('\\');
}
sanitized.append(c);
}
return sanitized.toString();
}
public void removeDpdkPort(String portToRemove) {
logger.debug("Removing DPDK port: " + portToRemove);
int port;
try {
port = Integer.valueOf(portToRemove);
} catch (NumberFormatException nfe) {
throw new CloudRuntimeException(String.format("Invalid DPDK port specified: '%s'", portToRemove));
}
Script.executeCommand("ovs-vsctl", "del-port", String.valueOf(port));
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
/**

View File

@ -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;
}

View File

@ -28,7 +28,5 @@ public interface ClusterServiceAdapter extends Adapter {
public ClusterService getPeerService(String strPeer) throws RemoteException;
public String getServiceEndpointName(String strPeer);
public int getServicePort();
}

View File

@ -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;
}

View File

@ -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");
}

View File

@ -17,98 +17,143 @@
package com.cloud.cluster;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.rmi.RemoteException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import javax.net.ssl.SSLContext;
import org.apache.cloudstack.framework.ca.CAService;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.Profiler;
import com.cloud.utils.nio.Link;
import com.google.gson.Gson;
public class ClusterServiceServletImpl implements ClusterService {
private static final long serialVersionUID = 4574025200012566153L;
private static final Logger s_logger = Logger.getLogger(ClusterServiceServletImpl.class);
private String _serviceUrl;
private String serviceUrl;
protected static HttpClient s_client = null;
private CAService caService;
private Gson gson = new Gson();
protected static CloseableHttpClient s_client = null;
private void logPostParametersForFailedEncoding(List<NameValuePair> parameters) {
if (s_logger.isTraceEnabled()) {
s_logger.trace(String.format("%s encoding failed for POST parameters: %s", HttpUtils.UTF_8,
gson.toJson(parameters)));
}
}
public ClusterServiceServletImpl() {
}
public ClusterServiceServletImpl(final String serviceUrl) {
s_logger.info("Setup cluster service servlet. service url: " + serviceUrl + ", request timeout: " + ClusterServiceAdapter.ClusterMessageTimeOut.value() +
" seconds");
public ClusterServiceServletImpl(final String serviceUrl, final CAService caService) {
s_logger.info(String.format("Setup cluster service servlet. service url: %s, request timeout: %d seconds", serviceUrl,
ClusterServiceAdapter.ClusterMessageTimeOut.value()));
this.serviceUrl = serviceUrl;
this.caService = caService;
}
_serviceUrl = serviceUrl;
protected List<NameValuePair> getClusterServicePduPostParameters(final ClusterServicePdu pdu) {
List<NameValuePair> postParameters = new ArrayList<>();
postParameters.add(new BasicNameValuePair("method", Integer.toString(RemoteMethodConstants.METHOD_DELIVER_PDU)));
postParameters.add(new BasicNameValuePair("sourcePeer", pdu.getSourcePeer()));
postParameters.add(new BasicNameValuePair("destPeer", pdu.getDestPeer()));
postParameters.add(new BasicNameValuePair("pduSeq", Long.toString(pdu.getSequenceId())));
postParameters.add(new BasicNameValuePair("pduAckSeq", Long.toString(pdu.getAckSequenceId())));
postParameters.add(new BasicNameValuePair("agentId", Long.toString(pdu.getAgentId())));
postParameters.add(new BasicNameValuePair("gsonPackage", pdu.getJsonPackage()));
postParameters.add(new BasicNameValuePair("stopOnError", pdu.isStopOnError() ? "1" : "0"));
postParameters.add(new BasicNameValuePair("pduType", Integer.toString(pdu.getPduType())));
return postParameters;
}
@Override
public String execute(final ClusterServicePdu pdu) throws RemoteException {
final HttpClient client = getHttpClient();
final PostMethod method = new PostMethod(_serviceUrl);
method.addParameter("method", Integer.toString(RemoteMethodConstants.METHOD_DELIVER_PDU));
method.addParameter("sourcePeer", pdu.getSourcePeer());
method.addParameter("destPeer", pdu.getDestPeer());
method.addParameter("pduSeq", Long.toString(pdu.getSequenceId()));
method.addParameter("pduAckSeq", Long.toString(pdu.getAckSequenceId()));
method.addParameter("agentId", Long.toString(pdu.getAgentId()));
method.addParameter("gsonPackage", pdu.getJsonPackage());
method.addParameter("stopOnError", pdu.isStopOnError() ? "1" : "0");
method.addParameter("pduType", Integer.toString(pdu.getPduType()));
if (s_logger.isDebugEnabled()) {
s_logger.debug(String.format("Executing ClusterServicePdu with service URL: %s", serviceUrl));
}
final CloseableHttpClient client = getHttpClient();
final HttpPost method = new HttpPost(serviceUrl);
final List<NameValuePair> postParameters = getClusterServicePduPostParameters(pdu);
try {
method.setEntity(new UrlEncodedFormEntity(postParameters, HttpUtils.UTF_8));
} catch (UnsupportedEncodingException e) {
s_logger.error("Failed to encode request POST parameters", e);
logPostParametersForFailedEncoding(postParameters);
throw new RemoteException("Failed to encode request POST parameters", e);
}
return executePostMethod(client, method);
}
protected List<NameValuePair> getPingPostParameters(final String callingPeer) {
List<NameValuePair> postParameters = new ArrayList<>();
postParameters.add(new BasicNameValuePair("method", Integer.toString(RemoteMethodConstants.METHOD_PING)));
postParameters.add(new BasicNameValuePair("callingPeer", callingPeer));
return postParameters;
}
@Override
public boolean ping(final String callingPeer) throws RemoteException {
if (s_logger.isDebugEnabled()) {
s_logger.debug("Ping at " + _serviceUrl);
s_logger.debug("Ping at " + serviceUrl);
}
final HttpClient client = getHttpClient();
final PostMethod method = new PostMethod(_serviceUrl);
final CloseableHttpClient client = getHttpClient();
final HttpPost method = new HttpPost(serviceUrl);
method.addParameter("method", Integer.toString(RemoteMethodConstants.METHOD_PING));
method.addParameter("callingPeer", callingPeer);
List<NameValuePair> postParameters = getPingPostParameters(callingPeer);
try {
method.setEntity(new UrlEncodedFormEntity(postParameters, HttpUtils.UTF_8));
} catch (UnsupportedEncodingException e) {
s_logger.error("Failed to encode ping request POST parameters", e);
logPostParametersForFailedEncoding(postParameters);
throw new RemoteException("Failed to encode ping request POST parameters", e);
}
final String returnVal = executePostMethod(client, method);
if ("true".equalsIgnoreCase(returnVal)) {
return true;
}
return false;
return Boolean.TRUE.toString().equalsIgnoreCase(returnVal);
}
private String executePostMethod(final HttpClient client, final PostMethod method) {
int response = 0;
private String executePostMethod(final CloseableHttpClient client, final HttpPost method) {
String result = null;
try {
final Profiler profiler = new Profiler();
profiler.start();
response = client.executeMethod(method);
CloseableHttpResponse httpResponse = client.execute(method);
int response = httpResponse.getStatusLine().getStatusCode();
if (response == HttpStatus.SC_OK) {
result = method.getResponseBodyAsString();
result = EntityUtils.toString(httpResponse.getEntity());
profiler.stop();
if (s_logger.isDebugEnabled()) {
s_logger.debug("POST " + _serviceUrl + " response :" + result + ", responding time: " + profiler.getDurationInMillis() + " ms");
s_logger.debug("POST " + serviceUrl + " response :" + result + ", responding time: " + profiler.getDurationInMillis() + " ms");
}
} else {
profiler.stop();
s_logger.error("Invalid response code : " + response + ", from : " + _serviceUrl + ", method : " + method.getParameter("method") + " responding time: " +
s_logger.error("Invalid response code : " + response + ", from : " + serviceUrl + ", method : " + method.getParams().getParameter("method") + " responding time: " +
profiler.getDurationInMillis());
}
} catch (final HttpException e) {
s_logger.error("HttpException from : " + _serviceUrl + ", method : " + method.getParameter("method"));
} catch (final IOException e) {
s_logger.error("IOException from : " + _serviceUrl + ", method : " + method.getParameter("method"));
} catch (final Throwable e) {
s_logger.error("Exception from : " + _serviceUrl + ", method : " + method.getParameter("method") + ", exception :", e);
} catch (IOException e) {
s_logger.error("Exception from : " + serviceUrl + ", method : " + method.getParams().getParameter("method") + ", exception :", e);
} finally {
method.releaseConnection();
}
@ -116,20 +161,25 @@ 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;
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,87 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.cluster;
import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLContextSpi;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import org.apache.commons.collections.CollectionUtils;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.utils.StringUtils;
@RunWith(MockitoJUnitRunner.class)
public class ClusterServiceServletContainerTest {
private void runGetSecuredServerSocket(String ip) {
SSLContext sslContext = Mockito.mock(SSLContext.class);
SSLContextSpi sslContextSpi = Mockito.mock(SSLContextSpi.class);
ReflectionTestUtils.setField(sslContext, "contextSpi", sslContextSpi);
SSLServerSocketFactory factory = Mockito.mock(SSLServerSocketFactory.class);
Mockito.when(sslContext.getServerSocketFactory()).thenReturn(factory);
int port = 9090;
final List<Boolean> socketNeedClientAuth = new ArrayList<>();
try {
SSLServerSocket socketMock = Mockito.mock(SSLServerSocket.class);
if (StringUtils.isBlank(ip)) {
Mockito.when(factory.createServerSocket(port)).thenReturn(socketMock);
} else {
Mockito.when(factory.createServerSocket(Mockito.anyInt(), Mockito.anyInt(),
Mockito.any(InetAddress.class))).thenReturn(socketMock);
}
Mockito.doAnswer((Answer<Void>) invocationOnMock -> {
boolean needClientAuth = (boolean) invocationOnMock.getArguments()[0];
socketNeedClientAuth.add(needClientAuth);
return null;
}).when(socketMock).setNeedClientAuth(Mockito.anyBoolean());
SSLServerSocket socket = ClusterServiceServletContainer.getSecuredServerSocket(sslContext, ip, 9090);
if (StringUtils.isBlank(ip)) {
Mockito.verify(factory, Mockito.times(1)).createServerSocket(port);
} else {
Mockito.verify(factory, Mockito.times(1)).createServerSocket(port, 0, InetAddress.getByName(ip));
}
Mockito.verify(socket, Mockito.times(1)).setNeedClientAuth(Mockito.anyBoolean());
Assert.assertTrue(CollectionUtils.isNotEmpty(socketNeedClientAuth));
Assert.assertTrue(socketNeedClientAuth.get(0));
} catch (IOException e) {
Assert.fail("Exception occurred: " + e.getMessage());
}
}
@Test
public void testGetSecuredServerSocketNoIp() {
runGetSecuredServerSocket("");
}
@Test
public void testGetSecuredServerSocketIp() {
runGetSecuredServerSocket("1.2.3.4");
}
}

View File

@ -0,0 +1,64 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.cluster;
import java.util.List;
import java.util.Optional;
import org.apache.commons.collections.CollectionUtils;
import org.apache.http.NameValuePair;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class ClusterServiceServletImplTest {
@InjectMocks
ClusterServiceServletImpl clusterServiceServlet = new ClusterServiceServletImpl();
@Test
public void testClusterServicePduPostParameters() {
List<NameValuePair> parameters =
clusterServiceServlet.getClusterServicePduPostParameters(Mockito.mock(ClusterServicePdu.class));
Assert.assertTrue(CollectionUtils.isNotEmpty(parameters));
Optional<NameValuePair> opt = parameters.stream().filter(x -> x.getName().equals("method")).findFirst();
Assert.assertTrue(opt.isPresent());
NameValuePair val = opt.get();
Assert.assertEquals(Integer.toString(RemoteMethodConstants.METHOD_DELIVER_PDU), val.getValue());
}
@Test
public void testPingPostParameters() {
String peer = "1.2.3.4";
List<NameValuePair> parameters =
clusterServiceServlet.getPingPostParameters(peer);
Assert.assertTrue(CollectionUtils.isNotEmpty(parameters));
Optional<NameValuePair> opt = parameters.stream().filter(x -> x.getName().equals("method")).findFirst();
Assert.assertTrue(opt.isPresent());
NameValuePair val = opt.get();
Assert.assertEquals(Integer.toString(RemoteMethodConstants.METHOD_PING), val.getValue());
opt = parameters.stream().filter(x -> x.getName().equals("callingPeer")).findFirst();
Assert.assertTrue(opt.isPresent());
val = opt.get();
Assert.assertEquals(peer, val.getValue());
}
}

View File

@ -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/*"

View File

@ -33,9 +33,11 @@ import java.security.Security;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -57,6 +59,7 @@ import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.utils.security.CertUtils;
import org.apache.cloudstack.utils.security.KeyStoreUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.pkcs.Attribute;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
@ -127,6 +130,8 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
"true",
"When set to true, it will allow expired client certificate during SSL handshake.", true);
private static String managementCertificateCustomSAN;
///////////////////////////////////////////////////////////
/////////////// Root CA Private Methods ///////////////////
@ -365,8 +370,11 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
if (managementKeyStore != null) {
return true;
}
final Certificate serverCertificate = issueCertificate(Collections.singletonList(NetUtils.getHostName()),
NetUtils.getAllDefaultNicIps(), getCaValidityDays());
List<String> domainNames = new ArrayList<>();
domainNames.add(NetUtils.getHostName());
domainNames.add(CAManager.CertManagementCustomSubjectAlternativeName.value());
final Certificate serverCertificate = issueCertificate(
domainNames, NetUtils.getAllDefaultNicIps(), getCaValidityDays());
if (serverCertificate == null || serverCertificate.getPrivateKey() == null) {
throw new CloudRuntimeException("Failed to generate management server certificate and load management server keystore");
}
@ -402,6 +410,7 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
@Override
public boolean start() {
managementCertificateCustomSAN = CAManager.CertManagementCustomSubjectAlternativeName.value();
return loadRootCAKeyPair() && loadRootCAKeyPair() && loadManagementKeyStore();
}
@ -456,4 +465,26 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
public String getDescription() {
return "CloudStack's Root CA provider plugin";
}
@Override
public boolean isManagementCertificate(java.security.cert.Certificate certificate) throws CertificateParsingException {
if (!(certificate instanceof X509Certificate)) {
return false;
}
X509Certificate x509Certificate = (X509Certificate) certificate;
// Check for alternative names
Collection<List<?>> altNames = x509Certificate.getSubjectAlternativeNames();
if (CollectionUtils.isEmpty(altNames)) {
return false;
}
for (List<?> altName : altNames) {
int type = (Integer) altName.get(0);
String name = (String) altName.get(1);
if (type == GeneralName.dNSName && managementCertificateCustomSAN.equals(name)) {
return true;
}
}
return false;
}
}

View File

@ -26,8 +26,13 @@ import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import javax.net.ssl.SSLEngine;
@ -35,15 +40,16 @@ import org.apache.cloudstack.framework.ca.Certificate;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.utils.security.CertUtils;
import org.apache.cloudstack.utils.security.SSLUtils;
import org.bouncycastle.asn1.x509.GeneralName;
import org.joda.time.DateTime;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
@RunWith(MockitoJUnitRunner.class)
@ -150,4 +156,56 @@ public class RootCAProviderTest {
Assert.assertEquals(provider.getProviderName(), "root");
}
@Test
public void testIsManagementCertificateNotX509() {
try {
Assert.assertFalse(provider.isManagementCertificate(Mockito.mock(java.security.cert.Certificate.class)));
} catch (CertificateParsingException e) {
Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
}
}
@Test
public void testIsManagementCertificateNoAltNames() {
try {
X509Certificate certificate = Mockito.mock(X509Certificate.class);
Mockito.when(certificate.getSubjectAlternativeNames()).thenReturn(new ArrayList<>());
Assert.assertFalse(provider.isManagementCertificate(certificate));
} catch (CertificateParsingException e) {
Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
}
}
@Test
public void testIsManagementCertificateNoMatch() {
ReflectionTestUtils.setField(provider, "managementCertificateCustomSAN", "cloudstack");
try {
X509Certificate certificate = Mockito.mock(X509Certificate.class);
List<List<?>> altNames = new ArrayList<>();
altNames.add(List.of(GeneralName.dNSName, UUID.randomUUID().toString()));
altNames.add(List.of(GeneralName.dNSName, UUID.randomUUID().toString()));
Collection<List<?>> collection = new ArrayList<>(altNames);
Mockito.when(certificate.getSubjectAlternativeNames()).thenReturn(collection);
Assert.assertFalse(provider.isManagementCertificate(certificate));
} catch (CertificateParsingException e) {
Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
}
}
@Test
public void testIsManagementCertificateMatch() {
String customSAN = "cloudstack";
ReflectionTestUtils.setField(provider, "managementCertificateCustomSAN", customSAN);
try {
X509Certificate certificate = Mockito.mock(X509Certificate.class);
List<List<?>> altNames = new ArrayList<>();
altNames.add(List.of(GeneralName.dNSName, customSAN));
altNames.add(List.of(GeneralName.dNSName, UUID.randomUUID().toString()));
Collection<List<?>> collection = new ArrayList<>(altNames);
Mockito.when(certificate.getSubjectAlternativeNames()).thenReturn(collection);
Assert.assertTrue(provider.isManagementCertificate(certificate));
} catch (CertificateParsingException e) {
Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
}
}
}

View File

@ -19,6 +19,9 @@
package com.cloud.hypervisor.kvm.resource.wrapper;
import java.util.ArrayList;
import java.util.List;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
import org.apache.log4j.Logger;
@ -36,8 +39,8 @@ import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk;
import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.storage.Volume;
import com.cloud.storage.Storage.ImageFormat;
import com.cloud.storage.Volume;
import com.cloud.utils.script.Script;
@ResourceWrapper(handles = DeleteVMSnapshotCommand.class)
@ -96,12 +99,20 @@ public final class LibvirtDeleteVMSnapshotCommandWrapper extends CommandWrapper<
PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO) rootVolume.getDataStore();
KVMPhysicalDisk rootDisk = storagePoolMgr.getPhysicalDisk(primaryStore.getPoolType(),
primaryStore.getUuid(), rootVolume.getPath());
String qemu_img_snapshot = Script.runSimpleBashScript("qemu-img snapshot -l " + rootDisk.getPath() + " | tail -n +3 | awk -F ' ' '{print $2}' | grep ^" + cmd.getTarget().getSnapshotName() + "$");
String qemuImgPath = Script.getExecutableAbsolutePath("qemu-img");
List<String[]> commands = new ArrayList<>();
commands.add(new String[]{qemuImgPath, "snapshot", "-l", sanitizeBashCommandArgument(rootDisk.getPath())});
commands.add(new String[]{Script.getExecutableAbsolutePath("tail"), "-n", "+3"});
commands.add(new String[]{Script.getExecutableAbsolutePath("awk"), "-F", " ", "{print $2}"});
commands.add(new String[]{Script.getExecutableAbsolutePath("grep"), "^" + sanitizeBashCommandArgument(cmd.getTarget().getSnapshotName()) + "$"});
String qemu_img_snapshot = Script.executePipedCommands(commands, 0).second();
if (qemu_img_snapshot == null) {
s_logger.info("Cannot find snapshot " + cmd.getTarget().getSnapshotName() + " in file " + rootDisk.getPath() + ", return true");
return new DeleteVMSnapshotAnswer(cmd, cmd.getVolumeTOs());
}
int result = Script.runSimpleBashScriptForExitValue("qemu-img snapshot -d " + cmd.getTarget().getSnapshotName() + " " + rootDisk.getPath());
int result = Script.executeCommandForExitValue(qemuImgPath, "snapshot", "-d",
sanitizeBashCommandArgument(cmd.getTarget().getSnapshotName()),
sanitizeBashCommandArgument(rootDisk.getPath()));
if (result != 0) {
return new DeleteVMSnapshotAnswer(cmd, false,
"Delete VM Snapshot Failed due to can not remove snapshot from image file " + rootDisk.getPath() + " : " + result);

View File

@ -19,6 +19,11 @@
package com.cloud.hypervisor.kvm.resource.wrapper;
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.GetVmIpAddressCommand;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
@ -26,7 +31,6 @@ import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.net.NetUtils;
import com.cloud.utils.script.Script;
import org.apache.log4j.Logger;
@ResourceWrapper(handles = GetVmIpAddressCommand.class)
public final class LibvirtGetVmIpAddressCommandWrapper extends CommandWrapper<GetVmIpAddressCommand, Answer, LibvirtComputingResource> {
@ -37,31 +41,51 @@ public final class LibvirtGetVmIpAddressCommandWrapper extends CommandWrapper<Ge
public Answer execute(final GetVmIpAddressCommand command, final LibvirtComputingResource libvirtComputingResource) {
String ip = null;
boolean result = false;
String vmName = command.getVmName();
String sanitizedVmName = sanitizeBashCommandArgument(vmName);
String networkCidr = command.getVmNetworkCidr();
List<String[]> commands = new ArrayList<>();
final String virt_ls_path = Script.getExecutableAbsolutePath("virt-ls");
final String virt_cat_path = Script.getExecutableAbsolutePath("virt-cat");
final String virt_win_reg_path = Script.getExecutableAbsolutePath("virt-win-reg");
final String tail_path = Script.getExecutableAbsolutePath("tail");
final String grep_path = Script.getExecutableAbsolutePath("grep");
final String awk_path = Script.getExecutableAbsolutePath("awk");
final String sed_path = Script.getExecutableAbsolutePath("sed");
if(!command.isWindows()) {
//List all dhcp lease files inside guestVm
String leasesList = Script.runSimpleBashScript(new StringBuilder().append("virt-ls ").append(command.getVmName())
.append(" /var/lib/dhclient/ | grep .*\\*.leases").toString());
commands.add(new String[]{virt_ls_path, sanitizedVmName, "/var/lib/dhclient/"});
commands.add(new String[]{grep_path, ".*\\*.leases"});
String leasesList = Script.executePipedCommands(commands, 0).second();
if(leasesList != null) {
String[] leasesFiles = leasesList.split("\n");
for(String leaseFile : leasesFiles){
//Read from each dhclient lease file inside guest Vm using virt-cat libguestfs ulitiy
String ipAddr = Script.runSimpleBashScript(new StringBuilder().append("virt-cat ").append(command.getVmName())
.append(" /var/lib/dhclient/" + leaseFile + " | tail -16 | grep 'fixed-address' | awk '{print $2}' | sed -e 's/;//'").toString());
//Read from each dhclient lease file inside guest Vm using virt-cat libguestfs utility
commands = new ArrayList<>();
commands.add(new String[]{virt_cat_path, sanitizedVmName, "/var/lib/dhclient/" + leaseFile});
commands.add(new String[]{tail_path, "-16"});
commands.add(new String[]{grep_path, "fixed-address"});
commands.add(new String[]{awk_path, "{print $2}"});
commands.add(new String[]{sed_path, "-e", "s/;//"});
String ipAddr = Script.executePipedCommands(commands, 0).second();
// Check if the IP belongs to the network
if((ipAddr != null) && NetUtils.isIpWithInCidrRange(ipAddr, networkCidr)){
if((ipAddr != null) && NetUtils.isIpWithInCidrRange(ipAddr, networkCidr)) {
ip = ipAddr;
break;
}
s_logger.debug("GetVmIp: "+command.getVmName()+ " Ip: "+ipAddr+" does not belong to network "+networkCidr);
s_logger.debug("GetVmIp: "+ vmName + " Ip: "+ipAddr+" does not belong to network "+networkCidr);
}
}
} else {
// For windows, read from guest Vm registry using virt-win-reg libguestfs ulitiy. Registry Path: HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\<service>\DhcpIPAddress
String ipList = Script.runSimpleBashScript(new StringBuilder().append("virt-win-reg --unsafe-printable-strings ").append(command.getVmName())
.append(" 'HKEY_LOCAL_MACHINE\\SYSTEM\\ControlSet001\\Services\\Tcpip\\Parameters\\Interfaces' | grep DhcpIPAddress | awk -F : '{print $2}' | sed -e 's/^\"//' -e 's/\"$//'").toString());
commands = new ArrayList<>();
commands.add(new String[]{virt_win_reg_path, "--unsafe-printable-strings", sanitizedVmName, "HKEY_LOCAL_MACHINE\\SYSTEM\\ControlSet001\\Services\\Tcpip\\Parameters\\Interfaces"});
commands.add(new String[]{grep_path, "DhcpIPAddress"});
commands.add(new String[]{awk_path, "-F", ":", "{print $2}"});
commands.add(new String[]{sed_path, "-e", "s/^\"//", "-e", "s/\"$//"});
String ipList = Script.executePipedCommands(commands, 0).second();
if(ipList != null) {
s_logger.debug("GetVmIp: "+command.getVmName()+ "Ips: "+ipList);
s_logger.debug("GetVmIp: "+ vmName + "Ips: "+ipList);
String[] ips = ipList.split("\n");
for (String ipAddr : ips){
// Check if the IP belongs to the network
@ -69,13 +93,13 @@ public final class LibvirtGetVmIpAddressCommandWrapper extends CommandWrapper<Ge
ip = ipAddr;
break;
}
s_logger.debug("GetVmIp: "+command.getVmName()+ " Ip: "+ipAddr+" does not belong to network "+networkCidr);
s_logger.debug("GetVmIp: "+ vmName + " Ip: "+ipAddr+" does not belong to network "+networkCidr);
}
}
}
if(ip != null){
result = true;
s_logger.debug("GetVmIp: "+command.getVmName()+ " Found Ip: "+ip);
s_logger.debug("GetVmIp: "+ vmName + " Found Ip: "+ip);
}
return new Answer(command, result, ip);
}

View File

@ -19,7 +19,12 @@
package com.cloud.hypervisor.kvm.resource.wrapper;
import org.apache.commons.lang3.StringUtils;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import org.apache.log4j.Logger;
import com.cloud.agent.api.Answer;
@ -28,33 +33,73 @@ import com.cloud.agent.api.OvsFetchInterfaceCommand;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.script.Script;
import com.cloud.utils.Ternary;
@ResourceWrapper(handles = OvsFetchInterfaceCommand.class)
public final class LibvirtOvsFetchInterfaceCommandWrapper extends CommandWrapper<OvsFetchInterfaceCommand, Answer, LibvirtComputingResource> {
private static final Logger s_logger = Logger.getLogger(LibvirtOvsFetchInterfaceCommandWrapper.class);
private String getSubnetMaskForAddress(NetworkInterface networkInterface, InetAddress inetAddress) {
for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) {
if (!inetAddress.equals(address.getAddress())) {
continue;
}
int prefixLength = address.getNetworkPrefixLength();
int mask = 0xffffffff << (32 - prefixLength);
return String.format("%d.%d.%d.%d",
(mask >>> 24) & 0xff,
(mask >>> 16) & 0xff,
(mask >>> 8) & 0xff,
mask & 0xff);
}
return "";
}
private String getMacAddress(NetworkInterface networkInterface) throws SocketException {
byte[] macBytes = networkInterface.getHardwareAddress();
if (macBytes == null) {
return "";
}
StringBuilder macAddress = new StringBuilder();
for (byte b : macBytes) {
macAddress.append(String.format("%02X:", b));
}
if (macAddress.length() > 0) {
macAddress.deleteCharAt(macAddress.length() - 1); // Remove trailing colon
}
return macAddress.toString();
}
public Ternary<String, String, String> getInterfaceDetails(String interfaceName) throws SocketException {
NetworkInterface networkInterface = NetworkInterface.getByName(interfaceName);
if (networkInterface == null) {
logger.warn(String.format("Network interface: '%s' not found", interfaceName));
return new Ternary<>(null, null, null);
}
Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
InetAddress inetAddress = inetAddresses.nextElement();
if (inetAddress instanceof java.net.Inet4Address) {
String ipAddress = inetAddress.getHostAddress();
String subnetMask = getSubnetMaskForAddress(networkInterface, inetAddress);
String macAddress = getMacAddress(networkInterface);
return new Ternary<>(ipAddress, subnetMask, macAddress);
}
}
return new Ternary<>(null, null, null);
}
@Override
public Answer execute(final OvsFetchInterfaceCommand command, final LibvirtComputingResource libvirtComputingResource) {
final String label = command.getLabel();
final String label = "'" + command.getLabel() + "'";
s_logger.debug("Will look for network with name-label:" + label);
try {
String ipadd = Script.runSimpleBashScript("ifconfig " + label + " | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'");
if (StringUtils.isEmpty(ipadd)) {
ipadd = Script.runSimpleBashScript("ifconfig " + label + " | grep ' inet ' | awk '{ print $2}'");
}
String mask = Script.runSimpleBashScript("ifconfig " + label + " | grep 'inet addr:' | cut -d: -f4");
if (StringUtils.isEmpty(mask)) {
mask = Script.runSimpleBashScript("ifconfig " + label + " | grep ' inet ' | awk '{ print $4}'");
}
String mac = Script.runSimpleBashScript("ifconfig " + label + " | grep HWaddr | awk -F \" \" '{print $5}'");
if (StringUtils.isEmpty(mac)) {
mac = Script.runSimpleBashScript("ifconfig " + label + " | grep ' ether ' | awk '{ print $2}'");
}
Ternary<String, String, String> interfaceDetails = getInterfaceDetails(label);
return new OvsFetchInterfaceAnswer(command, true, "Interface " + label
+ " retrieved successfully", ipadd, mask, mac);
+ " retrieved successfully", interfaceDetails.first(), interfaceDetails.second(),
interfaceDetails.third());
} catch (final Exception e) {
s_logger.warn("Caught execption when fetching interface", e);

View File

@ -47,7 +47,6 @@ import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.storage.Volume;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;
@ResourceWrapper(handles = PrepareForMigrationCommand.class)
public final class LibvirtPrepareForMigrationCommandWrapper extends CommandWrapper<PrepareForMigrationCommand, Answer, LibvirtComputingResource> {
@ -126,9 +125,7 @@ public final class LibvirtPrepareForMigrationCommandWrapper extends CommandWrapp
} catch (final LibvirtException | CloudRuntimeException | InternalErrorException | URISyntaxException e) {
if (MapUtils.isNotEmpty(dpdkInterfaceMapping)) {
for (DpdkTO to : dpdkInterfaceMapping.values()) {
String cmd = String.format("ovs-vsctl del-port %s", to.getPort());
s_logger.debug("Removing DPDK port: " + to.getPort());
Script.runSimpleBashScript(cmd);
removeDpdkPort(to.getPort());
}
}
return new PrepareForMigrationAnswer(command, e.toString());

View File

@ -19,9 +19,13 @@
package com.cloud.hypervisor.kvm.resource.wrapper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.ReadyAnswer;
import com.cloud.agent.api.ReadyCommand;
@ -31,8 +35,6 @@ import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.script.Script;
import org.apache.log4j.Logger;
@ResourceWrapper(handles = ReadyCommand.class)
public final class LibvirtReadyCommandWrapper extends CommandWrapper<ReadyCommand, Answer, LibvirtComputingResource> {
@ -50,12 +52,17 @@ public final class LibvirtReadyCommandWrapper extends CommandWrapper<ReadyComman
}
private boolean hostSupportsUefi(boolean isUbuntuHost) {
String cmd = "rpm -qa | grep -i ovmf";
int result;
if (isUbuntuHost) {
cmd = "dpkg -l ovmf";
s_logger.debug("Running command : dpkg -l ovmf");
result = Script.executeCommandForExitValue(Script.getExecutableAbsolutePath("dpkg"), "-l", "ovmf");
} else {
s_logger.debug("Running command : rpm -qa | grep -i ovmf");
List<String[]> commands = new ArrayList<>();
commands.add(new String[]{Script.getExecutableAbsolutePath("rpm"), "-qa"});
commands.add(new String[]{Script.getExecutableAbsolutePath("grep"), "-i", "ovmf"});
result = Script.executePipedCommands(commands, 0).first();
}
s_logger.debug("Running command : " + cmd);
int result = Script.runSimpleBashScriptForExitValue(cmd);
s_logger.debug("Got result : " + result);
return result == 0;
}

View File

@ -19,6 +19,15 @@
package com.cloud.hypervisor.kvm.resource.wrapper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.apache.cloudstack.agent.directdownload.RevokeDirectDownloadCertificateCommand;
import org.apache.cloudstack.utils.security.KeyStoreUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
@ -26,14 +35,6 @@ import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;
import org.apache.cloudstack.agent.directdownload.RevokeDirectDownloadCertificateCommand;
import org.apache.cloudstack.utils.security.KeyStoreUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@ResourceWrapper(handles = RevokeDirectDownloadCertificateCommand.class)
public class LibvirtRevokeDirectDownloadCertificateWrapper extends CommandWrapper<RevokeDirectDownloadCertificateCommand, Answer, LibvirtComputingResource> {
@ -84,17 +85,17 @@ public class LibvirtRevokeDirectDownloadCertificateWrapper extends CommandWrappe
}
final String keyStoreFile = getKeyStoreFilePath(agentFile);
String checkCmd = String.format("keytool -list -alias %s -keystore %s -storepass %s",
certificateAlias, keyStoreFile, privatePassword);
int existsCmdResult = Script.runSimpleBashScriptForExitValue(checkCmd);
String keyToolPath = Script.getExecutableAbsolutePath("keytool");
int existsCmdResult = Script.executeCommandForExitValue(keyToolPath, "-list", "-alias",
sanitizeBashCommandArgument(certificateAlias), "-keystore", keyStoreFile, "-storepass",
privatePassword);
if (existsCmdResult == 1) {
s_logger.error("Certificate alias " + certificateAlias + " does not exist, no need to revoke it");
} else {
String revokeCmd = String.format("keytool -delete -alias %s -keystore %s -storepass %s",
certificateAlias, keyStoreFile, privatePassword);
s_logger.debug("Revoking certificate alias " + certificateAlias + " from keystore " + keyStoreFile);
Script.runSimpleBashScriptForExitValue(revokeCmd);
Script.executeCommandForExitValue(keyToolPath, "-delete", "-alias",
sanitizeBashCommandArgument(certificateAlias), "-keystore", keyStoreFile, "-storepass",
privatePassword);;
}
} catch (FileNotFoundException | CloudRuntimeException e) {
s_logger.error("Error while setting up certificate " + certificateAlias, e);

View File

@ -18,21 +18,26 @@
//
package com.cloud.hypervisor.kvm.resource.wrapper;
import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.cloudstack.agent.directdownload.SetupDirectDownloadCertificateCommand;
import org.apache.cloudstack.utils.security.KeyStoreUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.FileUtil;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;
@ResourceWrapper(handles = SetupDirectDownloadCertificateCommand.class)
public class LibvirtSetupDirectDownloadCertificateCommandWrapper extends CommandWrapper<SetupDirectDownloadCertificateCommand, Answer, LibvirtComputingResource> {
@ -79,9 +84,10 @@ 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

View File

@ -23,28 +23,27 @@ import java.io.File;
import java.util.List;
import java.util.Map;
import com.cloud.agent.api.to.DpdkTO;
import com.cloud.hypervisor.kvm.resource.LibvirtKvmAgentHook;
import com.cloud.utils.Pair;
import com.cloud.utils.script.Script;
import com.cloud.utils.ssh.SshHelper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.log4j.Logger;
import org.libvirt.Connect;
import org.libvirt.Domain;
import org.libvirt.DomainInfo.DomainState;
import org.libvirt.LibvirtException;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.StopAnswer;
import com.cloud.agent.api.StopCommand;
import com.cloud.agent.api.to.DpdkTO;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.hypervisor.kvm.resource.LibvirtKvmAgentHook;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef;
import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef;
import com.cloud.hypervisor.kvm.resource.VifDriver;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import org.libvirt.LibvirtException;
import com.cloud.utils.Pair;
import com.cloud.utils.ssh.SshHelper;
@ResourceWrapper(handles = StopCommand.class)
public final class LibvirtStopCommandWrapper extends CommandWrapper<StopCommand, Answer, LibvirtComputingResource> {
@ -121,10 +120,7 @@ public final class LibvirtStopCommandWrapper extends CommandWrapper<StopCommand,
Map<String, DpdkTO> dpdkInterfaceMapping = command.getDpdkInterfaceMapping();
if (MapUtils.isNotEmpty(dpdkInterfaceMapping)) {
for (DpdkTO to : dpdkInterfaceMapping.values()) {
String portToRemove = to.getPort();
String cmd = String.format("ovs-vsctl del-port %s", portToRemove);
s_logger.debug("Removing DPDK port: " + portToRemove);
Script.runSimpleBashScript(cmd);
removeDpdkPort(to.getPort());
}
}
} else {

View File

@ -0,0 +1,105 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.hypervisor.kvm.resource.wrapper;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.List;
import java.util.UUID;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Spy;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import com.cloud.utils.StringUtils;
import com.cloud.utils.Ternary;
@RunWith(PowerMockRunner.class)
@PrepareForTest(value = {LibvirtOvsFetchInterfaceCommandWrapper.class})
public class LibvirtOvsFetchInterfaceCommandWrapperTest {
@Spy
LibvirtOvsFetchInterfaceCommandWrapper wrapper = new LibvirtOvsFetchInterfaceCommandWrapper();
@Test
public void testGetInterfaceDetailsValidValid() {
String interfaceName = null;
String ipAddress = null;
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while(interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
if (networkInterface.getInetAddresses().hasMoreElements() &&
(networkInterface.getName().startsWith("eth") ||
networkInterface.getName().startsWith("wl"))) {
interfaceName = networkInterface.getName();
Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
while(addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if (addr instanceof Inet4Address) {
ipAddress = addr.getHostAddress();
break;
};
}
}
}
} catch (SocketException ignored) {}
Ternary<String, String, String> result = null;
try {
result = wrapper.getInterfaceDetails(interfaceName);
} catch (SocketException e) {
Assert.fail("Exception occurred: " + e.getMessage());
}
Assert.assertNotNull(result);
Assert.assertEquals(ipAddress, result.first().trim());
}
private String getTempFilepath() {
return String.format("%s/%s.txt", System.getProperty("java.io.tmpdir"), UUID.randomUUID());
}
private void runTestGetInterfaceDetailsForRandomInterfaceName(String arg) {
try {
Ternary<String, String, String> result = wrapper.getInterfaceDetails(arg);
Assert.assertTrue(StringUtils.isAllEmpty(result.first(), result.second(), result.third()));
} catch (SocketException e) {
Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
}
}
@Test
public void testGetInterfaceDetailsForRandomInterfaceName() {
List<String> commandVariants = List.of(
"';touch %s'",
";touch %s",
"&& touch %s",
"|| touch %s",
UUID.randomUUID().toString());
for (String cmd : commandVariants) {
String filePath = getTempFilepath();
String arg = String.format(cmd, filePath);
runTestGetInterfaceDetailsForRandomInterfaceName(arg);
}
}
}

View File

@ -1,4 +1,3 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
@ -15,15 +14,14 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
package com.cloud.hypervisor.kvm.resource.wrapper;
import com.cloud.agent.api.PrepareForMigrationAnswer;
import com.cloud.agent.api.PrepareForMigrationCommand;
import com.cloud.agent.api.to.DpdkTO;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -33,8 +31,12 @@ import org.mockito.Spy;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.util.HashMap;
import java.util.Map;
import com.cloud.agent.api.PrepareForMigrationAnswer;
import com.cloud.agent.api.PrepareForMigrationCommand;
import com.cloud.agent.api.to.DpdkTO;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.utils.exception.CloudRuntimeException;
@RunWith(PowerMockRunner.class)
@PrepareForTest(value = {LibvirtPrepareForMigrationCommandWrapper.class})
@ -72,4 +74,29 @@ public class LibvirtPrepareForMigrationCommandWrapperTest {
Assert.assertEquals(cpuShares, prepareForMigrationAnswer.getNewVmCpuShares().intValue());
}
private String getTempFilepath() {
return String.format("%s/%s.txt", System.getProperty("java.io.tmpdir"), UUID.randomUUID());
}
private void runTestRemoveDpdkPortForCommandInjection(String portWithCommand) {
try {
libvirtPrepareForMigrationCommandWrapperSpy.removeDpdkPort(portWithCommand);
Assert.fail(String.format("Command injection working for portWithCommand: %s", portWithCommand));
} catch (CloudRuntimeException ignored) {}
}
@Test
public void testRemoveDpdkPortForCommandInjection() {
List<String> commandVariants = List.of(
"';touch %s'",
";touch %s",
"&& touch %s",
"|| touch %s",
UUID.randomUUID().toString());
for (String cmd : commandVariants) {
String portWithCommand = String.format(cmd, getTempFilepath());
runTestRemoveDpdkPortForCommandInjection(portWithCommand);
}
}
}

View File

@ -0,0 +1,93 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
package com.cloud.hypervisor.kvm.resource.wrapper;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Spy;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
@PrepareForTest(value = {LibvirtSetupDirectDownloadCertificateCommandWrapper.class})
public class LibvirtSetupDirectDownloadCertificateCommandWrapperTest {
@Spy
LibvirtSetupDirectDownloadCertificateCommandWrapper wrapper = new LibvirtSetupDirectDownloadCertificateCommandWrapper();
private String getTempFilepath() {
return String.format("%s/%s.txt", System.getProperty("java.io.tmpdir"), UUID.randomUUID());
}
private void runTestCleanupTemporaryFileForRandomFileNames(String fileWithCommand, String filePath) {
wrapper.cleanupTemporaryFile(fileWithCommand);
File f = new File(filePath);
if(f.exists() && !f.isDirectory()) {
Assert.fail(String.format("Command injection working for fileWithCommand: %s", fileWithCommand));
}
}
@Test
public void testCleanupTemporaryFileForRandomFileNames() {
List<String> commandVariants = List.of(
"';touch %s'",
";touch %s",
"&& touch %s",
"|| touch %s",
"%s");
for (String cmd : commandVariants) {
String filePath = getTempFilepath();
String arg = String.format(cmd, filePath);
runTestCleanupTemporaryFileForRandomFileNames(arg, filePath);
}
}
private String createTempFile() {
String filePath = getTempFilepath();
Path path = Paths.get(getTempFilepath());
try {
if (Files.notExists(path)) {
Files.createFile(path);
}
} catch (IOException e) {
Assert.fail(String.format("Error while creating file: %s due to %s", filePath, e.getMessage()));
}
return filePath;
}
@Test
public void testCleanupTemporaryFileValid() {
String filePath = createTempFile();
wrapper.cleanupTemporaryFile(filePath);
File f = new File(filePath);
if(f.exists() && !f.isDirectory()) {
Assert.fail(String.format("Command injection working for fileWithCommand: %s", filePath));
}
}
}

View File

@ -0,0 +1,63 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.hypervisor.kvm.resource.wrapper;
import java.util.List;
import java.util.UUID;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Spy;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import com.cloud.utils.exception.CloudRuntimeException;
@RunWith(PowerMockRunner.class)
@PrepareForTest(value = {LibvirtStopCommandWrapper.class})
public class LibvirtStopCommandWrapperTest {
@Spy
LibvirtStopCommandWrapper wrapper = new LibvirtStopCommandWrapper();
private String getTempFilepath() {
return String.format("%s/%s.txt", System.getProperty("java.io.tmpdir"), UUID.randomUUID());
}
private void runTestRemoveDpdkPortForCommandInjection(String portWithCommand) {
try {
wrapper.removeDpdkPort(portWithCommand);
Assert.fail(String.format("Command injection working for portWithCommand: %s", portWithCommand));
} catch (CloudRuntimeException ignored) {}
}
@Test
public void testRemoveDpdkPortForCommandInjection() {
List<String> commandVariants = List.of(
"';touch %s'",
";touch %s",
"&& touch %s",
"|| touch %s",
UUID.randomUUID().toString());
for (String cmd : commandVariants) {
String portWithCommand = String.format(cmd, getTempFilepath());
runTestRemoveDpdkPortForCommandInjection(portWithCommand);
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -0,0 +1,73 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.api;
import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
@PrepareForTest(ApiServer.class)
public class ApiServerTest {
@InjectMocks
ApiServer apiServer = new ApiServer();
private List<ApiServer.ListenerThread> createdListeners;
private void runTestSetupIntegrationPortListenerInvalidPorts(Integer port) {
try {
ApiServer.ListenerThread mocked = Mockito.mock(ApiServer.ListenerThread.class);
PowerMockito.whenNew(ApiServer.ListenerThread.class).withAnyArguments().thenReturn(mocked);
apiServer.setupIntegrationPortListener(port);
Mockito.verify(mocked, Mockito.never()).start();
} catch (Exception e) {
Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
}
}
@Test
public void testSetupIntegrationPortListenerInvalidPorts() {
List<Integer> ports = new ArrayList<>(List.of(-1, -10, 0));
ports.add(null);
for (Integer port : ports) {
runTestSetupIntegrationPortListenerInvalidPorts(port);
}
}
@Test
public void testSetupIntegrationPortListenerValidPort() {
Integer validPort = 8080;
try {
ApiServer.ListenerThread mocked = Mockito.mock(ApiServer.ListenerThread.class);
PowerMockito.whenNew(ApiServer.ListenerThread.class).withAnyArguments().thenReturn(mocked);
apiServer.setupIntegrationPortListener(validPort);
PowerMockito.verifyNew(ApiServer.ListenerThread.class).withArguments(apiServer, validPort);
Mockito.verify(mocked).start();
} catch (Exception e) {
Assert.fail(String.format("Exception occurred: %s", e.getMessage()));
}
}
}

View File

@ -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;
}
}

View File

@ -19,14 +19,6 @@
package com.cloud.utils.script;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.concurrency.NamedThreadFactory;
import com.cloud.utils.script.OutputInterpreter.TimedOutLogger;
import org.apache.cloudstack.utils.security.KeyStoreUtils;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.joda.time.Duration;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
@ -40,10 +32,24 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.apache.cloudstack.utils.security.KeyStoreUtils;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.joda.time.Duration;
import com.cloud.utils.Pair;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.concurrency.NamedThreadFactory;
import com.cloud.utils.script.OutputInterpreter.TimedOutLogger;
public class Script implements Callable<String> {
private static final Logger s_logger = Logger.getLogger(Script.class);
@ -52,7 +58,7 @@ public class Script implements Callable<String> {
public static final String ERR_EXECUTE = "execute.error";
public static final String ERR_TIMEOUT = "timeout";
private int _defaultTimeout = 3600 * 1000; /* 1 hour */
private static final int DEFAULT_TIMEOUT = 3600 * 1000; /* 1 hour */
private volatile boolean _isTimeOut = false;
private boolean _passwordCommand = false;
@ -84,7 +90,7 @@ public class Script implements Callable<String> {
_timeout = timeout;
if (_timeout == 0) {
/* always using default timeout 1 hour to avoid thread hang */
_timeout = _defaultTimeout;
_timeout = DEFAULT_TIMEOUT;
}
_process = null;
_logger = logger != null ? logger : s_logger;
@ -493,16 +499,7 @@ public class Script implements Callable<String> {
return null;
}
public static String runSimpleBashScript(String command) {
return Script.runSimpleBashScript(command, 0);
}
public static String runSimpleBashScript(String command, int timeout) {
Script s = new Script("/bin/bash", timeout);
s.add("-c");
s.add(command);
private static String runScript(Script s) {
OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser();
if (s.execute(parser) != null)
return null;
@ -514,16 +511,83 @@ public class Script implements Callable<String> {
return result.trim();
}
public static int runSimpleBashScriptForExitValue(String command) {
return runSimpleBashScriptForExitValue(command, 0);
}
public static int runSimpleBashScriptForExitValue(String command, int timeout) {
public static String runSimpleBashScript(String command, int timeout) {
Script s = new Script("/bin/bash", timeout);
s.add("-c");
s.add(command);
return runScript(s);
}
public static String runSimpleBashScript(String command) {
return Script.runSimpleBashScript(command, 0);
}
public static String getExecutableAbsolutePath(String executable) {
for (String dirName : System.getenv("PATH").split(File.pathSeparator)) {
File file = new File(dirName, executable);
if (file.isFile() && file.canExecute()) {
return file.getAbsolutePath();
}
}
return executable;
}
private static Script getScriptForCommandRun(String... command) {
Script s = new Script(command[0], 0);
if (command.length > 1) {
for (int i = 1; i < command.length; ++i) {
s.add(command[i]);
}
}
return s;
}
public static String executeCommand(String... command) {
return runScript(getScriptForCommandRun(command));
}
public static int executeCommandForExitValue(String... command) {
return runScriptForExitValue(getScriptForCommandRun(command));
}
public static Pair<Integer, String> executePipedCommands(List<String[]> commands, long timeout) {
if (timeout <= 0) {
timeout = DEFAULT_TIMEOUT;
}
Callable<Pair<Integer, String>> commandRunner = () -> {
List<ProcessBuilder> builders = commands.stream().map(ProcessBuilder::new).collect(Collectors.toList());
List<Process> processes = ProcessBuilder.startPipeline(builders);
Process last = processes.get(processes.size()-1);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(last.getInputStream()))) {
String line;
StringBuilder output = new StringBuilder();
while ((line = reader.readLine()) != null) {
output.append(line).append(System.lineSeparator());
}
last.waitFor();
s_logger.debug("Piped commands executed successfully");
return new Pair<>(last.exitValue(), output.toString());
} catch (IOException | InterruptedException e) {
s_logger.error("Error executing piped commands", e);
return new Pair<>(-1, stackTraceAsString(e));
}
};
Future<Pair<Integer, String>> future = s_executors.submit(commandRunner);
Pair<Integer, String> result = new Pair<>(-1, ERR_EXECUTE);
try {
result = future.get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
s_logger.error("Piped command execution timed out, attempting to terminate the processes.");
future.cancel(true);
result.second(ERR_TIMEOUT);
} catch (InterruptedException | ExecutionException e) {
s_logger.error("Error executing piped commands", e);
}
return result;
}
private static int runScriptForExitValue(Script s) {
String result = s.execute(null);
if (result == null || result.trim().isEmpty())
return -1;
@ -536,4 +600,14 @@ public class Script implements Callable<String> {
}
}
public static int runSimpleBashScriptForExitValue(String command) {
return runSimpleBashScriptForExitValue(command, 0);
}
public static int runSimpleBashScriptForExitValue(String command, int timeout) {
Script s = new Script("/bin/bash", timeout);
s.add("-c");
s.add(command);
return runScriptForExitValue(s);
}
}

View File

@ -0,0 +1,81 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.utils.script;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.utils.Pair;
@RunWith(MockitoJUnitRunner.class)
public class ScriptTest {
@Test
public void testExecutePipedCommandsSingle() {
String keyword = "Hello World!";
List<String[]> commands = new ArrayList<>();
commands.add(new String[]{"echo", keyword});
Pair<Integer, String> result = Script.executePipedCommands(commands, 0);
Assert.assertNotNull("Result should not be null", result);
Assert.assertEquals(0, result.first().intValue());
String output = result.second().trim();
Assert.assertTrue(StringUtils.isNotEmpty(output));
Assert.assertEquals(keyword, output);
}
@Test
public void testExecutePipedCommandsMultiple() {
String keyword = "Hello";
List<String[]> commands = Arrays.asList(
new String[]{"echo", String.format("%s\n World", keyword)},
new String[]{"grep", keyword}
);
Pair<Integer, String> result = Script.executePipedCommands(commands, 0);
Assert.assertNotNull("Result should not be null", result);
Assert.assertEquals(0, result.first().intValue());
String output = result.second().trim();
Assert.assertTrue(StringUtils.isNotEmpty(output));
Assert.assertEquals(keyword, output);
}
@Test
public void testExecutePipedCommandsTimeout() {
List<String[]> commands = new ArrayList<>();
commands.add(new String[]{"sh", "-c", "sleep 10"});
Pair<Integer, String> result = Script.executePipedCommands(commands, TimeUnit.SECONDS.toMillis(1));
Assert.assertNotNull("Result should not be null", result);
Assert.assertEquals(-1, result.first().intValue());
Assert.assertEquals(Script.ERR_TIMEOUT, result.second());
}
@Test
public void testGetExecutableAbsolutePath() {
if (System.getProperty("os.name").startsWith("Windows")) {
return;
}
String result = Script.getExecutableAbsolutePath("ls");
Assert.assertTrue(List.of("/usr/bin/ls", "/bin/ls").contains(result));
}
}