diff --git a/agent/src/com/cloud/agent/Agent.java b/agent/src/com/cloud/agent/Agent.java index ac2d9ba29cc..4cdc4996629 100755 --- a/agent/src/com/cloud/agent/Agent.java +++ b/agent/src/com/cloud/agent/Agent.java @@ -16,12 +16,14 @@ // under the License. package com.cloud.agent; +import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.channels.ClosedChannelException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -35,9 +37,15 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.naming.ConfigurationException; -import org.apache.log4j.Logger; - +import org.apache.cloudstack.ca.SetupCertificateAnswer; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; +import org.apache.cloudstack.ca.SetupKeystoreAnswer; import org.apache.cloudstack.managed.context.ManagedContextTimerTask; +import org.apache.cloudstack.utils.security.KeyStoreUtils; +import org.apache.commons.io.FileUtils; +import org.apache.log4j.Logger; +import org.slf4j.MDC; import com.cloud.agent.api.AgentControlAnswer; import com.cloud.agent.api.AgentControlCommand; @@ -59,6 +67,8 @@ import com.cloud.utils.PropertiesUtil; import com.cloud.utils.backoff.BackoffAlgorithm; import com.cloud.utils.concurrency.NamedThreadFactory; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.exception.NioConnectionException; +import com.cloud.utils.exception.TaskExecutionException; import com.cloud.utils.nio.HandlerFactory; import com.cloud.utils.nio.Link; import com.cloud.utils.nio.NioClient; @@ -66,6 +76,7 @@ import com.cloud.utils.nio.NioConnection; import com.cloud.utils.nio.Task; import com.cloud.utils.script.OutputInterpreter; import com.cloud.utils.script.Script; +import com.google.common.base.Strings; /** * @config @@ -121,11 +132,14 @@ public class Agent implements HandlerFactory, IAgentControl { long _startupWait = _startupWaitDefault; boolean _reconnectAllowed = true; //For time sentitive task, e.g. PingTask - private ThreadPoolExecutor _ugentTaskPool; + private final ThreadPoolExecutor _ugentTaskPool; ExecutorService _executor; + private String _keystoreSetupPath; + private String _keystoreCertImportPath; + // for simulator use only - public Agent(IAgentShell shell) { + public Agent(final IAgentShell shell) { _shell = shell; _link = null; @@ -134,29 +148,29 @@ public class Agent implements HandlerFactory, IAgentControl { Runtime.getRuntime().addShutdownHook(new ShutdownThread(this)); _ugentTaskPool = - new ThreadPoolExecutor(shell.getPingRetries(), 2 * shell.getPingRetries(), 10, TimeUnit.MINUTES, new SynchronousQueue(), new NamedThreadFactory( - "UgentTask")); + new ThreadPoolExecutor(shell.getPingRetries(), 2 * shell.getPingRetries(), 10, TimeUnit.MINUTES, new SynchronousQueue(), new NamedThreadFactory( + "UgentTask")); _executor = - new ThreadPoolExecutor(_shell.getWorkers(), 5 * _shell.getWorkers(), 1, TimeUnit.DAYS, new LinkedBlockingQueue(), new NamedThreadFactory( - "agentRequest-Handler")); + new ThreadPoolExecutor(_shell.getWorkers(), 5 * _shell.getWorkers(), 1, TimeUnit.DAYS, new LinkedBlockingQueue(), new NamedThreadFactory( + "agentRequest-Handler")); } - public Agent(IAgentShell shell, int localAgentId, ServerResource resource) throws ConfigurationException { + public Agent(final IAgentShell shell, final int localAgentId, final ServerResource resource) throws ConfigurationException { _shell = shell; _resource = resource; _link = null; resource.setAgentControl(this); - String value = _shell.getPersistentProperty(getResourceName(), "id"); + final String value = _shell.getPersistentProperty(getResourceName(), "id"); _id = value != null ? Long.parseLong(value) : null; - s_logger.info("id is " + ((_id != null) ? _id : "")); + s_logger.info("id is " + (_id != null ? _id : "")); final Map params = PropertiesUtil.toMap(_shell.getProperties()); // merge with properties from command line to let resource access command line parameters - for (Map.Entry cmdLineProp : _shell.getCmdLineProperties().entrySet()) { + for (final Map.Entry cmdLineProp : _shell.getCmdLineProperties().entrySet()) { params.put(cmdLineProp.getKey(), cmdLineProp.getValue()); } @@ -164,7 +178,8 @@ public class Agent implements HandlerFactory, IAgentControl { throw new ConfigurationException("Unable to configure " + _resource.getName()); } - _connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this); + final String host = _shell.getHost(); + _connection = new NioClient("Agent", host, _shell.getPort(), _shell.getWorkers(), this); // ((NioClient)_connection).setBindAddress(_shell.getPrivateIp()); @@ -172,15 +187,15 @@ public class Agent implements HandlerFactory, IAgentControl { Runtime.getRuntime().addShutdownHook(new ShutdownThread(this)); _ugentTaskPool = - new ThreadPoolExecutor(shell.getPingRetries(), 2 * shell.getPingRetries(), 10, TimeUnit.MINUTES, new SynchronousQueue(), new NamedThreadFactory( - "UgentTask")); + new ThreadPoolExecutor(shell.getPingRetries(), 2 * shell.getPingRetries(), 10, TimeUnit.MINUTES, new SynchronousQueue(), new NamedThreadFactory( + "UgentTask")); _executor = - new ThreadPoolExecutor(_shell.getWorkers(), 5 * _shell.getWorkers(), 1, TimeUnit.DAYS, new LinkedBlockingQueue(), new NamedThreadFactory( - "agentRequest-Handler")); + new ThreadPoolExecutor(_shell.getWorkers(), 5 * _shell.getWorkers(), 1, TimeUnit.DAYS, new LinkedBlockingQueue(), new NamedThreadFactory( + "agentRequest-Handler")); s_logger.info("Agent [id = " + (_id != null ? _id : "new") + " : type = " + getResourceName() + " : zone = " + _shell.getZone() + " : pod = " + _shell.getPod() + - " : workers = " + _shell.getWorkers() + " : host = " + _shell.getHost() + " : port = " + _shell.getPort()); + " : workers = " + _shell.getWorkers() + " : host = " + host + " : port = " + _shell.getPort()); } public String getVersion() { @@ -188,7 +203,7 @@ public class Agent implements HandlerFactory, IAgentControl { } public String getResourceGuid() { - String guid = _shell.getGuid(); + final String guid = _shell.getGuid(); return guid + "-" + getResourceName(); } @@ -222,11 +237,31 @@ public class Agent implements HandlerFactory, IAgentControl { throw new CloudRuntimeException("Unable to start the resource: " + _resource.getName()); } - _connection.start(); + _keystoreSetupPath = Script.findScript("scripts/util/", KeyStoreUtils.keyStoreSetupScript); + if (_keystoreSetupPath == null) { + throw new CloudRuntimeException(String.format("Unable to find the '%s' script", KeyStoreUtils.keyStoreSetupScript)); + } + + _keystoreCertImportPath = Script.findScript("scripts/util/", KeyStoreUtils.keyStoreImportScript); + if (_keystoreCertImportPath == null) { + throw new CloudRuntimeException(String.format("Unable to find the '%s' script", KeyStoreUtils.keyStoreImportScript)); + } + + try { + _connection.start(); + } catch (final NioConnectionException e) { + s_logger.warn("NIO Connection Exception " + e); + s_logger.info("Attempted to connect to the server, but received an unexpected exception, trying again..."); + } while (!_connection.isStartup()) { _shell.getBackoffAlgorithm().waitBeforeRetry(); _connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this); - _connection.start(); + try { + _connection.start(); + } catch (final NioConnectionException e) { + s_logger.warn("NIO Connection Exception " + e); + s_logger.info("Attempted to connect to the server, but received an unexpected exception, trying again..."); + } } } @@ -236,12 +271,12 @@ public class Agent implements HandlerFactory, IAgentControl { final ShutdownCommand cmd = new ShutdownCommand(reason, detail); try { if (_link != null) { - Request req = new Request((_id != null ? _id : -1), -1, cmd, false); + final Request req = new Request(_id != null ? _id : -1, -1, cmd, false); _link.send(req.toBytes()); } } catch (final ClosedChannelException e) { s_logger.warn("Unable to send: " + cmd.toString()); - } catch (Exception e) { + } catch (final Exception e) { s_logger.warn("Unable to send: " + cmd.toString() + " due to exception: ", e); } s_logger.debug("Sending shutdown to management server"); @@ -294,13 +329,13 @@ public class Agent implements HandlerFactory, IAgentControl { _watchList.clear(); } } - public synchronized void lockStartupTask(Link link) + public synchronized void lockStartupTask(final Link link) { _startup = new StartupTask(link); _timer.schedule(_startup, _startupWait); } - public void sendStartup(Link link) { + public void sendStartup(final Link link) { final StartupCommand[] startup = _resource.initialize(); if (startup != null) { final Command[] commands = new Command[startup.length]; @@ -323,7 +358,7 @@ public class Agent implements HandlerFactory, IAgentControl { } } - protected void setupStartupCommand(StartupCommand startup) { + protected void setupStartupCommand(final StartupCommand startup) { InetAddress addr; try { addr = InetAddress.getLocalHost(); @@ -349,7 +384,7 @@ public class Agent implements HandlerFactory, IAgentControl { } @Override - public Task create(Task.Type type, Link link, byte[] data) { + public Task create(final Task.Type type, final Link link, final byte[] data) { return new ServerHandler(type, link, data); } @@ -385,25 +420,38 @@ public class Agent implements HandlerFactory, IAgentControl { } while (inProgress > 0); _connection.stop(); + + try { + _connection.cleanUp(); + } catch (final IOException e) { + s_logger.warn("Fail to clean up old connection. " + e); + } + while (_connection.isStartup()) { _shell.getBackoffAlgorithm().waitBeforeRetry(); } - try { - _connection.cleanUp(); - } catch (IOException e) { - s_logger.warn("Fail to clean up old connection. " + e); - } - _connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this); do { + _connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this); s_logger.info("Reconnecting..."); - _connection.start(); + try { + _connection.start(); + } catch (final NioConnectionException e) { + s_logger.warn("NIO Connection Exception " + e); + s_logger.info("Attempted to connect to the server, but received an unexpected exception, trying again..."); + _connection.stop(); + try { + _connection.cleanUp(); + } catch (final IOException ex) { + s_logger.warn("Fail to clean up old connection. " + ex); + } + } _shell.getBackoffAlgorithm().waitBeforeRetry(); } while (!_connection.isStartup()); s_logger.info("Connected to the server"); } - public void processStartupAnswer(Answer answer, Response response, Link link) { + public void processStartupAnswer(final Answer answer, final Response response, final Link link) { boolean cancelled = false; synchronized (this) { if (_startup != null) { @@ -445,12 +493,15 @@ public class Agent implements HandlerFactory, IAgentControl { for (int i = 0; i < cmds.length; i++) { final Command cmd = cmds[i]; - Answer answer; + Answer answer = null; try { + if (cmd.getContextParam("logid") != null) { + MDC.put("logcontextid", cmd.getContextParam("logid")); + } if (s_logger.isDebugEnabled()) { if (!requestLogged) // ensures request is logged only once per method call { - String requestMsg = request.toString(); + final String requestMsg = request.toString(); if (requestMsg != null) { s_logger.debug("Request:" + requestMsg); } @@ -464,7 +515,7 @@ public class Agent implements HandlerFactory, IAgentControl { scheduleWatch(link, request, (long)watch.getInterval() * 1000, watch.getInterval() * 1000); answer = new Answer(cmd, true, null); } else if (cmd instanceof ShutdownCommand) { - ShutdownCommand shutdown = (ShutdownCommand)cmd; + final ShutdownCommand shutdown = (ShutdownCommand)cmd; s_logger.debug("Received shutdownCommand, due to: " + shutdown.getReason()); cancelTasks(); _reconnectAllowed = false; @@ -481,7 +532,7 @@ public class Agent implements HandlerFactory, IAgentControl { } else if (cmd instanceof AgentControlCommand) { answer = null; synchronized (_controlListeners) { - for (IAgentControlListener listener : _controlListeners) { + for (final IAgentControlListener listener : _controlListeners) { answer = listener.processControlRequest(request, (AgentControlCommand)cmd); if (answer != null) { break; @@ -493,7 +544,10 @@ public class Agent implements HandlerFactory, IAgentControl { s_logger.warn("No handler found to process cmd: " + cmd.toString()); answer = new AgentControlAnswer(cmd); } - + } else if (cmd instanceof SetupKeyStoreCommand && ((SetupKeyStoreCommand) cmd).isHandleByAgent()) { + answer = setupAgentKeystore((SetupKeyStoreCommand) cmd); + } else if (cmd instanceof SetupCertificateCommand && ((SetupCertificateCommand) cmd).isHandleByAgent()) { + answer = setupAgentCertificate((SetupCertificateCommand) cmd); } else { if (cmd instanceof ReadyCommand) { processReadyCommand(cmd); @@ -527,7 +581,7 @@ public class Agent implements HandlerFactory, IAgentControl { response = new Response(request, answers); } finally { if (s_logger.isDebugEnabled()) { - String responseMsg = response.toString(); + final String responseMsg = response.toString(); if (responseMsg != null) { s_logger.debug(response.toString()); } @@ -543,6 +597,86 @@ public class Agent implements HandlerFactory, IAgentControl { } } + public Answer setupAgentKeystore(final SetupKeyStoreCommand cmd) { + final String keyStorePassword = cmd.getKeystorePassword(); + final long validityDays = cmd.getValidityDays(); + + s_logger.debug("Setting up agent keystore file and generating CSR"); + + final File agentFile = PropertiesUtil.findConfigFile("agent.properties"); + if (agentFile == null) { + return new Answer(cmd, false, "Failed to find agent.properties file"); + } + final String keyStoreFile = agentFile.getParent() + KeyStoreUtils.defaultKeystoreFile; + final String csrFile = agentFile.getParent() + KeyStoreUtils.defaultCsrFile; + + String storedPassword = _shell.getPersistentProperty(null, KeyStoreUtils.passphrasePropertyName); + if (Strings.isNullOrEmpty(storedPassword)) { + storedPassword = keyStorePassword; + _shell.setPersistentProperty(null, KeyStoreUtils.passphrasePropertyName, storedPassword); + } + + Script script = new Script(_keystoreSetupPath, 60000, s_logger); + script.add(agentFile.getAbsolutePath()); + script.add(keyStoreFile); + script.add(storedPassword); + script.add(String.valueOf(validityDays)); + script.add(csrFile); + String result = script.execute(); + if (result != null) { + throw new CloudRuntimeException("Unable to setup keystore file"); + } + + final String csrString; + try { + csrString = FileUtils.readFileToString(new File(csrFile), Charset.defaultCharset()); + } catch (IOException e) { + throw new CloudRuntimeException("Unable to read generated CSR file", e); + } + return new SetupKeystoreAnswer(csrString); + } + + private Answer setupAgentCertificate(final SetupCertificateCommand cmd) { + final String certificate = cmd.getCertificate(); + final String privateKey = cmd.getPrivateKey(); + final String caCertificates = cmd.getCaCertificates(); + + s_logger.debug("Importing received certificate to agent's keystore"); + + final File agentFile = PropertiesUtil.findConfigFile("agent.properties"); + if (agentFile == null) { + return new Answer(cmd, false, "Failed to find agent.properties file"); + } + final String keyStoreFile = agentFile.getParent() + KeyStoreUtils.defaultKeystoreFile; + final String certFile = agentFile.getParent() + KeyStoreUtils.defaultCertFile; + final String privateKeyFile = agentFile.getParent() + KeyStoreUtils.defaultPrivateKeyFile; + final String caCertFile = agentFile.getParent() + KeyStoreUtils.defaultCaCertFile; + + try { + FileUtils.writeStringToFile(new File(certFile), certificate, Charset.defaultCharset()); + FileUtils.writeStringToFile(new File(caCertFile), caCertificates, Charset.defaultCharset()); + s_logger.debug("Saved received client certificate to: " + certFile); + } catch (IOException e) { + throw new CloudRuntimeException("Unable to save received agent client and ca certificates", e); + } + + Script script = new Script(_keystoreCertImportPath, 60000, s_logger); + script.add(agentFile.getAbsolutePath()); + script.add(keyStoreFile); + script.add(KeyStoreUtils.agentMode); + script.add(certFile); + script.add(""); + script.add(caCertFile); + script.add(""); + script.add(privateKeyFile); + script.add(privateKey); + String result = script.execute(); + if (result != null) { + throw new CloudRuntimeException("Unable to import certificate into keystore file"); + } + return new SetupCertificateAnswer(true); + } + public void processResponse(final Response response, final Link link) { final Answer answer = response.getAnswer(); if (s_logger.isDebugEnabled()) { @@ -553,7 +687,7 @@ public class Agent implements HandlerFactory, IAgentControl { } else if (answer instanceof AgentControlAnswer) { // Notice, we are doing callback while holding a lock! synchronized (_controlListeners) { - for (IAgentControlListener listener : _controlListeners) { + for (final IAgentControlListener listener : _controlListeners) { listener.processControlResponse(response, (AgentControlAnswer)answer); } } @@ -562,7 +696,7 @@ public class Agent implements HandlerFactory, IAgentControl { } } - public void processReadyCommand(Command cmd) { + public void processReadyCommand(final Command cmd) { final ReadyCommand ready = (ReadyCommand)cmd; @@ -574,10 +708,10 @@ public class Agent implements HandlerFactory, IAgentControl { } - public void processOtherTask(Task task) { + public void processOtherTask(final Task task) { final Object obj = task.get(); if (obj instanceof Response) { - if ((System.currentTimeMillis() - _lastPingResponseTime) > _pingInterval * _shell.getPingRetries()) { + if (System.currentTimeMillis() - _lastPingResponseTime > _pingInterval * _shell.getPingRetries()) { s_logger.error("Ping Interval has gone past " + _pingInterval * _shell.getPingRetries() + ". Won't reconnect to mgt server, as connection is still alive"); return; } @@ -600,6 +734,9 @@ public class Agent implements HandlerFactory, IAgentControl { } else if (obj instanceof Request) { final Request req = (Request)obj; final Command command = req.getCommand(); + if (command.getContextParam("logid") != null) { + MDC.put("logcontextid", command.getContextParam("logid")); + } Answer answer = null; _inProgress.incrementAndGet(); try { @@ -633,25 +770,25 @@ public class Agent implements HandlerFactory, IAgentControl { } @Override - public void registerControlListener(IAgentControlListener listener) { + public void registerControlListener(final IAgentControlListener listener) { synchronized (_controlListeners) { _controlListeners.add(listener); } } @Override - public void unregisterControlListener(IAgentControlListener listener) { + public void unregisterControlListener(final IAgentControlListener listener) { synchronized (_controlListeners) { _controlListeners.remove(listener); } } @Override - public AgentControlAnswer sendRequest(AgentControlCommand cmd, int timeoutInMilliseconds) throws AgentControlChannelException { - Request request = new Request(this.getId(), -1, new Command[] {cmd}, true, false); + public AgentControlAnswer sendRequest(final AgentControlCommand cmd, final int timeoutInMilliseconds) throws AgentControlChannelException { + final Request request = new Request(getId(), -1, new Command[] {cmd}, true, false); request.setSequence(getNextSequence()); - AgentControlListener listener = new AgentControlListener(request); + final AgentControlListener listener = new AgentControlListener(request); registerControlListener(listener); try { @@ -659,7 +796,7 @@ public class Agent implements HandlerFactory, IAgentControl { synchronized (listener) { try { listener.wait(timeoutInMilliseconds); - } catch (InterruptedException e) { + } catch (final InterruptedException e) { s_logger.warn("sendRequest is interrupted, exit waiting"); } } @@ -671,13 +808,13 @@ public class Agent implements HandlerFactory, IAgentControl { } @Override - public void postRequest(AgentControlCommand cmd) throws AgentControlChannelException { - Request request = new Request(this.getId(), -1, new Command[] {cmd}, true, false); + public void postRequest(final AgentControlCommand cmd) throws AgentControlChannelException { + final Request request = new Request(getId(), -1, new Command[] {cmd}, true, false); request.setSequence(getNextSequence()); postRequest(request); } - private void postRequest(Request request) throws AgentControlChannelException { + private void postRequest(final Request request) throws AgentControlChannelException { if (_link != null) { try { _link.send(request.toBytes()); @@ -694,7 +831,7 @@ public class Agent implements HandlerFactory, IAgentControl { private AgentControlAnswer _answer; private final Request _request; - public AgentControlListener(Request request) { + public AgentControlListener(final Request request) { _request = request; } @@ -703,12 +840,12 @@ public class Agent implements HandlerFactory, IAgentControl { } @Override - public Answer processControlRequest(Request request, AgentControlCommand cmd) { + public Answer processControlRequest(final Request request, final AgentControlCommand cmd) { return null; } @Override - public void processControlResponse(Response response, AgentControlAnswer answer) { + public void processControlResponse(final Response response, final AgentControlAnswer answer) { if (_request.getSequence() == response.getSequence()) { _answer = answer; synchronized (this) { @@ -797,13 +934,13 @@ public class Agent implements HandlerFactory, IAgentControl { } public class AgentRequestHandler extends Task { - public AgentRequestHandler(Task.Type type, Link link, Request req) { + public AgentRequestHandler(final Task.Type type, final Link link, final Request req) { super(type, link, req); } @Override - protected void doTask(Task task) throws Exception { - Request req = (Request)this.get(); + protected void doTask(final Task task) throws TaskExecutionException { + final Request req = (Request)get(); if (!(req instanceof Response)) { processRequest(req, task.getLink()); } @@ -811,16 +948,16 @@ public class Agent implements HandlerFactory, IAgentControl { } public class ServerHandler extends Task { - public ServerHandler(Task.Type type, Link link, byte[] data) { + public ServerHandler(final Task.Type type, final Link link, final byte[] data) { super(type, link, data); } - public ServerHandler(Task.Type type, Link link, Request req) { + public ServerHandler(final Task.Type type, final Link link, final Request req) { super(type, link, req); } @Override - public void doTask(final Task task) { + public void doTask(final Task task) throws TaskExecutionException { if (task.getType() == Task.Type.CONNECT) { _shell.getBackoffAlgorithm().reset(); setLink(task.getLink()); @@ -835,7 +972,7 @@ public class Agent implements HandlerFactory, IAgentControl { } else { //put the requests from mgt server into another thread pool, as the request may take a longer time to finish. Don't block the NIO main thread pool //processRequest(request, task.getLink()); - _executor.execute(new AgentRequestHandler(this.getType(), this.getLink(), request)); + _executor.submit(new AgentRequestHandler(getType(), getLink(), request)); } } catch (final ClassNotFoundException e) { s_logger.error("Unable to find this request "); diff --git a/agent/src/com/cloud/agent/AgentShell.java b/agent/src/com/cloud/agent/AgentShell.java index 78430aee0b5..eb4bf8067a1 100644 --- a/agent/src/com/cloud/agent/AgentShell.java +++ b/agent/src/com/cloud/agent/AgentShell.java @@ -17,10 +17,8 @@ package com.cloud.agent; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -37,7 +35,6 @@ import javax.naming.ConfigurationException; import org.apache.commons.daemon.Daemon; import org.apache.commons.daemon.DaemonContext; import org.apache.commons.daemon.DaemonInitException; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang.math.NumberUtils; import org.apache.log4j.Logger; import org.apache.log4j.xml.DOMConfigurator; @@ -70,6 +67,7 @@ public class AgentShell implements IAgentShell, Daemon { private int _proxyPort; private int _workers; private String _guid; + private int _hostCounter = 0; private int _nextAgentId = 1; private volatile boolean _exit = false; private int _pingRetries; @@ -110,7 +108,16 @@ public class AgentShell implements IAgentShell, Daemon { @Override public String getHost() { - return _host; + String[] hosts = _host.split(","); + if (_hostCounter >= hosts.length) { + _hostCounter = 0; + } + s_logger.info("Connecting to host: " + hosts[_hostCounter % hosts.length]); + return hosts[_hostCounter++ % hosts.length]; + } + + public void setHost(final String host) { + _host = host; } @Override @@ -174,16 +181,12 @@ public class AgentShell implements IAgentShell, Daemon { s_logger.info("agent.properties found at " + file.getAbsolutePath()); - InputStream propertiesStream = null; try { - propertiesStream = new FileInputStream(file); - _properties.load(propertiesStream); + PropertiesUtil.loadFromFile(_properties, file); } catch (final FileNotFoundException ex) { throw new CloudRuntimeException("Cannot find the file: " + file.getAbsolutePath(), ex); } catch (final IOException ex) { throw new CloudRuntimeException("IOException in reading " + file.getAbsolutePath(), ex); - } finally { - IOUtils.closeQuietly(propertiesStream); } } @@ -416,11 +419,6 @@ public class AgentShell implements IAgentShell, Daemon { /* By default we only search for log4j.xml */ LogUtils.initLog4j("log4j-cloud.xml"); - /* - By default we disable IPv6 for now to maintain backwards - compatibility. At a later point in time we can change this - behavior to prefer IPv6 over IPv4. - */ boolean ipv6disabled = true; String ipv6 = getProperty(null, "ipv6disabled"); if (ipv6 != null) { @@ -471,6 +469,7 @@ public class AgentShell implements IAgentShell, Daemon { while (!_exit) Thread.sleep(1000); } catch (InterruptedException e) { + s_logger.debug("[ignored] AgentShell was interupted."); } } catch (final ConfigurationException e) { diff --git a/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java b/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java index df1b1ea7b27..1f66cb8fcf0 100755 --- a/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java +++ b/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java @@ -51,6 +51,9 @@ public class PropertiesStorage implements StorageComponent { @Override public synchronized void persist(String key, String value) { + if (!loadFromFile(_file)) { + s_logger.warn("Failed to load changes and then write to them"); + } _properties.setProperty(key, value); FileOutputStream output = null; try { @@ -65,6 +68,20 @@ public class PropertiesStorage implements StorageComponent { } } + private synchronized boolean loadFromFile(final File file) { + try { + PropertiesUtil.loadFromFile(_properties, file); + _file = file; + } catch (FileNotFoundException e) { + s_logger.error("How did we get here? ", e); + return false; + } catch (IOException e) { + s_logger.error("IOException: ", e); + return false; + } + return true; + } + @Override public synchronized boolean configure(String name, Map params) { _name = name; @@ -86,17 +103,7 @@ public class PropertiesStorage implements StorageComponent { return false; } } - try { - PropertiesUtil.loadFromFile(_properties, file); - _file = file; - } catch (FileNotFoundException e) { - s_logger.error("How did we get here? ", e); - return false; - } catch (IOException e) { - s_logger.error("IOException: ", e); - return false; - } - return true; + return loadFromFile(file); } @Override diff --git a/agent/src/com/cloud/agent/resource/DummyResource.java b/agent/src/com/cloud/agent/resource/DummyResource.java index ea23f96d741..c0167f63598 100755 --- a/agent/src/com/cloud/agent/resource/DummyResource.java +++ b/agent/src/com/cloud/agent/resource/DummyResource.java @@ -41,6 +41,7 @@ import com.cloud.network.Networks.RouterPrivateIpStrategy; import com.cloud.resource.ServerResource; import com.cloud.storage.Storage; import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.utils.StringUtils; @Local(value = {ServerResource.class}) public class DummyResource implements ServerResource { @@ -135,7 +136,7 @@ public class DummyResource implements ServerResource { String hostIp = getConfiguredProperty("private.ip.address", "127.0.0.1"); String localStoragePath = getConfiguredProperty("local.storage.path", "/mnt"); String lh = hostIp + localStoragePath; - String uuid = UUID.nameUUIDFromBytes(lh.getBytes()).toString(); + String uuid = UUID.nameUUIDFromBytes(lh.getBytes(StringUtils.getPreferredCharset())).toString(); String capacity = getConfiguredProperty("local.storage.capacity", "1000000000"); String available = getConfiguredProperty("local.storage.avail", "10000000"); diff --git a/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java b/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java index 16173aac2ff..1fed3be753c 100644 --- a/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java +++ b/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java @@ -32,11 +32,8 @@ import java.util.Properties; import javax.naming.ConfigurationException; -import org.apache.log4j.Logger; - -import com.google.gson.Gson; - import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.log4j.Logger; import com.cloud.agent.Agent.ExitStatus; import com.cloud.agent.api.AgentControlAnswer; @@ -64,6 +61,7 @@ import com.cloud.resource.ServerResourceBase; import com.cloud.utils.NumbersUtil; import com.cloud.utils.net.NetUtils; import com.cloud.utils.script.Script; +import com.google.gson.Gson; /** * @@ -149,7 +147,7 @@ public class ConsoleProxyResource extends ServerResourceBase implements ServerRe final URLConnection conn = url.openConnection(); final InputStream is = conn.getInputStream(); - final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + final BufferedReader reader = new BufferedReader(new InputStreamReader(is,"UTF-8")); final StringBuilder sb2 = new StringBuilder(); String line = null; try { @@ -240,9 +238,11 @@ public class ConsoleProxyResource extends ServerResourceBase implements ServerRe _proxyVmId = NumbersUtil.parseLong(value, 0); if (_localgw != null) { - String mgmtHost = (String)params.get("host"); + String mgmtHosts = (String)params.get("host"); if (_eth1ip != null) { - addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); + for (final String mgmtHost : mgmtHosts.split(",")) { + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); + } String internalDns1 = (String) params.get("internaldns1"); if (internalDns1 == null) { s_logger.warn("No DNS entry found during configuration of NfsSecondaryStorage"); diff --git a/agent/test/com/cloud/agent/AgentShellTest.java b/agent/test/com/cloud/agent/AgentShellTest.java index 1f60d87ff29..a3610c1712c 100644 --- a/agent/test/com/cloud/agent/AgentShellTest.java +++ b/agent/test/com/cloud/agent/AgentShellTest.java @@ -16,14 +16,18 @@ // under the License. package com.cloud.agent; +import java.util.Arrays; +import java.util.List; import java.util.UUID; import javax.naming.ConfigurationException; -import junit.framework.Assert; - import org.junit.Test; +import com.cloud.utils.StringUtils; + +import junit.framework.Assert; + public class AgentShellTest { @Test public void parseCommand() throws ConfigurationException { @@ -45,4 +49,15 @@ public class AgentShellTest { Assert.assertNotNull(shell.getProperties()); Assert.assertFalse(shell.getProperties().entrySet().isEmpty()); } + + @Test + public void testGetHost() { + AgentShell shell = new AgentShell(); + List hosts = Arrays.asList("10.1.1.1", "20.2.2.2", "30.3.3.3", "2001:db8::1"); + shell.setHost(StringUtils.listToCsvTags(hosts)); + for (String host : hosts) { + Assert.assertEquals(host, shell.getHost()); + } + Assert.assertEquals(shell.getHost(), hosts.get(0)); + } } diff --git a/api/pom.xml b/api/pom.xml index 7bbf8b18ede..dfb35728d5f 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -51,6 +51,11 @@ cloud-framework-config ${project.version} + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + diff --git a/api/src/com/cloud/event/EventTypes.java b/api/src/com/cloud/event/EventTypes.java index f641c09aa76..4ecdbf42861 100755 --- a/api/src/com/cloud/event/EventTypes.java +++ b/api/src/com/cloud/event/EventTypes.java @@ -19,24 +19,10 @@ package com.cloud.event; import java.util.HashMap; import java.util.Map; -import com.cloud.network.IpAddress; -import com.cloud.network.Site2SiteCustomerGateway; -import com.cloud.network.Site2SiteVpnGateway; -import com.cloud.network.rules.FirewallRule; -import com.cloud.network.rules.HealthCheckPolicy; -import com.cloud.network.rules.StickinessPolicy; -import com.cloud.network.vpc.NetworkACL; -import com.cloud.network.vpc.NetworkACLItem; -import com.cloud.network.Site2SiteVpnConnection; -import com.cloud.server.ResourceTag; -import com.cloud.storage.snapshot.SnapshotPolicy; -import com.cloud.vm.ConsoleProxy; -import com.cloud.vm.Nic; -import com.cloud.vm.NicSecondaryIp; -import com.cloud.vm.SecondaryStorageVm; import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RolePermission; import org.apache.cloudstack.config.Configuration; +import org.apache.cloudstack.ha.HAConfig; import com.cloud.dc.DataCenter; import com.cloud.dc.Pod; @@ -45,20 +31,29 @@ import com.cloud.dc.Vlan; import com.cloud.domain.Domain; import com.cloud.host.Host; import com.cloud.network.GuestVlan; +import com.cloud.network.IpAddress; import com.cloud.network.Network; import com.cloud.network.PhysicalNetwork; import com.cloud.network.PhysicalNetworkServiceProvider; import com.cloud.network.PhysicalNetworkTrafficType; import com.cloud.network.RemoteAccessVpn; +import com.cloud.network.Site2SiteCustomerGateway; +import com.cloud.network.Site2SiteVpnConnection; +import com.cloud.network.Site2SiteVpnGateway; import com.cloud.network.as.AutoScaleCounter; import com.cloud.network.as.AutoScalePolicy; import com.cloud.network.as.AutoScaleVmGroup; import com.cloud.network.as.AutoScaleVmProfile; import com.cloud.network.as.Condition; import com.cloud.network.router.VirtualRouter; +import com.cloud.network.rules.FirewallRule; +import com.cloud.network.rules.HealthCheckPolicy; import com.cloud.network.rules.LoadBalancer; import com.cloud.network.rules.StaticNat; +import com.cloud.network.rules.StickinessPolicy; import com.cloud.network.security.SecurityGroup; +import com.cloud.network.vpc.NetworkACL; +import com.cloud.network.vpc.NetworkACLItem; import com.cloud.network.vpc.PrivateGateway; import com.cloud.network.vpc.StaticRoute; import com.cloud.network.vpc.Vpc; @@ -66,15 +61,20 @@ import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; import com.cloud.projects.Project; +import com.cloud.server.ResourceTag; import com.cloud.storage.GuestOS; import com.cloud.storage.GuestOSHypervisor; import com.cloud.storage.Snapshot; import com.cloud.storage.Volume; +import com.cloud.storage.snapshot.SnapshotPolicy; import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.Account; import com.cloud.user.User; +import com.cloud.vm.ConsoleProxy; +import com.cloud.vm.Nic; +import com.cloud.vm.NicSecondaryIp; +import com.cloud.vm.SecondaryStorageVm; import com.cloud.vm.VirtualMachine; -import org.apache.cloudstack.ha.HAConfig; public class EventTypes { @@ -178,6 +178,11 @@ public class EventTypes { public static final String EVENT_ROLE_PERMISSION_UPDATE = "ROLE.PERMISSION.UPDATE"; public static final String EVENT_ROLE_PERMISSION_DELETE = "ROLE.PERMISSION.DELETE"; + // CA events + public static final String EVENT_CA_CERTIFICATE_ISSUE = "CA.CERTIFICATE.ISSUE"; + public static final String EVENT_CA_CERTIFICATE_REVOKE = "CA.CERTIFICATE.REVOKE"; + public static final String EVENT_CA_CERTIFICATE_PROVISION = "CA.CERTIFICATE.PROVISION"; + // Account events public static final String EVENT_ACCOUNT_ENABLE = "ACCOUNT.ENABLE"; public static final String EVENT_ACCOUNT_DISABLE = "ACCOUNT.DISABLE"; diff --git a/api/src/org/apache/cloudstack/alert/AlertService.java b/api/src/org/apache/cloudstack/alert/AlertService.java index 1c33125d093..293bac460b4 100644 --- a/api/src/org/apache/cloudstack/alert/AlertService.java +++ b/api/src/org/apache/cloudstack/alert/AlertService.java @@ -67,6 +67,7 @@ public interface AlertService { public static final AlertType ALERT_TYPE_SYNC = new AlertType((short)27, "ALERT.TYPE.SYNC", true); public static final AlertType ALERT_TYPE_OOBM_AUTH_ERROR = new AlertType((short)29, "ALERT.OOBM.AUTHERROR", true); public static final AlertType ALERT_TYPE_HA_ACTION = new AlertType((short)30, "ALERT.HA.ACTION", true); + public static final AlertType ALERT_TYPE_CA_CERT = new AlertType((short)31, "ALERT.CA.CERT", true); public short getType() { return type; diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index a366226d1c5..6710d2f3ea6 100755 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -39,10 +39,12 @@ public class ApiConstants { public static final String BYTES_WRITE_RATE = "byteswriterate"; public static final String CATEGORY = "category"; public static final String CAN_REVERT = "canrevert"; + public static final String CA_CERTIFICATES = "cacertificates"; public static final String CERTIFICATE = "certificate"; public static final String CERTIFICATE_CHAIN = "certchain"; public static final String CERTIFICATE_FINGERPRINT = "fingerprint"; public static final String CERTIFICATE_ID = "certid"; + public static final String CSR = "csr"; public static final String PRIVATE_KEY = "privatekey"; public static final String DOMAIN_SUFFIX = "domainsuffix"; public static final String DNS_SEARCH_ORDER = "dnssearchorder"; @@ -54,6 +56,7 @@ public class ApiConstants { public static final String CLUSTER_ID = "clusterid"; public static final String CLUSTER_NAME = "clustername"; public static final String CLUSTER_TYPE = "clustertype"; + public static final String CN = "cn"; public static final String COMMAND = "command"; public static final String CMD_EVENT_TYPE = "cmdeventtype"; public static final String COMPONENT = "component"; @@ -220,6 +223,7 @@ public class ApiConstants { public static final String PUBLIC_END_PORT = "publicendport"; public static final String PUBLIC_ZONE = "publiczone"; public static final String RECEIVED_BYTES = "receivedbytes"; + public static final String RECONNECT = "reconnect"; public static final String RECOVER = "recover"; public static final String REQUIRES_HVM = "requireshvm"; public static final String RESOURCE_TYPE = "resourcetype"; @@ -239,6 +243,7 @@ public class ApiConstants { public static final String SECURITY_GROUP_ID = "securitygroupid"; public static final String SENT = "sent"; public static final String SENT_BYTES = "sentbytes"; + public static final String SERIAL = "serial"; public static final String SERVICE_OFFERING_ID = "serviceofferingid"; public static final String SESSIONKEY = "sessionkey"; public static final String SHOW_CAPACITIES = "showcapacities"; diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java new file mode 100644 index 00000000000..bc112118622 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java @@ -0,0 +1,162 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.ca; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.CertificateResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.log4j.Logger; + +import com.cloud.event.EventTypes; +import com.google.common.base.Strings; + +@APICommand(name = IssueCertificateCmd.APINAME, + description = "Issues a client certificate using configured or provided CA plugin", + responseObject = CertificateResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class IssueCertificateCmd extends BaseAsyncCmd { + private static final Logger LOG = Logger.getLogger(IssueCertificateCmd.class); + + public static final String APINAME = "issueCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.CSR, type = BaseCmd.CommandType.STRING, description = "The certificate signing request (in pem format), if CSR is not provided then configured/provided options are considered", length = 65535) + private String csr; + + @Parameter(name = ApiConstants.DOMAIN, type = BaseCmd.CommandType.STRING, description = "Comma separated list of domains, the certificate should be issued for. When csr is not provided, the first domain is used as a subject/CN") + private String domains; + + @Parameter(name = ApiConstants.IP_ADDRESS, type = BaseCmd.CommandType.STRING, description = "Comma separated list of IP addresses, the certificate should be issued for") + private String addresses; + + @Parameter(name = ApiConstants.DURATION, type = CommandType.INTEGER, description = "Certificate validity duration in number of days, when not provided the default configured value will be used") + private Integer validityDuration; + + @Parameter(name = ApiConstants.PROVIDER, type = BaseCmd.CommandType.STRING, description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getCsr() { + return csr; + } + + private List processList(final String string) { + final List list = new ArrayList<>(); + if (!Strings.isNullOrEmpty(string)) { + for (final String address: string.split(",")) { + list.add(address.trim()); + } + } + return list; + } + + public List getAddresses() { + return processList(addresses); + } + + public List getDomains() { + return processList(domains); + } + + public Integer getValidityDuration() { + return validityDuration; + } + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + if (Strings.isNullOrEmpty(getCsr()) && getDomains().isEmpty()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Please provide the domains or the CSR, none of them are provided"); + } + final Certificate certificate = caManager.issueCertificate(getCsr(), getDomains(), getAddresses(), getValidityDuration(), getProvider()); + if (certificate == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to issue client certificate with given provider"); + } + + final CertificateResponse certificateResponse = new CertificateResponse(); + try { + certificateResponse.setCertificate(CertUtils.x509CertificateToPem(certificate.getClientCertificate())); + if (certificate.getPrivateKey() != null) { + certificateResponse.setPrivateKey(CertUtils.privateKeyToPem(certificate.getPrivateKey())); + } + if (certificate.getCaCertificates() != null) { + certificateResponse.setCaCertificate(CertUtils.x509CertificatesToPem(certificate.getCaCertificates())); + } + } catch (final IOException e) { + LOG.error("Failed to generate and convert client certificate(s) to PEM due to error: ", e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to process and return client certificate"); + } + certificateResponse.setResponseName(getCommandName()); + setResponseObject(certificateResponse); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_CA_CERTIFICATE_ISSUE; + } + + @Override + public String getEventDescription() { + return "issuing certificate for domain(s)=" + domains + ", ip(s)=" + addresses; + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/ListCAProvidersCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCAProvidersCmd.java new file mode 100644 index 00000000000..e1e8e375163 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCAProvidersCmd.java @@ -0,0 +1,102 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.ca; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.CAProviderResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.framework.ca.CAProvider; + +import com.cloud.user.Account; + +@APICommand(name = ListCAProvidersCmd.APINAME, + description = "Lists available certificate authority providers in CloudStack", + responseObject = CAProviderResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class ListCAProvidersCmd extends BaseCmd { + public static final String APINAME = "listCAProviders"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "List CA service provider by name") + private String name; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + private void setupResponse(final List providers) { + final ListResponse response = new ListResponse<>(); + final List responses = new ArrayList<>(); + for (final CAProvider provider : providers) { + if (provider == null || (getName() != null && !provider.getProviderName().equals(getName()))) { + continue; + } + final CAProviderResponse caProviderResponse = new CAProviderResponse(); + caProviderResponse.setName(provider.getProviderName()); + caProviderResponse.setDescription(provider.getDescription()); + caProviderResponse.setObjectName("caprovider"); + responses.add(caProviderResponse); + } + response.setResponses(responses); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public void execute() { + final List caProviders = caManager.getCaProviders(); + setupResponse(caProviders); + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/ListCaCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCaCertificateCmd.java new file mode 100644 index 00000000000..1baa84179f0 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCaCertificateCmd.java @@ -0,0 +1,90 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.ca; + +import java.io.IOException; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.CertificateResponse; +import org.apache.cloudstack.ca.CAManager; + +import com.cloud.user.Account; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = ListCaCertificateCmd.APINAME, + description = "Lists the CA public certificate(s) as support by the configured/provided CA plugin", + responseObject = CertificateResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListCaCertificateCmd extends BaseCmd { + public static final String APINAME = "listCaCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + final String caCertificates; + try { + caCertificates = caManager.getCaCertificate(getProvider()); + } catch (final IOException e) { + throw new CloudRuntimeException("Failed to get CA certificates for given CA provider"); + } + final CertificateResponse certificateResponse = new CertificateResponse("cacertificates"); + certificateResponse.setCertificate(caCertificates); + certificateResponse.setResponseName(getCommandName()); + setResponseObject(certificateResponse); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_TYPE_NORMAL; + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java new file mode 100644 index 00000000000..2745f071dd0 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java @@ -0,0 +1,125 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.ca; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandJobType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.host.Host; + +@APICommand(name = ProvisionCertificateCmd.APINAME, + description = "Issues and propagates client certificate on a connected host/agent using configured CA plugin", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class ProvisionCertificateCmd extends BaseAsyncCmd { + public static final String APINAME = "provisionCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, required = true, entityType = HostResponse.class, + description = "The host/agent uuid to which the certificate has to be provisioned (issued and propagated)") + private Long hostId; + + @Parameter(name = ApiConstants.RECONNECT, type = CommandType.BOOLEAN, + description = "Whether to attempt reconnection with host/agent after successful deployment of certificate. When option is not provided, configured global setting is used") + private Boolean reconnect; + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, + description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getHostId() { + return hostId; + } + + public Boolean getReconnect() { + return reconnect; + } + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + final Host host = _resourceService.getHost(getHostId()); + if (host == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to find host by ID: " + getHostId()); + } + + boolean result = caManager.provisionCertificate(host, getReconnect(), getProvider()); + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setSuccess(result); + setResponseObject(response); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_CA_CERTIFICATE_PROVISION; + } + + @Override + public String getEventDescription() { + return "provisioning certificate for host id=" + hostId + " using provider=" + provider; + } + + @Override + public ApiCommandJobType getInstanceType() { + return ApiCommandJobType.Host; + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/RevokeCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/RevokeCertificateCmd.java new file mode 100644 index 00000000000..62ed4ac2f25 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/RevokeCertificateCmd.java @@ -0,0 +1,116 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.ca; + +import java.math.BigInteger; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.google.common.base.Strings; + +@APICommand(name = RevokeCertificateCmd.APINAME, + description = "Revokes certificate using configured CA plugin", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class RevokeCertificateCmd extends BaseAsyncCmd { + + public static final String APINAME = "revokeCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SERIAL, type = BaseCmd.CommandType.STRING, required = true, description = "The certificate serial number, as a hex value") + private String serial; + + @Parameter(name = ApiConstants.CN, type = BaseCmd.CommandType.STRING, description = "The certificate CN") + private String cn; + + @Parameter(name = ApiConstants.PROVIDER, type = BaseCmd.CommandType.STRING, description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public BigInteger getSerialBigInteger() { + if (Strings.isNullOrEmpty(serial)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Certificate serial cannot be empty"); + } + return new BigInteger(serial, 16); + } + + public String getCn() { + return cn; + } + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + boolean result = caManager.revokeCertificate(getSerialBigInteger(), getCn(), getProvider()); + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setSuccess(result); + setResponseObject(response); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_CA_CERTIFICATE_REVOKE; + } + + @Override + public String getEventDescription() { + return "revoking certificate with serial id=" + serial + ", cn=" + cn; + } +} diff --git a/api/src/org/apache/cloudstack/api/response/CAProviderResponse.java b/api/src/org/apache/cloudstack/api/response/CAProviderResponse.java new file mode 100644 index 00000000000..94d5882e18a --- /dev/null +++ b/api/src/org/apache/cloudstack/api/response/CAProviderResponse.java @@ -0,0 +1,52 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.framework.ca.CAProvider; + +@EntityReference(value = CAProvider.class) +public class CAProviderResponse extends BaseResponse { + @SerializedName(ApiConstants.NAME) + @Param(description = "the CA service provider name") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "the description of the CA service provider") + private String description; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/api/src/org/apache/cloudstack/api/response/CertificateResponse.java b/api/src/org/apache/cloudstack/api/response/CertificateResponse.java new file mode 100644 index 00000000000..f8c3ecc7404 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/response/CertificateResponse.java @@ -0,0 +1,58 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class CertificateResponse extends BaseResponse { + @SerializedName(ApiConstants.CERTIFICATE) + @Param(description = "The client certificate") + private String certificate = ""; + + @SerializedName(ApiConstants.PRIVATE_KEY) + @Param(description = "Private key for the certificate") + private String privateKey; + + @SerializedName(ApiConstants.CA_CERTIFICATES) + @Param(description = "The CA certificate(s)") + private String caCertificate; + + public CertificateResponse() { + setObjectName("certificates"); + } + + public CertificateResponse(final String objectName) { + setObjectName(objectName); + } + + public void setCertificate(final String certificate) { + this.certificate = certificate; + } + + public void setPrivateKey(final String privateKey) { + this.privateKey = privateKey; + } + + public void setCaCertificate(final String caCertificate) { + this.caCertificate = caCertificate; + } +} diff --git a/api/src/org/apache/cloudstack/ca/CAManager.java b/api/src/org/apache/cloudstack/ca/CAManager.java new file mode 100644 index 00000000000..c32cfbfe345 --- /dev/null +++ b/api/src/org/apache/cloudstack/ca/CAManager.java @@ -0,0 +1,163 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.ca; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.framework.ca.CAProvider; +import org.apache.cloudstack.framework.ca.CAService; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.utils.component.PluggableService; + +public interface CAManager extends CAService, Configurable, PluggableService { + + ConfigKey CAProviderPlugin = new ConfigKey<>("Advanced", String.class, + "ca.framework.provider.plugin", + "root", + "The CA provider plugin that is used for secure CloudStack management server-agent communication for encryption and authentication. Restart management server(s) when changed.", true); + + ConfigKey CertKeySize = new ConfigKey<>("Advanced", Integer.class, + "ca.framework.cert.keysize", + "2048", + "The key size to be used for random certificate keypair generation.", true); + + ConfigKey CertSignatureAlgorithm = new ConfigKey<>("Advanced", String.class, + "ca.framework.cert.signature.algorithm", + "SHA256withRSA", + "The default signature algorithm to use for certificate generation.", true); + + + ConfigKey CertValidityPeriod = new ConfigKey<>("Advanced", Integer.class, + "ca.framework.cert.validity.period", + "365", + "The validity period of a client certificate in number of days. Set the value to be more than the expiry alert period.", true); + + ConfigKey AutomaticCertRenewal = new ConfigKey<>("Advanced", Boolean.class, + "ca.framework.cert.automatic.renewal", + "true", + "Enable automatic renewal and provisioning of certificate to agents as supported by the configured CA plugin.", true, ConfigKey.Scope.Cluster); + + ConfigKey CABackgroundJobDelay = new ConfigKey<>("Advanced", Long.class, + "ca.framework.background.task.delay", + "3600", + "The CA framework background task delay in seconds. Background task runs expiry checks and renews certificate if auto-renewal is enabled.", true); + + ConfigKey CertExpiryAlertPeriod = new ConfigKey<>("Advanced", Integer.class, + "ca.framework.cert.expiry.alert.period", + "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); + + /** + * Returns a list of available CA provider plugins + * @return returns list of CAProvider + */ + List getCaProviders(); + + /** + * Returns a map of active agents/hosts certificates + * @return returns a non-null map + */ + Map getActiveCertificatesMap(); + + /** + * Checks whether the configured CA plugin can provision/create certificates + * @return returns certificate creation capability + */ + boolean canProvisionCertificates(); + + /** + * Returns PEM-encoded chained CA certificate + * @param caProvider + * @return returns CA certificate chain string + */ + String getCaCertificate(final String caProvider) throws IOException; + + /** + * Issues client Certificate + * @param csr + * @param ipAddresses + * @param domainNames + * @param validityDays + * @param provider + * @return returns Certificate + */ + Certificate issueCertificate(final String csr, final List domainNames, final List ipAddresses, final Integer validityDays, final String provider); + + /** + * Revokes certificate from provided serial and CN + * @param certSerial + * @param certCn + * @return returns success/failure as boolean + */ + boolean revokeCertificate(final BigInteger certSerial, final String certCn, final String provider); + + /** + * Provisions certificate for given active and connected agent host + * @param host + * @param provider + * @return returns success/failure as boolean + */ + boolean provisionCertificate(final Host host, final Boolean reconnect, final String provider); + + /** + * Setups up a new keystore and generates CSR for a host + * @param host + * @param sshAccessDetails when provided, VirtualRoutingResource uses router proxy to execute commands via SSH in systemvms + * @return + * @throws AgentUnavailableException + * @throws OperationTimedoutException + */ + String generateKeyStoreAndCsr(final Host host, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException; + + /** + * Deploys a Certificate payload to a provided host + * @param host + * @param certificate + * @param reconnect when true the host/agent is reconnected on successful deployment of the certificate + * @param sshAccessDetails when provided, VirtualRoutingResource uses router proxy to execute commands via SSH in systemvms + * @return + * @throws AgentUnavailableException + * @throws OperationTimedoutException + */ + boolean deployCertificate(final Host host, final Certificate certificate, final Boolean reconnect, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException; + + /** + * Removes the host from an internal active client/certificate map + * @param host + */ + void purgeHostCertificate(final Host host); + + /** + * Sends a CA cert event alert to admins with a subject and a message + * @param host + * @param subject + * @param message + */ + void sendAlert(final Host host, final String subject, final String message); + +} diff --git a/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java b/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java index 8eea147955b..5f1b3300c48 100644 --- a/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java +++ b/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java @@ -18,4 +18,10 @@ package org.apache.cloudstack.poll; public interface BackgroundPollTask extends Runnable { + /** + * Returns delay in milliseconds between two rounds + * When it returns null a default value is used + * @return + */ + Long getDelay(); } diff --git a/client/pom.xml b/client/pom.xml index cf99d667106..a7f453c93bb 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -55,6 +55,11 @@ cloud-plugin-acl-dynamic-role-based ${project.version} + + org.apache.cloudstack + cloud-plugin-ca-rootca + ${project.version} + org.apache.cloudstack cloud-plugin-dedicated-resources @@ -261,6 +266,11 @@ cloud-mom-inmemory ${project.version} + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + org.apache.cloudstack cloud-framework-ipc diff --git a/client/tomcatconf/cloudmanagementserver.keystore b/client/tomcatconf/cloudmanagementserver.keystore deleted file mode 100644 index 3ee4d13565a..00000000000 Binary files a/client/tomcatconf/cloudmanagementserver.keystore and /dev/null differ diff --git a/client/tomcatconf/server-ssl.xml.in b/client/tomcatconf/server-ssl.xml.in index 2e61251be1a..7d6ee4d6edf 100755 --- a/client/tomcatconf/server-ssl.xml.in +++ b/client/tomcatconf/server-ssl.xml.in @@ -94,7 +94,7 @@ maxThreads="150" scheme="https" secure="true" URIEncoding="UTF-8" clientAuth="false" sslProtocol="TLS" keystoreType="JKS" - keystoreFile="/etc/cloudstack/management/cloudmanagementserver.keystore" + keystoreFile="/etc/cloudstack/management/cloud.jks" keystorePass="vmops.com"/> @@ -200,7 +200,7 @@ maxThreads="150" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS" keystoreType="JKS" - keystoreFile="/etc/cloudstack/management/cloudmanagementserver.keystore" + keystoreFile="/etc/cloudstack/management/cloud.jks" keystorePass="vmops.com"/> diff --git a/client/tomcatconf/server7-ssl.xml.in b/client/tomcatconf/server7-ssl.xml.in index 97421ba52be..02d3b4f640e 100755 --- a/client/tomcatconf/server7-ssl.xml.in +++ b/client/tomcatconf/server7-ssl.xml.in @@ -94,7 +94,7 @@ maxThreads="150" scheme="https" secure="true" URIEncoding="UTF-8" clientAuth="false" sslProtocol="TLS" keystoreType="JKS" - keystoreFile="/etc/cloudstack/management/cloudmanagementserver.keystore" + keystoreFile="/etc/cloudstack/management/cloud.jks" keystorePass="vmops.com"/> @@ -200,7 +200,7 @@ maxThreads="150" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS" keystoreType="JKS" - keystoreFile="/etc/cloudstack/management/cloudmanagementserver.keystore" + keystoreFile="/etc/cloudstack/management/cloud.jks" keystorePass="vmops.com"/> diff --git a/client/tomcatconf/tomcat6-ssl.conf.in b/client/tomcatconf/tomcat6-ssl.conf.in index e7c53ac9f8f..1d6f59b0787 100644 --- a/client/tomcatconf/tomcat6-ssl.conf.in +++ b/client/tomcatconf/tomcat6-ssl.conf.in @@ -40,7 +40,7 @@ CATALINA_TMPDIR="@MSENVIRON@/temp" # Use JAVA_OPTS to set java.library.path for libtcnative.so #JAVA_OPTS="-Djava.library.path=/usr/lib64" -JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloudmanagementserver.keystore -Djavax.net.ssl.trustStorePassword=vmops.com -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=@MSLOGDIR@ -XX:MaxPermSize=800m -XX:PermSize=512M -Djava.security.properties=/etc/cloudstack/management/java.security.ciphers" +JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloud.jks -Djavax.net.ssl.trustStorePassword=vmops.com -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=@MSLOGDIR@ -XX:MaxPermSize=800m -XX:PermSize=512M -Djava.security.properties=/etc/cloudstack/management/java.security.ciphers" # What user should run tomcat TOMCAT_USER="@MSUSER@" diff --git a/core/resources/META-INF/cloudstack/ca/module.properties b/core/resources/META-INF/cloudstack/ca/module.properties new file mode 100644 index 00000000000..1a6915aea90 --- /dev/null +++ b/core/resources/META-INF/cloudstack/ca/module.properties @@ -0,0 +1,21 @@ +# +# 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. +# + +name=ca +parent=backend diff --git a/core/resources/META-INF/cloudstack/ca/spring-core-lifecycle-ca-context-inheritable.xml b/core/resources/META-INF/cloudstack/ca/spring-core-lifecycle-ca-context-inheritable.xml new file mode 100644 index 00000000000..1566a4b076b --- /dev/null +++ b/core/resources/META-INF/cloudstack/ca/spring-core-lifecycle-ca-context-inheritable.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index d4cb8da068f..a5d3460d559 100644 --- a/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -317,4 +317,9 @@ + + + + diff --git a/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java b/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java index e4105c1562a..107598c21c0 100644 --- a/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java +++ b/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java @@ -19,9 +19,10 @@ package com.cloud.agent.api.routing; -import com.cloud.agent.api.Command; - import java.util.HashMap; +import java.util.Map; + +import com.cloud.agent.api.Command; public abstract class NetworkElementCommand extends Command { HashMap accessDetails = new HashMap(0); @@ -45,6 +46,18 @@ public abstract class NetworkElementCommand extends Command { super(); } + public void setAccessDetail(Map details) { + if (details == null) { + return; + } + for (final Map.Entry detail : details.entrySet()) { + if (detail == null) { + continue; + } + setAccessDetail(detail.getKey(), detail.getValue()); + } + } + public void setAccessDetail(String name, String value) { accessDetails.put(name, value); } diff --git a/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java b/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java index 5c1ee0d0d9f..94e790194a5 100755 --- a/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java +++ b/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java @@ -34,6 +34,11 @@ import java.util.concurrent.locks.ReentrantLock; import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.SetupCertificateAnswer; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; +import org.apache.cloudstack.ca.SetupKeystoreAnswer; +import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.log4j.Logger; import com.cloud.agent.api.Answer; @@ -105,6 +110,14 @@ public class VirtualRoutingResource { return executeQueryCommand(cmd); } + if (cmd instanceof SetupKeyStoreCommand) { + return execute((SetupKeyStoreCommand) cmd); + } + + if (cmd instanceof SetupCertificateCommand) { + return execute((SetupCertificateCommand) cmd); + } + if (cmd instanceof AggregationControlCommand) { return execute((AggregationControlCommand)cmd); } @@ -136,6 +149,37 @@ public class VirtualRoutingResource { } } + private Answer execute(final SetupKeyStoreCommand cmd) { + final String args = String.format("/usr/local/cloud/systemvm/conf/agent.properties " + + "/usr/local/cloud/systemvm/conf/%s " + + "%s %d " + + "/usr/local/cloud/systemvm/conf/%s", + KeyStoreUtils.defaultKeystoreFile, + cmd.getKeystorePassword(), + cmd.getValidityDays(), + KeyStoreUtils.defaultCsrFile); + ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), KeyStoreUtils.keyStoreSetupScript, args); + return new SetupKeystoreAnswer(result.getDetails()); + } + + private Answer execute(final SetupCertificateCommand cmd) { + final String args = String.format("/usr/local/cloud/systemvm/conf/agent.properties " + + "/usr/local/cloud/systemvm/conf/%s %s " + + "/usr/local/cloud/systemvm/conf/%s \"%s\" " + + "/usr/local/cloud/systemvm/conf/%s \"%s\" " + + "/usr/local/cloud/systemvm/conf/%s \"%s\"", + KeyStoreUtils.defaultKeystoreFile, + KeyStoreUtils.sshMode, + KeyStoreUtils.defaultCertFile, + cmd.getEncodedCertificate(), + KeyStoreUtils.defaultCaCertFile, + cmd.getEncodedCaCertificates(), + KeyStoreUtils.defaultPrivateKeyFile, + cmd.getEncodedPrivateKey()); + ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), KeyStoreUtils.keyStoreImportScript, args); + return new SetupCertificateAnswer(result.isSuccess()); + } + private Answer executeQueryCommand(NetworkElementCommand cmd) { if (cmd instanceof CheckRouterCommand) { return execute((CheckRouterCommand)cmd); diff --git a/core/src/org/apache/cloudstack/ca/SetupCertificateAnswer.java b/core/src/org/apache/cloudstack/ca/SetupCertificateAnswer.java new file mode 100644 index 00000000000..4df5d158da9 --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupCertificateAnswer.java @@ -0,0 +1,29 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import com.cloud.agent.api.Answer; + +public class SetupCertificateAnswer extends Answer { + public SetupCertificateAnswer(final boolean result) { + super(null); + this.result = result; + } +} diff --git a/core/src/org/apache/cloudstack/ca/SetupCertificateCommand.java b/core/src/org/apache/cloudstack/ca/SetupCertificateCommand.java new file mode 100644 index 00000000000..1cd31509d39 --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupCertificateCommand.java @@ -0,0 +1,99 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import java.io.IOException; +import java.util.Map; + +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.cloudstack.utils.security.KeyStoreUtils; + +import com.cloud.agent.api.LogLevel; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.utils.exception.CloudRuntimeException; + +public class SetupCertificateCommand extends NetworkElementCommand { + @LogLevel(LogLevel.Log4jLevel.Off) + private String certificate; + @LogLevel(LogLevel.Log4jLevel.Off) + private String privateKey = ""; + @LogLevel(LogLevel.Log4jLevel.Off) + private String caCertificates; + + private boolean handleByAgent = true; + + public SetupCertificateCommand(final Certificate certificate) { + super(); + if (certificate == null) { + throw new CloudRuntimeException("A null certificate was provided to setup"); + } + setWait(60); + try { + this.certificate = CertUtils.x509CertificateToPem(certificate.getClientCertificate()); + this.caCertificates = CertUtils.x509CertificatesToPem(certificate.getCaCertificates()); + if (certificate.getPrivateKey() != null) { + this.privateKey = CertUtils.privateKeyToPem(certificate.getPrivateKey()); + } + } catch (final IOException e) { + throw new CloudRuntimeException("Failed to transform X509 cert to PEM format", e); + } + } + + @Override + public void setAccessDetail(final Map accessDetails) { + handleByAgent = false; + super.setAccessDetail(accessDetails); + } + + @Override + public void setAccessDetail(String name, String value) { + handleByAgent = false; + super.setAccessDetail(name, value); + } + + public String getPrivateKey() { + return privateKey; + } + + public String getCertificate() { + return certificate; + } + + public String getCaCertificates() { + return caCertificates; + } + + public String getEncodedPrivateKey() { + return privateKey.replace("\n", KeyStoreUtils.certNewlineEncoder).replace(" ", KeyStoreUtils.certSpaceEncoder); + } + + public String getEncodedCertificate() { + return certificate.replace("\n", KeyStoreUtils.certNewlineEncoder).replace(" ", KeyStoreUtils.certSpaceEncoder); + } + + public String getEncodedCaCertificates() { + return caCertificates.replace("\n", KeyStoreUtils.certNewlineEncoder).replace(" ", KeyStoreUtils.certSpaceEncoder); + } + + public boolean isHandleByAgent() { + return handleByAgent; + } +} diff --git a/core/src/org/apache/cloudstack/ca/SetupKeyStoreCommand.java b/core/src/org/apache/cloudstack/ca/SetupKeyStoreCommand.java new file mode 100644 index 00000000000..7cd5cbee20f --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupKeyStoreCommand.java @@ -0,0 +1,75 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import java.util.Map; + +import com.cloud.agent.api.LogLevel; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.utils.PasswordGenerator; + +public class SetupKeyStoreCommand extends NetworkElementCommand { + @LogLevel(LogLevel.Log4jLevel.Off) + private int validityDays; + @LogLevel(LogLevel.Log4jLevel.Off) + private String keystorePassword; + + private boolean handleByAgent = true; + + public SetupKeyStoreCommand(final int validityDays) { + super(); + setWait(60); + this.validityDays = validityDays; + if (this.validityDays < 1) { + this.validityDays = 1; + } + this.keystorePassword = PasswordGenerator.generateRandomPassword(16); + } + + @Override + public void setAccessDetail(final Map accessDetails) { + handleByAgent = false; + super.setAccessDetail(accessDetails); + } + + + @Override + public void setAccessDetail(String name, String value) { + handleByAgent = false; + super.setAccessDetail(name, value); + } + + public int getValidityDays() { + return validityDays; + } + + public String getKeystorePassword() { + return keystorePassword; + } + + public boolean isHandleByAgent() { + return handleByAgent; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/org/apache/cloudstack/ca/SetupKeystoreAnswer.java b/core/src/org/apache/cloudstack/ca/SetupKeystoreAnswer.java new file mode 100644 index 00000000000..16ddc96c5ea --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupKeystoreAnswer.java @@ -0,0 +1,37 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import com.cloud.agent.api.LogLevel; +import com.google.common.base.Strings; + +public class SetupKeystoreAnswer extends SetupCertificateAnswer { + @LogLevel(LogLevel.Log4jLevel.Off) + private final String csr; + + public SetupKeystoreAnswer(final String csr) { + super(!Strings.isNullOrEmpty(csr)); + this.csr = csr; + } + + public String getCsr() { + return csr; + } +} diff --git a/debian/cloudstack-management.postinst b/debian/cloudstack-management.postinst index 366ccc12fa1..d995c88d5a2 100644 --- a/debian/cloudstack-management.postinst +++ b/debian/cloudstack-management.postinst @@ -47,9 +47,6 @@ if [ "$1" = configure ]; then cp -a $OLDCONFDIR/$FILE $NEWCONFDIR/$FILE fi done - if [ -f "$OLDCONFDIR/cloud.keystore" ]; then - cp -a $OLDCONFDIR/cloud.keystore $NEWCONFDIR/cloudmanagementserver.keystore - fi fi chmod 0640 /etc/cloudstack/management/db.properties diff --git a/developer/developer-prefill.sql b/developer/developer-prefill.sql index 5d301f5ae40..9c335ebb219 100644 --- a/developer/developer-prefill.sql +++ b/developer/developer-prefill.sql @@ -83,6 +83,11 @@ INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'RoleService', 'dynamic.apichecker.enabled', 'true'); +-- Enable RootCA auth strictness for fresh deployments +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'RootCAProvider', + 'ca.plugin.root.auth.strictness', 'true'); + -- Add developer configuration entry; allows management server to be run as a user other than "cloud" INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'management-server', diff --git a/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index 62aec8f28a2..a3a0c17d596 100755 --- a/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -96,6 +96,8 @@ public interface NetworkOrchestrationService { List getNicProfiles(VirtualMachine vm); + Map getSystemVMAccessDetails(VirtualMachine vm); + Pair implementNetwork(long networkId, DeployDestination dest, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException; diff --git a/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java b/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java index cde890ba312..f7d8d3e45db 100755 --- a/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java +++ b/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java @@ -38,16 +38,16 @@ import javax.ejb.Local; import javax.inject.Inject; import javax.naming.ConfigurationException; -import org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDao; -import org.apache.log4j.Logger; - +import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.jobs.AsyncJob; import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDao; import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.log4j.Logger; import com.cloud.agent.AgentManager; import com.cloud.agent.Listener; @@ -105,6 +105,8 @@ import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.HypervisorVersionChangedException; +import com.cloud.utils.exception.NioConnectionException; +import com.cloud.utils.exception.TaskExecutionException; import com.cloud.utils.fsm.NoTransitionException; import com.cloud.utils.fsm.StateMachine2; import com.cloud.utils.nio.HandlerFactory; @@ -135,6 +137,8 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl protected int _monitorId = 0; private final Lock _agentStatusLock = new ReentrantLock(); + @Inject + protected CAManager caService; @Inject protected EntityManager _entityMgr; @@ -222,7 +226,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl //allow core threads to time out even when there are no items in the queue _connectExecutor.allowCoreThreadTimeOut(true); - _connection = new NioServer("AgentManager", Port.value(), Workers.value() + 10, this); + _connection = new NioServer("AgentManager", Port.value(), Workers.value() + 10, this, caService); s_logger.info("Listening on " + Port.value() + " with " + Workers.value() + " workers"); // executes all agent commands other than cron and ping @@ -587,7 +591,11 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl startDirectlyConnectedHosts(); if (_connection != null) { - _connection.start(); + try { + _connection.start(); + } catch (final NioConnectionException e) { + s_logger.error("Error when connecting to the NioServer!", e); + } } _monitorExecutor.scheduleWithFixedDelay(new MonitorTask(), PingInterval.value(), PingInterval.value(), TimeUnit.SECONDS); @@ -783,6 +791,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl s_logger.debug("The next status of agent " + hostId + "is " + nextStatus + ", current status is " + currentStatus); } } + caService.purgeHostCertificate(host); } if (s_logger.isDebugEnabled()) { @@ -1288,7 +1297,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl } @Override - protected void doTask(final Task task) throws Exception { + protected void doTask(final Task task) throws TaskExecutionException { TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.CLOUD_DB); try { final Type type = task.getType(); @@ -1304,6 +1313,10 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl } catch (final UnsupportedVersionException e) { s_logger.warn(e.getMessage()); // upgradeAgent(task.getLink(), data, e.getReason()); + } catch (final ClassNotFoundException e) { + final String message = String.format("Exception occured when executing taks! Error '%s'", e.getMessage()); + s_logger.error(message); + throw new TaskExecutionException(message, e); } } else if (type == Task.Type.CONNECT) { } else if (type == Task.Type.DISCONNECT) { diff --git a/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java b/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java index 195a711105f..1ef8a4895e2 100755 --- a/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java +++ b/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java @@ -43,19 +43,16 @@ import javax.naming.ConfigurationException; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; -import org.apache.cloudstack.ha.dao.HAConfigDao; -import org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDao; -import org.apache.log4j.Logger; - -import com.google.gson.Gson; - import org.apache.cloudstack.framework.config.ConfigDepot; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.ha.dao.HAConfigDao; import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.cloudstack.managed.context.ManagedContextTimerTask; +import org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDao; import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.cloudstack.utils.security.SSLUtils; +import org.apache.log4j.Logger; import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; @@ -95,8 +92,10 @@ import com.cloud.utils.db.QueryBuilder; import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.exception.TaskExecutionException; import com.cloud.utils.nio.Link; import com.cloud.utils.nio.Task; +import com.google.gson.Gson; @Local(value = {AgentManager.class, ClusteredAgentRebalanceService.class}) public class ClusteredAgentManagerImpl extends AgentManagerImpl implements ClusterManagerListener, ClusteredAgentRebalanceService { @@ -145,7 +144,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust "Interval between scans to load agents", false, ConfigKey.Scope.Global, 1000); @Override - public boolean configure(String name, Map xmlParams) throws ConfigurationException { + public boolean configure(final String name, final Map xmlParams) throws ConfigurationException { _peers = new HashMap(7); _sslEngines = new HashMap(7); _nodeId = ManagementServerNode.getManagementServerId(); @@ -198,17 +197,17 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } // for agents that are self-managed, threshold to be considered as disconnected after pingtimeout - long cutSeconds = (System.currentTimeMillis() >> 10) - getTimeout(); - List hosts = _hostDao.findAndUpdateDirectAgentToLoad(cutSeconds, LoadSize.value().longValue(), _nodeId); - List appliances = _hostDao.findAndUpdateApplianceToLoad(cutSeconds, _nodeId); + final long cutSeconds = (System.currentTimeMillis() >> 10) - getTimeout(); + final List hosts = _hostDao.findAndUpdateDirectAgentToLoad(cutSeconds, LoadSize.value().longValue(), _nodeId); + final List appliances = _hostDao.findAndUpdateApplianceToLoad(cutSeconds, _nodeId); - if (hosts != null) { + if (hosts != null) { hosts.addAll(appliances); if (hosts.size() > 0) { s_logger.debug("Found " + hosts.size() + " unmanaged direct hosts, processing connect for them..."); - for (HostVO host : hosts) { + for (final HostVO host : hosts) { try { - AgentAttache agentattache = findAttache(host.getId()); + final AgentAttache agentattache = findAttache(host.getId()); if (agentattache != null) { // already loaded, skip if (agentattache.forForward()) { @@ -225,7 +224,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust s_logger.debug("Loading directly connected host " + host.getId() + "(" + host.getName() + ")"); } loadDirectlyConnectedHost(host, false); - } catch (Throwable e) { + } catch (final Throwable e) { s_logger.warn(" can not load directly connected host " + host.getId() + "(" + host.getName() + ") due to ", e); } } @@ -241,20 +240,20 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust protected void runInContext() { try { runDirectAgentScanTimerTask(); - } catch (Throwable e) { + } catch (final Throwable e) { s_logger.error("Unexpected exception " + e.getMessage(), e); } } } @Override - public Task create(Task.Type type, Link link, byte[] data) { + public Task create(final Task.Type type, final Link link, final byte[] data) { return new ClusteredAgentHandler(type, link, data); } - protected AgentAttache createAttache(long id) { + protected AgentAttache createAttache(final long id) { s_logger.debug("create forwarding ClusteredAgentAttache for " + id); - HostVO host = _hostDao.findById(id); + final HostVO host = _hostDao.findById(id); final AgentAttache attache = new ClusteredAgentAttache(this, id, host.getName()); AgentAttache old = null; synchronized (_agents) { @@ -271,7 +270,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } @Override - protected AgentAttache createAttacheForConnect(HostVO host, Link link) { + protected AgentAttache createAttacheForConnect(final HostVO host, final Link link) { s_logger.debug("create ClusteredAgentAttache for " + host.getId()); final AgentAttache attache = new ClusteredAgentAttache(this, host.getId(), host.getName(), link, host.isInMaintenanceStates()); link.attach(attache); @@ -287,7 +286,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } @Override - protected AgentAttache createAttacheForDirectConnect(Host host, ServerResource resource) { + protected AgentAttache createAttacheForDirectConnect(final Host host, final ServerResource resource) { s_logger.debug("create ClusteredDirectAgentAttache for " + host.getId()); final DirectAgentAttache attache = new ClusteredDirectAgentAttache(this, host.getId(), host.getName(), _nodeId, resource, host.isInMaintenanceStates()); AgentAttache old = null; @@ -302,16 +301,16 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } @Override - protected boolean handleDisconnectWithoutInvestigation(AgentAttache attache, Status.Event event, boolean transitState, boolean removeAgent) { + protected boolean handleDisconnectWithoutInvestigation(final AgentAttache attache, final Status.Event event, final boolean transitState, final boolean removeAgent) { return handleDisconnect(attache, event, false, true, removeAgent); } @Override - protected boolean handleDisconnectWithInvestigation(AgentAttache attache, Status.Event event) { + protected boolean handleDisconnectWithInvestigation(final AgentAttache attache, final Status.Event event) { return handleDisconnect(attache, event, true, true, true); } - protected boolean handleDisconnect(AgentAttache agent, Status.Event event, boolean investigate, boolean broadcast, boolean removeAgent) { + protected boolean handleDisconnect(final AgentAttache agent, final Status.Event event, final boolean investigate, final boolean broadcast, final boolean removeAgent) { boolean res; if (!investigate) { res = super.handleDisconnectWithoutInvestigation(agent, event, true, removeAgent); @@ -330,16 +329,16 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } @Override - public boolean executeUserRequest(long hostId, Event event) throws AgentUnavailableException { + public boolean executeUserRequest(final long hostId, final Event event) throws AgentUnavailableException { if (event == Event.AgentDisconnected) { if (s_logger.isDebugEnabled()) { s_logger.debug("Received agent disconnect event for host " + hostId); } - AgentAttache attache = findAttache(hostId); + final AgentAttache attache = findAttache(hostId); if (attache != null) { // don't process disconnect if the host is being rebalanced if (isAgentRebalanceEnabled()) { - HostTransferMapVO transferVO = _hostTransferDao.findById(hostId); + final HostTransferMapVO transferVO = _hostTransferDao.findById(hostId); if (transferVO != null) { if (transferVO.getFutureOwner() == _nodeId && transferVO.getState() == HostTransferState.TransferStarted) { s_logger.debug("Not processing " + Event.AgentDisconnected + " event for the host id=" + hostId + " as the host is being connected to " + @@ -374,7 +373,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust if (result != null) { return result; } - } catch (AgentUnavailableException e) { + } catch (final AgentUnavailableException e) { s_logger.debug("cannot propagate agent reconnect because agent is not available", e); return false; } @@ -382,9 +381,9 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust return super.reconnect(hostId); } - public void notifyNodesInCluster(AgentAttache attache) { + public void notifyNodesInCluster(final AgentAttache attache) { s_logger.debug("Notifying other nodes of to disconnect"); - Command[] cmds = new Command[] {new ChangeAgentCommand(attache.getId(), Event.AgentDisconnected)}; + final Command[] cmds = new Command[] {new ChangeAgentCommand(attache.getId(), Event.AgentDisconnected)}; _clusterMgr.broadcast(attache.getId(), _gson.toJson(cmds)); } @@ -393,26 +392,26 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust if (s_logger.isDebugEnabled()) { s_logger.debug("Notifying other MS nodes to run host scan task"); } - Command[] cmds = new Command[] {new ScheduleHostScanTaskCommand()}; + final Command[] cmds = new Command[] {new ScheduleHostScanTaskCommand()}; _clusterMgr.broadcast(0, _gson.toJson(cmds)); } - protected static void logT(byte[] bytes, final String msg) { + protected static void logT(final byte[] bytes, final String msg) { s_logger.trace("Seq " + Request.getAgentId(bytes) + "-" + Request.getSequence(bytes) + ": MgmtId " + Request.getManagementServerId(bytes) + ": " + (Request.isRequest(bytes) ? "Req: " : "Resp: ") + msg); } - protected static void logD(byte[] bytes, final String msg) { + protected static void logD(final byte[] bytes, final String msg) { s_logger.debug("Seq " + Request.getAgentId(bytes) + "-" + Request.getSequence(bytes) + ": MgmtId " + Request.getManagementServerId(bytes) + ": " + (Request.isRequest(bytes) ? "Req: " : "Resp: ") + msg); } - protected static void logI(byte[] bytes, final String msg) { + protected static void logI(final byte[] bytes, final String msg) { s_logger.info("Seq " + Request.getAgentId(bytes) + "-" + Request.getSequence(bytes) + ": MgmtId " + Request.getManagementServerId(bytes) + ": " + (Request.isRequest(bytes) ? "Req: " : "Resp: ") + msg); } - public boolean routeToPeer(String peer, byte[] bytes) { + public boolean routeToPeer(final String peer, final byte[] bytes) { int i = 0; SocketChannel ch = null; SSLEngine sslEngine = null; @@ -438,7 +437,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } Link.write(ch, new ByteBuffer[] {ByteBuffer.wrap(bytes)}, sslEngine); return true; - } catch (IOException e) { + } catch (final IOException e) { try { logI(bytes, "Unable to route to peer: " + Request.parse(bytes).toString() + " due to " + e.getMessage()); } catch (ClassNotFoundException | UnsupportedVersionException ex) { @@ -451,28 +450,28 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust return false; } - public String findPeer(long hostId) { + public String findPeer(final long hostId) { return getPeerName(hostId); } - public SSLEngine getSSLEngine(String peerName) { + public SSLEngine getSSLEngine(final String peerName) { return _sslEngines.get(peerName); } - public void cancel(String peerName, long hostId, long sequence, String reason) { - CancelCommand cancel = new CancelCommand(sequence, reason); - Request req = new Request(hostId, _nodeId, cancel, true); + public void cancel(final String peerName, final long hostId, final long sequence, final String reason) { + final CancelCommand cancel = new CancelCommand(sequence, reason); + final Request req = new Request(hostId, _nodeId, cancel, true); req.setControl(true); routeToPeer(peerName, req.getBytes()); } - public void closePeer(String peerName) { + public void closePeer(final String peerName) { synchronized (_peers) { - SocketChannel ch = _peers.get(peerName); + final SocketChannel ch = _peers.get(peerName); if (ch != null) { try { ch.close(); - } catch (IOException e) { + } catch (final IOException e) { s_logger.warn("Unable to close peer socket connection to " + peerName); } } @@ -499,6 +498,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } String ip = ms.getServiceIP(); InetAddress addr; + int port = Port.value(); try { addr = InetAddress.getByName(ip); } catch (UnknownHostException e) { @@ -506,21 +506,21 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } SocketChannel ch1 = null; try { - ch1 = SocketChannel.open(new InetSocketAddress(addr, Port.value())); + ch1 = SocketChannel.open(new InetSocketAddress(addr, port)); ch1.configureBlocking(false); ch1.socket().setKeepAlive(true); ch1.socket().setSoTimeout(60 * 1000); try { - SSLContext sslContext = Link.initSSLContext(true); - sslEngine = sslContext.createSSLEngine(ip, Port.value()); + SSLContext sslContext = Link.initClientSSLContext(); + sslEngine = sslContext.createSSLEngine(ip, port); sslEngine.setUseClientMode(true); sslEngine.setEnabledProtocols(SSLUtils.getSupportedProtocols(sslEngine.getEnabledProtocols())); sslEngine.beginHandshake(); if (!Link.doHandshake(ch1, sslEngine, true)) { ch1.close(); - throw new IOException("SSL handshake failed!"); + throw new IOException(String.format("SSL: Handshake failed with peer management server '%s' on %s:%d ", peerName, ip, port)); } - s_logger.info("SSL: Handshake done"); + s_logger.info(String.format("SSL: Handshake done with peer management server '%s' on %s:%d ", peerName, ip, port)); } catch (Exception e) { ch1.close(); throw new IOException("SSL: Fail to init SSL! " + e); @@ -531,11 +531,13 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust _peers.put(peerName, ch1); _sslEngines.put(peerName, sslEngine); return ch1; - } catch (IOException e) { - try { - ch1.close(); - } catch (IOException ex) { - s_logger.error("failed to close failed peer socket: " + ex); + } catch (final IOException e) { + if (ch1 != null) { + try { + ch1.close(); + } catch (final IOException ex) { + s_logger.error("failed to close failed peer socket: " + ex); + } } s_logger.warn("Unable to connect to peer management server: " + peerName + ", ip: " + ip + " due to " + e.getMessage(), e); return null; @@ -549,8 +551,8 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } } - public SocketChannel connectToPeer(long hostId, SocketChannel prevCh) { - String peerName = getPeerName(hostId); + public SocketChannel connectToPeer(final long hostId, final SocketChannel prevCh) { + final String peerName = getPeerName(hostId); if (peerName == null) { return null; } @@ -560,8 +562,8 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust @Override protected AgentAttache getAttache(final Long hostId) throws AgentUnavailableException { - assert (hostId != null) : "Who didn't check their id value?"; - HostVO host = _hostDao.findById(hostId); + assert hostId != null : "Who didn't check their id value?"; + final HostVO host = _hostDao.findById(hostId); if (host == null) { throw new AgentUnavailableException("Can't find the host ", hostId); } @@ -576,7 +578,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } } if (agent == null) { - AgentUnavailableException ex = new AgentUnavailableException("Host with specified id is not in the right state: " + host.getStatus(), hostId); + final AgentUnavailableException ex = new AgentUnavailableException("Host with specified id is not in the right state: " + host.getStatus(), hostId); ex.addProxyObject(_entityMgr.findById(Host.class, hostId).getUuid()); throw ex; } @@ -612,13 +614,13 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust public class ClusteredAgentHandler extends AgentHandler { - public ClusteredAgentHandler(Task.Type type, Link link, byte[] data) { + public ClusteredAgentHandler(final Task.Type type, final Link link, final byte[] data) { super(type, link, data); } @Override - protected void doTask(final Task task) throws Exception { - TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.CLOUD_DB); + protected void doTask(final Task task) throws TaskExecutionException { + final TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.CLOUD_DB); try { if (task.getType() != Task.Type.DATA) { super.doTask(task); @@ -626,37 +628,37 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } final byte[] data = task.getData(); - Version ver = Request.getVersion(data); + final Version ver = Request.getVersion(data); if (ver.ordinal() != Version.v1.ordinal() && ver.ordinal() != Version.v3.ordinal()) { s_logger.warn("Wrong version for clustered agent request"); super.doTask(task); return; } - long hostId = Request.getAgentId(data); - Link link = task.getLink(); + final long hostId = Request.getAgentId(data); + final Link link = task.getLink(); if (Request.fromServer(data)) { - AgentAttache agent = findAttache(hostId); + final AgentAttache agent = findAttache(hostId); if (Request.isControl(data)) { if (agent == null) { logD(data, "No attache to process cancellation"); return; } - Request req = Request.parse(data); - Command[] cmds = req.getCommands(); - CancelCommand cancel = (CancelCommand)cmds[0]; + final Request req = Request.parse(data); + final Command[] cmds = req.getCommands(); + final CancelCommand cancel = (CancelCommand)cmds[0]; if (s_logger.isDebugEnabled()) { logD(data, "Cancel request received"); } agent.cancel(cancel.getSequence()); final Long current = agent._currentSequence; // if the request is the current request, always have to trigger sending next request in -// sequence, + // sequence, // otherwise the agent queue will be blocked - if (req.executeInSequence() && (current != null && current == Request.getSequence(data))) { + if (req.executeInSequence() && current != null && current == Request.getSequence(data)) { agent.sendNext(Request.getSequence(data)); } return; @@ -671,29 +673,29 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust // route it to the agent. // But we have the serialize the control commands here so we have // to deserialize this and send it through the agent attache. - Request req = Request.parse(data); + final Request req = Request.parse(data); agent.send(req, null); return; } else { if (agent instanceof Routable) { - Routable cluster = (Routable)agent; + final Routable cluster = (Routable)agent; cluster.routeToAgent(data); } else { agent.send(Request.parse(data)); } return; } - } catch (AgentUnavailableException e) { + } catch (final AgentUnavailableException e) { logD(data, e.getMessage()); cancel(Long.toString(Request.getManagementServerId(data)), hostId, Request.getSequence(data), e.getMessage()); } } else { - long mgmtId = Request.getManagementServerId(data); + final long mgmtId = Request.getManagementServerId(data); if (mgmtId != -1 && mgmtId != _nodeId) { routeToPeer(Long.toString(mgmtId), data); if (Request.requiresSequentialExecution(data)) { - AgentAttache attache = (AgentAttache)link.attachment(); + final AgentAttache attache = (AgentAttache)link.attachment(); if (attache != null) { attache.sendNext(Request.getSequence(data)); } else if (s_logger.isDebugEnabled()) { @@ -707,7 +709,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } else { // received an answer. final Response response = Response.parse(data); - AgentAttache attache = findAttache(response.getAgentId()); + final AgentAttache attache = findAttache(response.getAgentId()); if (attache == null) { s_logger.info("SeqA " + response.getAgentId() + "-" + response.getSequence() + "Unable to find attache to forward " + response.toString()); return; @@ -719,6 +721,14 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust return; } } + } catch (final ClassNotFoundException e) { + final String message = String.format("ClassNotFoundException occured when executing taks! Error '%s'", e.getMessage()); + s_logger.error(message); + throw new TaskExecutionException(message, e); + } catch (final UnsupportedVersionException e) { + final String message = String.format("UnsupportedVersionException occured when executing taks! Error '%s'", e.getMessage()); + s_logger.error(message); + throw new TaskExecutionException(message, e); } finally { txn.close(); } @@ -747,7 +757,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } @Override - public void removeAgent(AgentAttache attache, Status nextState) { + public void removeAgent(final AgentAttache attache, final Status nextState) { if (attache == null) { return; } @@ -756,7 +766,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } @Override - public boolean executeRebalanceRequest(long agentId, long currentOwnerId, long futureOwnerId, Event event) throws AgentUnavailableException, + public boolean executeRebalanceRequest(final long agentId, final long currentOwnerId, final long futureOwnerId, final Event event) throws AgentUnavailableException, OperationTimedoutException { boolean result = false; if (event == Event.RequestAgentRebalance) { @@ -764,7 +774,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } else if (event == Event.StartAgentRebalance) { try { result = rebalanceHost(agentId, currentOwnerId, futureOwnerId); - } catch (Exception e) { + } catch (final Exception e) { s_logger.warn("Unable to rebalance host id=" + agentId, e); } } @@ -803,7 +813,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } cancelled = true; } - } catch (Throwable e) { + } catch (final Throwable e) { s_logger.error("Unexpected exception " + e.toString(), e); } } @@ -811,11 +821,11 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust public void startRebalanceAgents() { s_logger.debug("Management server " + _nodeId + " is asking other peers to rebalance their agents"); - List allMS = _mshostDao.listBy(ManagementServerHost.State.Up); - QueryBuilder sc = QueryBuilder.create(HostVO.class); + final List allMS = _mshostDao.listBy(ManagementServerHost.State.Up); + final QueryBuilder sc = QueryBuilder.create(HostVO.class); sc.and(sc.entity().getManagementServerId(), Op.NNULL); sc.and(sc.entity().getType(), Op.EQ, Host.Type.Routing); - List allManagedAgents = sc.list(); + final List allManagedAgents = sc.list(); int avLoad = 0; @@ -836,11 +846,11 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust avLoad = 1; } - for (ManagementServerHostVO node : allMS) { + for (final ManagementServerHostVO node : allMS) { if (node.getMsid() != _nodeId) { List hostsToRebalance = new ArrayList(); - for (AgentLoadBalancerPlanner lbPlanner : _lbPlanners) { + for (final AgentLoadBalancerPlanner lbPlanner : _lbPlanners) { hostsToRebalance = lbPlanner.getHostsToRebalance(node.getMsid(), avLoad); if (hostsToRebalance != null && !hostsToRebalance.isEmpty()) { break; @@ -851,8 +861,8 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust if (hostsToRebalance != null && !hostsToRebalance.isEmpty()) { s_logger.debug("Found " + hostsToRebalance.size() + " hosts to rebalance from management server " + node.getMsid()); - for (HostVO host : hostsToRebalance) { - long hostId = host.getId(); + for (final HostVO host : hostsToRebalance) { + final long hostId = host.getId(); s_logger.debug("Asking management server " + node.getMsid() + " to give away host id=" + hostId); boolean result = true; @@ -864,23 +874,23 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust HostTransferMapVO transfer = null; try { transfer = _hostTransferDao.startAgentTransfering(hostId, node.getMsid(), _nodeId); - Answer[] answer = sendRebalanceCommand(node.getMsid(), hostId, node.getMsid(), _nodeId, Event.RequestAgentRebalance); + final Answer[] answer = sendRebalanceCommand(node.getMsid(), hostId, node.getMsid(), _nodeId, Event.RequestAgentRebalance); if (answer == null) { s_logger.warn("Failed to get host id=" + hostId + " from management server " + node.getMsid()); result = false; } - } catch (Exception ex) { + } catch (final Exception ex) { s_logger.warn("Failed to get host id=" + hostId + " from management server " + node.getMsid(), ex); result = false; } finally { if (transfer != null) { - HostTransferMapVO transferState = _hostTransferDao.findByIdAndFutureOwnerId(transfer.getId(), _nodeId); + final HostTransferMapVO transferState = _hostTransferDao.findByIdAndFutureOwnerId(transfer.getId(), _nodeId); if (!result && transferState != null && transferState.getState() == HostTransferState.TransferRequested) { if (s_logger.isDebugEnabled()) { s_logger.debug("Removing mapping from op_host_transfer as it failed to be set to transfer mode"); } // just remove the mapping (if exists) as nothing was done on the peer management -// server yet + // server yet _hostTransferDao.remove(transfer.getId()); } } @@ -893,31 +903,31 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } } - private Answer[] sendRebalanceCommand(long peer, long agentId, long currentOwnerId, long futureOwnerId, Event event) { - TransferAgentCommand transfer = new TransferAgentCommand(agentId, currentOwnerId, futureOwnerId, event); - Commands commands = new Commands(Command.OnError.Stop); + private Answer[] sendRebalanceCommand(final long peer, final long agentId, final long currentOwnerId, final long futureOwnerId, final Event event) { + final TransferAgentCommand transfer = new TransferAgentCommand(agentId, currentOwnerId, futureOwnerId, event); + final Commands commands = new Commands(Command.OnError.Stop); commands.addCommand(transfer); - Command[] cmds = commands.toCommands(); + final Command[] cmds = commands.toCommands(); try { if (s_logger.isDebugEnabled()) { s_logger.debug("Forwarding " + cmds[0].toString() + " to " + peer); } - String peerName = Long.toString(peer); - String cmdStr = _gson.toJson(cmds); - String ansStr = _clusterMgr.execute(peerName, agentId, cmdStr, true); - Answer[] answers = _gson.fromJson(ansStr, Answer[].class); + final String peerName = Long.toString(peer); + final String cmdStr = _gson.toJson(cmds); + final String ansStr = _clusterMgr.execute(peerName, agentId, cmdStr, true); + final Answer[] answers = _gson.fromJson(ansStr, Answer[].class); return answers; - } catch (Exception e) { + } catch (final Exception e) { s_logger.warn("Caught exception while talking to " + currentOwnerId, e); return null; } } - public String getPeerName(long agentHostId) { + public String getPeerName(final long agentHostId) { - HostVO host = _hostDao.findById(agentHostId); + final HostVO host = _hostDao.findById(agentHostId); if (host != null && host.getManagementServerId() != null) { if (_clusterMgr.getSelfPeerName().equals(Long.toString(host.getManagementServerId()))) { return null; @@ -928,7 +938,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust return null; } - public Boolean propagateAgentEvent(long agentId, Event event) throws AgentUnavailableException { + public Boolean propagateAgentEvent(final long agentId, final Event event) throws AgentUnavailableException { final String msPeer = getPeerName(agentId); if (msPeer == null) { return null; @@ -937,15 +947,15 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust if (s_logger.isDebugEnabled()) { s_logger.debug("Propagating agent change request event:" + event.toString() + " to agent:" + agentId); } - Command[] cmds = new Command[1]; + final Command[] cmds = new Command[1]; cmds[0] = new ChangeAgentCommand(agentId, event); - String ansStr = _clusterMgr.execute(msPeer, agentId, _gson.toJson(cmds), true); + final String ansStr = _clusterMgr.execute(msPeer, agentId, _gson.toJson(cmds), true); if (ansStr == null) { throw new AgentUnavailableException(agentId); } - Answer[] answers = _gson.fromJson(ansStr, Answer[].class); + final Answer[] answers = _gson.fromJson(ansStr, Answer[].class); if (s_logger.isDebugEnabled()) { s_logger.debug("Result for agent change is " + answers[0].getResult()); @@ -966,9 +976,9 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust if (_agentToTransferIds.size() > 0) { s_logger.debug("Found " + _agentToTransferIds.size() + " agents to transfer"); // for (Long hostId : _agentToTransferIds) { - for (Iterator iterator = _agentToTransferIds.iterator(); iterator.hasNext();) { - Long hostId = iterator.next(); - AgentAttache attache = findAttache(hostId); + for (final Iterator iterator = _agentToTransferIds.iterator(); iterator.hasNext();) { + final Long hostId = iterator.next(); + final AgentAttache attache = findAttache(hostId); // if the thread: // 1) timed out waiting for the host to reconnect @@ -976,8 +986,8 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust // 3) if the management server doesn't own the host any more // remove the host from re-balance list and delete from op_host_transfer DB // no need to do anything with the real attache as we haven't modified it yet - Date cutTime = DateUtil.currentGMTTime(); - HostTransferMapVO transferMap = + final Date cutTime = DateUtil.currentGMTTime(); + final HostTransferMapVO transferMap = _hostTransferDao.findActiveHostTransferMapByHostId(hostId, new Date(cutTime.getTime() - rebalanceTimeOut)); if (transferMap == null) { @@ -994,7 +1004,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust continue; } - ManagementServerHostVO ms = _mshostDao.findByMsid(transferMap.getFutureOwner()); + final ManagementServerHostVO ms = _mshostDao.findByMsid(transferMap.getFutureOwner()); if (ms != null && ms.getState() != ManagementServerHost.State.Up) { s_logger.debug("Can't transfer host " + hostId + " as it's future owner is not in UP state: " + ms + ", skipping rebalance for the host"); @@ -1007,7 +1017,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust iterator.remove(); try { _executor.execute(new RebalanceTask(hostId, transferMap.getInitialOwner(), transferMap.getFutureOwner())); - } catch (RejectedExecutionException ex) { + } catch (final RejectedExecutionException ex) { s_logger.warn("Failed to submit rebalance task for host id=" + hostId + "; postponing the execution"); continue; } @@ -1024,21 +1034,21 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } } - } catch (Throwable e) { + } catch (final Throwable e) { s_logger.error("Problem with the clustered agent transfer scan check!", e); } } }; } - private boolean setToWaitForRebalance(final long hostId, long currentOwnerId, long futureOwnerId) { + private boolean setToWaitForRebalance(final long hostId, final long currentOwnerId, final long futureOwnerId) { s_logger.debug("Adding agent " + hostId + " to the list of agents to transfer"); synchronized (_agentToTransferIds) { return _agentToTransferIds.add(hostId); } } - protected boolean rebalanceHost(final long hostId, long currentOwnerId, long futureOwnerId) throws AgentUnavailableException { + protected boolean rebalanceHost(final long hostId, final long currentOwnerId, final long futureOwnerId) throws AgentUnavailableException { boolean result = true; if (currentOwnerId == _nodeId) { @@ -1048,12 +1058,12 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust return false; } try { - Answer[] answer = sendRebalanceCommand(futureOwnerId, hostId, currentOwnerId, futureOwnerId, Event.StartAgentRebalance); + final Answer[] answer = sendRebalanceCommand(futureOwnerId, hostId, currentOwnerId, futureOwnerId, Event.StartAgentRebalance); if (answer == null || !answer[0].getResult()) { result = false; } - } catch (Exception ex) { + } catch (final Exception ex) { s_logger.warn("Host " + hostId + " failed to connect to the management server " + futureOwnerId + " as a part of rebalance process", ex); result = false; } @@ -1067,13 +1077,13 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } } else if (futureOwnerId == _nodeId) { - HostVO host = _hostDao.findById(hostId); + final HostVO host = _hostDao.findById(hostId); try { if (s_logger.isDebugEnabled()) { s_logger.debug("Disconnecting host " + host.getId() + "(" + host.getName() + " as a part of rebalance process without notification"); } - AgentAttache attache = findAttache(hostId); + final AgentAttache attache = findAttache(hostId); if (attache != null) { result = handleDisconnect(attache, Event.AgentDisconnected, false, false, true); } @@ -1088,7 +1098,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust s_logger.warn("Failed to disconnect " + host.getId() + "(" + host.getName() + " as a part of rebalance process without notification"); } - } catch (Exception ex) { + } catch (final Exception ex) { s_logger.warn("Failed to load directly connected host " + host.getId() + "(" + host.getName() + ") to the management server " + _nodeId + " as a part of rebalance process due to:", ex); result = false; @@ -1106,21 +1116,21 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust return result; } - protected void finishRebalance(final long hostId, long futureOwnerId, Event event) { + protected void finishRebalance(final long hostId, final long futureOwnerId, final Event event) { - boolean success = (event == Event.RebalanceCompleted) ? true : false; + final boolean success = event == Event.RebalanceCompleted ? true : false; if (s_logger.isDebugEnabled()) { s_logger.debug("Finishing rebalancing for the agent " + hostId + " with event " + event); } - AgentAttache attache = findAttache(hostId); + final AgentAttache attache = findAttache(hostId); if (attache == null || !(attache instanceof ClusteredAgentAttache)) { s_logger.debug("Unable to find forward attache for the host id=" + hostId + ", assuming that the agent disconnected already"); _hostTransferDao.completeAgentTransfer(hostId); return; } - ClusteredAgentAttache forwardAttache = (ClusteredAgentAttache)attache; + final ClusteredAgentAttache forwardAttache = (ClusteredAgentAttache)attache; if (success) { @@ -1132,7 +1142,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust while (requestToTransfer != null) { s_logger.debug("Forwarding request " + requestToTransfer.getSequence() + " held in transfer attache " + hostId + " from the management server " + _nodeId + " to " + futureOwnerId); - boolean routeResult = routeToPeer(Long.toString(futureOwnerId), requestToTransfer.getBytes()); + final boolean routeResult = routeToPeer(Long.toString(futureOwnerId), requestToTransfer.getBytes()); if (!routeResult) { logD(requestToTransfer.getBytes(), "Failed to route request to peer"); } @@ -1155,13 +1165,13 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust s_logger.debug("Management server " + _nodeId + " failed to rebalance agent " + hostId); _hostTransferDao.completeAgentTransfer(hostId); handleDisconnectWithoutInvestigation(findAttache(hostId), Event.RebalanceFailed, true, true); - } catch (Exception ex) { + } catch (final Exception ex) { s_logger.warn("Failed to reconnect host id=" + hostId + " as a part of failed rebalance task cleanup"); } } protected boolean startRebalance(final long hostId) { - HostVO host = _hostDao.findById(hostId); + final HostVO host = _hostDao.findById(hostId); if (host == null || host.getRemoved() != null) { s_logger.warn("Unable to find host record, fail start rebalancing process"); @@ -1169,10 +1179,10 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } synchronized (_agents) { - ClusteredDirectAgentAttache attache = (ClusteredDirectAgentAttache)_agents.get(hostId); + final ClusteredDirectAgentAttache attache = (ClusteredDirectAgentAttache)_agents.get(hostId); if (attache != null && attache.getQueueSize() == 0 && attache.getNonRecurringListenersSize() == 0) { handleDisconnectWithoutInvestigation(attache, Event.StartAgentRebalance, true, true); - ClusteredAgentAttache forwardAttache = (ClusteredAgentAttache)createAttache(hostId); + final ClusteredAgentAttache forwardAttache = (ClusteredAgentAttache)createAttache(hostId); if (forwardAttache == null) { s_logger.warn("Unable to create a forward attache for the host " + hostId + " as a part of rebalance process"); return false; @@ -1194,15 +1204,15 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust return true; } - protected void cleanupTransferMap(long msId) { - List hostsJoingingCluster = _hostTransferDao.listHostsJoiningCluster(msId); + protected void cleanupTransferMap(final long msId) { + final List hostsJoingingCluster = _hostTransferDao.listHostsJoiningCluster(msId); - for (HostTransferMapVO hostJoingingCluster : hostsJoingingCluster) { + for (final HostTransferMapVO hostJoingingCluster : hostsJoingingCluster) { _hostTransferDao.remove(hostJoingingCluster.getId()); } - List hostsLeavingCluster = _hostTransferDao.listHostsLeavingCluster(msId); - for (HostTransferMapVO hostLeavingCluster : hostsLeavingCluster) { + final List hostsLeavingCluster = _hostTransferDao.listHostsLeavingCluster(msId); + for (final HostTransferMapVO hostLeavingCluster : hostsLeavingCluster) { _hostTransferDao.remove(hostLeavingCluster.getId()); } } @@ -1212,7 +1222,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust Long currentOwnerId = null; Long futureOwnerId = null; - public RebalanceTask(long hostId, long currentOwnerId, long futureOwnerId) { + public RebalanceTask(final long hostId, final long currentOwnerId, final long futureOwnerId) { this.hostId = hostId; this.currentOwnerId = currentOwnerId; this.futureOwnerId = futureOwnerId; @@ -1225,20 +1235,20 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust s_logger.debug("Rebalancing host id=" + hostId); } rebalanceHost(hostId, currentOwnerId, futureOwnerId); - } catch (Exception e) { + } catch (final Exception e) { s_logger.warn("Unable to rebalance host id=" + hostId, e); } } } - private String handleScheduleHostScanTaskCommand(ScheduleHostScanTaskCommand cmd) { + private String handleScheduleHostScanTaskCommand(final ScheduleHostScanTaskCommand cmd) { if (s_logger.isDebugEnabled()) { s_logger.debug("Intercepting resource manager command: " + _gson.toJson(cmd)); } try { scheduleHostScanTask(); - } catch (Exception e) { + } catch (final Exception e) { // Scheduling host scan task in peer MS is a best effort operation during host add, regular host scan // happens at fixed intervals anyways. So handling any exceptions that may be thrown s_logger.warn("Exception happened while trying to schedule host scan task on mgmt server " + _clusterMgr.getSelfPeerName() + @@ -1246,14 +1256,14 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust return null; } - Answer[] answers = new Answer[1]; + final Answer[] answers = new Answer[1]; answers[0] = new Answer(cmd, true, null); return _gson.toJson(answers); } - public Answer[] sendToAgent(Long hostId, Command[] cmds, boolean stopOnError) throws AgentUnavailableException, OperationTimedoutException { - Commands commands = new Commands(stopOnError ? Command.OnError.Stop : Command.OnError.Continue); - for (Command cmd : cmds) { + public Answer[] sendToAgent(final Long hostId, final Command[] cmds, final boolean stopOnError) throws AgentUnavailableException, OperationTimedoutException { + final Commands commands = new Commands(stopOnError ? Command.OnError.Stop : Command.OnError.Continue); + for (final Command cmd : cmds) { commands.addCommand(cmd); } return send(hostId, commands); @@ -1266,7 +1276,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } @Override - public String dispatch(ClusterServicePdu pdu) { + public String dispatch(final ClusterServicePdu pdu) { if (s_logger.isDebugEnabled()) { s_logger.debug("Dispatch ->" + pdu.getAgentId() + ", json: " + pdu.getJsonPackage()); @@ -1275,13 +1285,13 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust Command[] cmds = null; try { cmds = _gson.fromJson(pdu.getJsonPackage(), Command[].class); - } catch (Throwable e) { - assert (false); + } catch (final Throwable e) { + assert false; s_logger.error("Excection in gson decoding : ", e); } if (cmds.length == 1 && cmds[0] instanceof ChangeAgentCommand) { // intercepted - ChangeAgentCommand cmd = (ChangeAgentCommand)cmds[0]; + final ChangeAgentCommand cmd = (ChangeAgentCommand)cmds[0]; if (s_logger.isDebugEnabled()) { s_logger.debug("Intercepting command for agent change: agent " + cmd.getAgentId() + " event: " + cmd.getEvent()); @@ -1293,16 +1303,16 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust s_logger.debug("Result is " + result); } - } catch (AgentUnavailableException e) { + } catch (final AgentUnavailableException e) { s_logger.warn("Agent is unavailable", e); return null; } - Answer[] answers = new Answer[1]; + final Answer[] answers = new Answer[1]; answers[0] = new ChangeAgentAnswer(cmd, result); return _gson.toJson(answers); } else if (cmds.length == 1 && cmds[0] instanceof TransferAgentCommand) { - TransferAgentCommand cmd = (TransferAgentCommand)cmds[0]; + final TransferAgentCommand cmd = (TransferAgentCommand)cmds[0]; if (s_logger.isDebugEnabled()) { s_logger.debug("Intercepting command for agent rebalancing: agent " + cmd.getAgentId() + " event: " + cmd.getEvent()); @@ -1314,18 +1324,18 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust s_logger.debug("Result is " + result); } - } catch (AgentUnavailableException e) { + } catch (final AgentUnavailableException e) { s_logger.warn("Agent is unavailable", e); return null; - } catch (OperationTimedoutException e) { + } catch (final OperationTimedoutException e) { s_logger.warn("Operation timed out", e); return null; } - Answer[] answers = new Answer[1]; + final Answer[] answers = new Answer[1]; answers[0] = new Answer(cmd, result, null); return _gson.toJson(answers); } else if (cmds.length == 1 && cmds[0] instanceof PropagateResourceEventCommand) { - PropagateResourceEventCommand cmd = (PropagateResourceEventCommand)cmds[0]; + final PropagateResourceEventCommand cmd = (PropagateResourceEventCommand)cmds[0]; s_logger.debug("Intercepting command to propagate event " + cmd.getEvent().name() + " for host " + cmd.getHostId()); @@ -1333,29 +1343,29 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust try { result = _resourceMgr.executeUserRequest(cmd.getHostId(), cmd.getEvent()); s_logger.debug("Result is " + result); - } catch (AgentUnavailableException ex) { + } catch (final AgentUnavailableException ex) { s_logger.warn("Agent is unavailable", ex); return null; } - Answer[] answers = new Answer[1]; + final Answer[] answers = new Answer[1]; answers[0] = new Answer(cmd, result, null); return _gson.toJson(answers); } else if (cmds.length == 1 && cmds[0] instanceof ScheduleHostScanTaskCommand) { - ScheduleHostScanTaskCommand cmd = (ScheduleHostScanTaskCommand)cmds[0]; - String response = handleScheduleHostScanTaskCommand(cmd); + final ScheduleHostScanTaskCommand cmd = (ScheduleHostScanTaskCommand)cmds[0]; + final String response = handleScheduleHostScanTaskCommand(cmd); return response; } try { - long startTick = System.currentTimeMillis(); + final long startTick = System.currentTimeMillis(); if (s_logger.isDebugEnabled()) { s_logger.debug("Dispatch -> " + pdu.getAgentId() + ", json: " + pdu.getJsonPackage()); } - Answer[] answers = sendToAgent(pdu.getAgentId(), cmds, pdu.isStopOnError()); + final Answer[] answers = sendToAgent(pdu.getAgentId(), cmds, pdu.isStopOnError()); if (answers != null) { - String jsonReturn = _gson.toJson(answers); + final String jsonReturn = _gson.toJson(answers); if (s_logger.isDebugEnabled()) { s_logger.debug("Completed dispatching -> " + pdu.getAgentId() + ", json: " + pdu.getJsonPackage() + " in " + @@ -1369,9 +1379,9 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust (System.currentTimeMillis() - startTick) + " ms, return null result"); } } - } catch (AgentUnavailableException e) { + } catch (final AgentUnavailableException e) { s_logger.warn("Agent is unavailable", e); - } catch (OperationTimedoutException e) { + } catch (final OperationTimedoutException e) { s_logger.warn("Timed Out", e); } @@ -1380,11 +1390,11 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } - public boolean executeAgentUserRequest(long agentId, Event event) throws AgentUnavailableException { + public boolean executeAgentUserRequest(final long agentId, final Event event) throws AgentUnavailableException { return executeUserRequest(agentId, event); } - public boolean rebalanceAgent(long agentId, Event event, long currentOwnerId, long futureOwnerId) throws AgentUnavailableException, OperationTimedoutException { + public boolean rebalanceAgent(final long agentId, final Event event, final long currentOwnerId, final long futureOwnerId) throws AgentUnavailableException, OperationTimedoutException { return executeRebalanceRequest(agentId, currentOwnerId, futureOwnerId, event); } @@ -1401,20 +1411,20 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust s_logger.trace("Agent rebalance task check, management server id:" + _nodeId); } // initiate agent lb task will be scheduled and executed only once, and only when number of agents -// loaded exceeds _connectedAgentsThreshold + // loaded exceeds _connectedAgentsThreshold if (!_agentLbHappened) { QueryBuilder sc = QueryBuilder.create(HostVO.class); sc.and(sc.entity().getManagementServerId(), Op.NNULL); sc.and(sc.entity().getType(), Op.EQ, Host.Type.Routing); - List allManagedRoutingAgents = sc.list(); + final List allManagedRoutingAgents = sc.list(); sc = QueryBuilder.create(HostVO.class); sc.and(sc.entity().getType(), Op.EQ, Host.Type.Routing); - List allAgents = sc.list(); - double allHostsCount = allAgents.size(); - double managedHostsCount = allManagedRoutingAgents.size(); + final List allAgents = sc.list(); + final double allHostsCount = allAgents.size(); + final double managedHostsCount = allManagedRoutingAgents.size(); if (allHostsCount > 0.0) { - double load = managedHostsCount / allHostsCount; + final double load = managedHostsCount / allHostsCount; if (load >= ConnectedAgentThreshold.value()) { s_logger.debug("Scheduling agent rebalancing task as the average agent load " + load + " is more than the threshold " + ConnectedAgentThreshold.value()); @@ -1426,7 +1436,7 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust } } } - } catch (Throwable e) { + } catch (final Throwable e) { s_logger.error("Problem with the clustered agent transfer scan check!", e); } } @@ -1448,9 +1458,9 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust @Override public ConfigKey[] getConfigKeys() { - ConfigKey[] keys = super.getConfigKeys(); + final ConfigKey[] keys = super.getConfigKeys(); - List> keysLst = new ArrayList>(); + final List> keysLst = new ArrayList>(); keysLst.addAll(Arrays.asList(keys)); keysLst.add(EnableLB); keysLst.add(ConnectedAgentThreshold); diff --git a/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java index 033cd7ab032..ad0b45cf9c1 100755 --- a/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java @@ -22,6 +22,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; @@ -38,15 +39,15 @@ import javax.ejb.Local; import javax.inject.Inject; import javax.naming.ConfigurationException; -import org.apache.log4j.Logger; - import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; +import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; +import org.apache.cloudstack.framework.ca.Certificate; import org.apache.cloudstack.framework.config.ConfigDepot; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; @@ -68,6 +69,7 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.log4j.Logger; import com.cloud.agent.AgentManager; import com.cloud.agent.Listener; @@ -96,6 +98,7 @@ import com.cloud.agent.api.StopCommand; import com.cloud.agent.api.UnPlugNicAnswer; import com.cloud.agent.api.UnPlugNicCommand; import com.cloud.agent.api.UnregisterVMCommand; +import com.cloud.agent.api.routing.NetworkElementCommand; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.GPUDeviceTO; import com.cloud.agent.api.to.NicTO; @@ -204,6 +207,7 @@ import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.snapshot.VMSnapshotManager; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.google.common.base.Strings; @Local(value = VirtualMachineManager.class) public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMachineManager, VmWorkJobHandler, Listener, Configurable { @@ -275,6 +279,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac protected VGPUTypesDao _vgpuTypesDao; @Inject protected EntityManager _entityMgr; + @Inject + protected CAManager caManager; @Inject ConfigDepot _configDepot; @@ -1004,7 +1010,6 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac cmds.addCommand(new StartCommand(vmTO, dest.getHost(), getExecuteInSequence(vm.getHypervisorType()))); - vmGuru.finalizeDeployment(cmds, vmProfile, dest, ctx); work = _workDao.findById(work.getId()); @@ -1046,6 +1051,24 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac if (s_logger.isDebugEnabled()) { s_logger.debug("Start completed for VM " + vm); } + final Host vmHost = _hostDao.findById(destHostId); + if (vmHost != null && (VirtualMachine.Type.ConsoleProxy.equals(vm.getType()) || + VirtualMachine.Type.SecondaryStorageVm.equals(vm.getType())) && caManager.canProvisionCertificates()) { + final Map sshAccessDetails = _networkMgr.getSystemVMAccessDetails(vm); + final String csr = caManager.generateKeyStoreAndCsr(vmHost, sshAccessDetails); + if (!Strings.isNullOrEmpty(csr)) { + final Map ipAddressDetails = new HashMap<>(sshAccessDetails); + ipAddressDetails.remove(NetworkElementCommand.ROUTER_NAME); + final Certificate certificate = caManager.issueCertificate(csr, Arrays.asList(vm.getHostName(), vm.getInstanceName()), new ArrayList<>(ipAddressDetails.values()), CAManager.CertValidityPeriod.value(), null); + final boolean result = caManager.deployCertificate(vmHost, certificate, false, sshAccessDetails); + if (!result) { + s_logger.error("Failed to setup certificate for system vm: " + vm.getInstanceName()); + } + } else { + s_logger.error("Failed to setup keystore and generate CSR for system vm: " + vm.getInstanceName()); + } + + } return; } else { if (s_logger.isDebugEnabled()) { diff --git a/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 093f1138215..d8efc4cba1f 100755 --- a/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -37,8 +37,6 @@ import javax.ejb.Local; import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.network.Networks; -import org.apache.log4j.Logger; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.cloud.entity.api.db.VMNetworkMapVO; @@ -53,6 +51,7 @@ import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.PublishScope; import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.cloudstack.region.PortableIpDao; +import org.apache.log4j.Logger; import com.cloud.agent.AgentManager; import com.cloud.agent.Listener; @@ -64,6 +63,7 @@ import com.cloud.agent.api.CheckNetworkCommand; import com.cloud.agent.api.Command; import com.cloud.agent.api.StartupCommand; import com.cloud.agent.api.StartupRoutingCommand; +import com.cloud.agent.api.routing.NetworkElementCommand; import com.cloud.agent.api.to.NicTO; import com.cloud.alert.AlertManager; import com.cloud.configuration.ConfigurationManager; @@ -110,6 +110,7 @@ import com.cloud.network.NetworkMigrationResponder; import com.cloud.network.NetworkModel; import com.cloud.network.NetworkProfile; import com.cloud.network.NetworkStateListener; +import com.cloud.network.Networks; import com.cloud.network.Networks.BroadcastDomainType; import com.cloud.network.Networks.TrafficType; import com.cloud.network.PhysicalNetwork; @@ -207,6 +208,7 @@ import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; +import com.google.common.base.Strings; /** * NetworkManagerImpl implements NetworkManager. @@ -3195,6 +3197,38 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra return profiles; } + @Override + public Map getSystemVMAccessDetails(final VirtualMachine vm) { + final Map accessDetails = new HashMap<>(); + accessDetails.put(NetworkElementCommand.ROUTER_NAME, vm.getInstanceName()); + String privateIpAddress = null; + for (final NicProfile profile : getNicProfiles(vm)) { + if (profile == null) { + continue; + } + final Network network = _networksDao.findById(profile.getNetworkId()); + if (network == null) { + continue; + } + if (network.getTrafficType() == Networks.TrafficType.Control) { + accessDetails.put(NetworkElementCommand.ROUTER_IP, profile.getIp4Address()); + } + if (network.getTrafficType() == Networks.TrafficType.Guest) { + accessDetails.put(NetworkElementCommand.ROUTER_GUEST_IP, profile.getIp4Address()); + } + if (network.getTrafficType() == Networks.TrafficType.Management) { + privateIpAddress = profile.getIp4Address(); + } + if (network.getTrafficType() != null && !Strings.isNullOrEmpty(profile.getIp4Address())) { + accessDetails.put(network.getTrafficType().name(), profile.getIp4Address()); + } + } + if (privateIpAddress != null && Strings.isNullOrEmpty(accessDetails.get(NetworkElementCommand.ROUTER_IP))) { + accessDetails.put(NetworkElementCommand.ROUTER_IP, privateIpAddress); + } + return accessDetails; + } + protected boolean stateTransitTo(NetworkVO network, Network.Event e) throws NoTransitionException { return _stateMachine.transitTo(network, e, null, _networksDao); } diff --git a/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 8b51bad52cc..066d93ea03f 100644 --- a/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -130,6 +130,7 @@ + diff --git a/engine/schema/src/com/cloud/certificate/CrlVO.java b/engine/schema/src/com/cloud/certificate/CrlVO.java new file mode 100644 index 00000000000..6df7530b290 --- /dev/null +++ b/engine/schema/src/com/cloud/certificate/CrlVO.java @@ -0,0 +1,85 @@ +// 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.certificate; + +import java.math.BigInteger; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.api.InternalIdentity; + +@Entity +@Table(name = "crl") +public class CrlVO implements InternalIdentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id = null; + + @Column(name = "serial") + private String certSerial; + + @Column(name = "cn") + private String certCn; + + @Column(name = "revoker_uuid") + private String revokerUuid; + + @Temporal(value = TemporalType.TIMESTAMP) + @Column(name = "revoked", updatable = true) + private Date revoked; + + public CrlVO() { + } + + public CrlVO(final BigInteger certSerial, final String certCn, final String revokerUuid) { + this.certSerial = certSerial.toString(16); + this.certCn = certCn; + this.revokerUuid = revokerUuid; + this.revoked = new Date(); + } + + @Override + public long getId() { + return id; + } + + public BigInteger getCertSerial() { + return new BigInteger(certSerial, 16); + } + + public String getCertCn() { + return certCn; + } + + public String getRevokerUuid() { + return revokerUuid; + } + + public Date getRevoked() { + return revoked; + } +} diff --git a/engine/schema/src/com/cloud/certificate/dao/CrlDao.java b/engine/schema/src/com/cloud/certificate/dao/CrlDao.java new file mode 100644 index 00000000000..613d9caf487 --- /dev/null +++ b/engine/schema/src/com/cloud/certificate/dao/CrlDao.java @@ -0,0 +1,28 @@ +// 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.certificate.dao; + +import java.math.BigInteger; + +import com.cloud.certificate.CrlVO; +import com.cloud.utils.db.GenericDao; + +public interface CrlDao extends GenericDao { + CrlVO findBySerial(final BigInteger certSerial); + CrlVO revokeCertificate(final BigInteger certSerial, final String certCn); +} \ No newline at end of file diff --git a/engine/schema/src/com/cloud/certificate/dao/CrlDaoImpl.java b/engine/schema/src/com/cloud/certificate/dao/CrlDaoImpl.java new file mode 100644 index 00000000000..2eee308f115 --- /dev/null +++ b/engine/schema/src/com/cloud/certificate/dao/CrlDaoImpl.java @@ -0,0 +1,57 @@ +// 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.certificate.dao; + +import java.math.BigInteger; + +import org.apache.cloudstack.context.CallContext; + +import com.cloud.certificate.CrlVO; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@DB +public class CrlDaoImpl extends GenericDaoBase implements CrlDao { + + private final SearchBuilder CrlBySerialSearch; + + public CrlDaoImpl() { + super(); + + CrlBySerialSearch = createSearchBuilder(); + CrlBySerialSearch.and("certSerial", CrlBySerialSearch.entity().getCertSerial(), SearchCriteria.Op.EQ); + CrlBySerialSearch.done(); + } + + @Override + public CrlVO findBySerial(final BigInteger certSerial) { + if (certSerial == null) { + return null; + } + final SearchCriteria sc = CrlBySerialSearch.create("certSerial", certSerial.toString(16)); + return findOneBy(sc); + } + + @Override + public CrlVO revokeCertificate(final BigInteger certSerial, final String certCn) { + final CrlVO revokedCertificate = new CrlVO(certSerial, certCn == null ? "" : certCn, CallContext.current().getCallingUserUuid()); + return persist(revokedCertificate); + } +} diff --git a/engine/schema/src/com/cloud/host/dao/HostDao.java b/engine/schema/src/com/cloud/host/dao/HostDao.java index 26e0644c59e..c7d84d64e0f 100755 --- a/engine/schema/src/com/cloud/host/dao/HostDao.java +++ b/engine/schema/src/com/cloud/host/dao/HostDao.java @@ -94,4 +94,6 @@ public interface HostDao extends GenericDao, StateDao listAllHostsByType(Host.Type type); HostVO findByPublicIp(String publicIp); + + HostVO findByIp(String publicIp); } diff --git a/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java index c04183a7725..1b6935915ff 100755 --- a/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java @@ -85,6 +85,7 @@ public class HostDaoImpl extends GenericDaoBase implements HostDao protected SearchBuilder DcPrivateIpAddressSearch; protected SearchBuilder DcStorageIpAddressSearch; protected SearchBuilder PublicIpAddressSearch; + protected SearchBuilder AnyIpAddressSearch; protected SearchBuilder GuidSearch; protected SearchBuilder DcSearch; @@ -212,6 +213,11 @@ public class HostDaoImpl extends GenericDaoBase implements HostDao PublicIpAddressSearch.and("publicIpAddress", PublicIpAddressSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); PublicIpAddressSearch.done(); + AnyIpAddressSearch = createSearchBuilder(); + AnyIpAddressSearch.or("publicIpAddress", AnyIpAddressSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); + AnyIpAddressSearch.or("privateIpAddress", AnyIpAddressSearch.entity().getPrivateIpAddress(), SearchCriteria.Op.EQ); + AnyIpAddressSearch.done(); + GuidSearch = createSearchBuilder(); GuidSearch.and("guid", GuidSearch.entity().getGuid(), SearchCriteria.Op.EQ); GuidSearch.done(); @@ -1088,6 +1094,13 @@ public class HostDaoImpl extends GenericDaoBase implements HostDao return findOneBy(sc); } + @Override + public HostVO findByIp(final String ipAddress) { + SearchCriteria sc = AnyIpAddressSearch.create(); + sc.setParameters("publicIpAddress", ipAddress); + sc.setParameters("privateIpAddress", ipAddress); + return findOneBy(sc); + } @Override public List findHypervisorHostInCluster(long clusterId) { diff --git a/framework/ca/pom.xml b/framework/ca/pom.xml new file mode 100644 index 00000000000..9374aaa08a7 --- /dev/null +++ b/framework/ca/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + cloud-framework-ca + Apache CloudStack Framework - Certificate Authority + + org.apache.cloudstack + cloudstack-framework + 4.5.3-SNAPSHOT + ../pom.xml + + diff --git a/framework/ca/src/org/apache/cloudstack/framework/ca/CAProvider.java b/framework/ca/src/org/apache/cloudstack/framework/ca/CAProvider.java new file mode 100644 index 00000000000..eacc1660e2e --- /dev/null +++ b/framework/ca/src/org/apache/cloudstack/framework/ca/CAProvider.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 org.apache.cloudstack.framework.ca; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +public interface CAProvider { + + /** + * Method returns capability of the plugin to participate in certificate issuance, revocation and provisioning + * @return + */ + boolean canProvisionCertificates(); + + /** + * Returns root CA certificate + * @return returns concatenated root CA certificate string + */ + List getCaCertificate(); + + /** + * Issues certificate with provided options + * @param domainNames + * @param ipAddresses + * @param validityDays + * @return + */ + Certificate issueCertificate(final List domainNames, final List ipAddresses, final int validityDays); + + /** + * Issues certificate using given CSR and other options + * @param csr + * @param domainNames + * @param ipAddresses + * @param validityDays + * @return + */ + Certificate issueCertificate(final String csr, final List domainNames, final List ipAddresses, final int validityDays); + + /** + * Revokes certificate using certificate serial and CN + * @param certSerial + * @param certCn + * @return returns true on success + */ + boolean revokeCertificate(final BigInteger certSerial, final String certCn); + + /** + * This method can add/inject custom TrustManagers for client connection validations. + * @param sslContext The SSL context used while accepting a client connection + * @param remoteAddress + * @param certMap + * @return + * @throws GeneralSecurityException + * @throws IOException + */ + SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress, final Map certMap) throws GeneralSecurityException, IOException; + + /** + * Returns the unique name of the provider + * @return + */ + String getProviderName(); + + /** + * Returns description about the CA provider plugin + * @return + */ + String getDescription(); +} diff --git a/framework/ca/src/org/apache/cloudstack/framework/ca/CAService.java b/framework/ca/src/org/apache/cloudstack/framework/ca/CAService.java new file mode 100644 index 00000000000..3aacb3b2b85 --- /dev/null +++ b/framework/ca/src/org/apache/cloudstack/framework/ca/CAService.java @@ -0,0 +1,36 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.framework.ca; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +public interface CAService { + /** + * Returns a SSLEngine to be used for handling client connections + * @param context + * @param remoteAddress + * @return + * @throws GeneralSecurityException + * @throws IOException + */ + SSLEngine createSSLEngine(final SSLContext context, final String remoteAddress) throws GeneralSecurityException, IOException; +} diff --git a/framework/ca/src/org/apache/cloudstack/framework/ca/Certificate.java b/framework/ca/src/org/apache/cloudstack/framework/ca/Certificate.java new file mode 100644 index 00000000000..b3a230d5a99 --- /dev/null +++ b/framework/ca/src/org/apache/cloudstack/framework/ca/Certificate.java @@ -0,0 +1,46 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.framework.ca; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; + +public class Certificate { + private X509Certificate clientCertificate; + private PrivateKey privateKey; + private List caCertificates; + + public Certificate(final X509Certificate clientCertificate, final PrivateKey privateKey, final List caCertificates) { + this.clientCertificate = clientCertificate; + this.privateKey = privateKey; + this.caCertificates = caCertificates; + } + + public X509Certificate getClientCertificate() { + return clientCertificate; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + + public List getCaCertificates() { + return caCertificates; + } +} diff --git a/framework/pom.xml b/framework/pom.xml index 64d1774c671..840268596c0 100644 --- a/framework/pom.xml +++ b/framework/pom.xml @@ -44,6 +44,7 @@ ipc + ca rest events jobs diff --git a/packaging/centos63/cloud.spec b/packaging/centos63/cloud.spec index d72840b517a..9c7f3a41c9a 100644 --- a/packaging/centos63/cloud.spec +++ b/packaging/centos63/cloud.spec @@ -521,12 +521,6 @@ else echo "Unable to determine ssl settings for tomcat.conf, please run cloudstack-setup-management manually" fi -if [ -f "%{_sysconfdir}/cloud.rpmsave/management/cloud.keystore" ]; then - cp -p %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/%{name}/management/cloudmanagementserver.keystore - # make sure we only do this on the first install of this RPM, don't want to overwrite on a reinstall - mv %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore.rpmsave -fi - %preun agent /sbin/service cloudstack-agent stop || true if [ "$1" == "0" ] ; then diff --git a/plugins/ca/root-ca/pom.xml b/plugins/ca/root-ca/pom.xml new file mode 100644 index 00000000000..58e632260d5 --- /dev/null +++ b/plugins/ca/root-ca/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + cloud-plugin-ca-rootca + Apache CloudStack Plugin - Inbuilt Root Certificate Authority + + org.apache.cloudstack + cloudstack-plugins + 4.5.3-SNAPSHOT + ../../pom.xml + + + + org.apache.cloudstack + cloud-utils + ${project.version} + + + org.apache.cloudstack + cloud-api + ${project.version} + + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + + + diff --git a/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/module.properties b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/module.properties new file mode 100644 index 00000000000..e086bc03073 --- /dev/null +++ b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/module.properties @@ -0,0 +1,18 @@ +# 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. +name=root-ca +parent=ca diff --git a/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/spring-root-ca-context.xml b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/spring-root-ca-context.xml new file mode 100644 index 00000000000..46503febd7b --- /dev/null +++ b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/spring-root-ca-context.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCAProvider.java b/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCAProvider.java new file mode 100644 index 00000000000..b83ac433266 --- /dev/null +++ b/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCAProvider.java @@ -0,0 +1,572 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.ca.provider; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.StringReader; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyManagementException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.Security; +import java.security.SignatureException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.framework.ca.CAProvider; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.framework.config.ConfigKey; +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.log4j.Logger; +import org.bouncycastle.jce.PKCS10CertificationRequest; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; + +import com.cloud.certificate.dao.CrlDao; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.db.DbProperties; +import com.cloud.utils.db.GlobalLock; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; +import com.cloud.utils.nio.Link; +import com.google.common.base.Strings; + +public final class RootCAProvider extends AdapterBase implements CAProvider, Configurable { + private static final Logger LOG = Logger.getLogger(RootCAProvider.class); + + public static final Integer caValidityYears = 30; + public static final String caAlias = "root"; + public static final String managementAlias = "management"; + + private static KeyPair caKeyPair = null; + private static X509Certificate caCertificate = null; + + @Inject + private ConfigurationDao configDao; + @Inject + private CrlDao crlDao; + + //////////////////////////////////////////////////// + /////////////// Root CA Settings /////////////////// + //////////////////////////////////////////////////// + + private static ConfigKey rootCAPrivateKey = new ConfigKey<>("Hidden", String.class, + "ca.plugin.root.private.key", + null, + "The ROOT CA private key.", true); + + private static ConfigKey rootCAPublicKey = new ConfigKey<>("Hidden", String.class, + "ca.plugin.root.public.key", + null, + "The ROOT CA public key.", true); + + private static ConfigKey rootCACertificate = new ConfigKey<>("Hidden", String.class, + "ca.plugin.root.ca.certificate", + null, + "The ROOT CA certificate.", true); + + private static ConfigKey rootCAIssuerDN = new ConfigKey<>("Advanced", String.class, + "ca.plugin.root.issuer.dn", + "CN=ca.cloudstack.apache.org", + "The ROOT CA issuer distinguished name.", true); + + protected static ConfigKey rootCAAuthStrictness = new ConfigKey<>("Advanced", Boolean.class, + "ca.plugin.root.auth.strictness", + "false", + "Set client authentication strictness, setting to true will enforce and require client certificate for authentication in applicable CA providers.", true); + + private static ConfigKey rootCAAllowExpiredCert = new ConfigKey<>("Advanced", Boolean.class, + "ca.plugin.root.allow.expired.cert", + "true", + "When set to true, it will allow expired client certificate during SSL handshake.", true); + + + /////////////////////////////////////////////////////////// + /////////////// Root CA Private Methods /////////////////// + /////////////////////////////////////////////////////////// + + private Certificate generateCertificate(final List domainNames, final List ipAddresses, final int validityDays) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, CertificateException, SignatureException, IOException { + if (domainNames == null || domainNames.size() < 1 || Strings.isNullOrEmpty(domainNames.get(0))) { + throw new CloudRuntimeException("No domain name is specified, cannot generate certificate"); + } + final String subject = "CN=" + domainNames.get(0); + + final KeyPair keyPair = CertUtils.generateRandomKeyPair(CAManager.CertKeySize.value()); + final X509Certificate clientCertificate = CertUtils.generateV3Certificate( + caCertificate, + caKeyPair.getPrivate(), + keyPair.getPublic(), + subject, + CAManager.CertSignatureAlgorithm.value(), + validityDays, + domainNames, + ipAddresses); + return new Certificate(clientCertificate, keyPair.getPrivate(), Collections.singletonList(caCertificate)); + } + + private Certificate generateCertificateUsingCsr(final String csr, final List domainNames, final List ipAddresses, final int validityDays) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, CertificateException, SignatureException, IOException { + PemObject pemObject = null; + + try { + final PemReader pemReader = new PemReader(new StringReader(csr)); + pemObject = pemReader.readPemObject(); + } catch (IOException e) { + LOG.error("Failed to read provided CSR string as a PEM object", e); + } + + if (pemObject == null) { + throw new CloudRuntimeException("Unable to read/process CSR: " + csr); + } + + final PKCS10CertificationRequest request = new PKCS10CertificationRequest(pemObject.getContent()); + + final X509Certificate clientCertificate = CertUtils.generateV3Certificate( + caCertificate, caKeyPair.getPrivate(), + request.getPublicKey(), + request.getCertificationRequestInfo().getSubject().toString(), + CAManager.CertSignatureAlgorithm.value(), + validityDays, + domainNames, + ipAddresses); + return new Certificate(clientCertificate, null, Collections.singletonList(caCertificate)); + + } + + //////////////////////////////////////////////////////// + /////////////// Root CA API Handlers /////////////////// + //////////////////////////////////////////////////////// + + @Override + public boolean canProvisionCertificates() { + return true; + } + + @Override + public List getCaCertificate() { + return Collections.singletonList(caCertificate); + } + + @Override + public Certificate issueCertificate(final List domainNames, final List ipAddresses, final int validityDays) { + try { + return generateCertificate(domainNames, ipAddresses, validityDays); + } catch (final CertificateException | IOException | SignatureException | NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException e) { + LOG.error("Failed to create client certificate, due to: ", e); + throw new CloudRuntimeException("Failed to generate certificate due to:" + e.getMessage()); + } + } + + @Override + public Certificate issueCertificate(final String csr, final List domainNames, final List ipAddresses, final int validityDays) { + try { + return generateCertificateUsingCsr(csr, domainNames, ipAddresses, validityDays); + } catch (final CertificateException | IOException | SignatureException | NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException e) { + LOG.error("Failed to generate certificate from CSR: ", e); + throw new CloudRuntimeException("Failed to generate certificate using CSR due to:" + e.getMessage()); + } + } + + @Override + public boolean revokeCertificate(final BigInteger certSerial, final String certCn) { + return true; + } + + //////////////////////////////////////////////////////////// + /////////////// Root CA Trust Management /////////////////// + //////////////////////////////////////////////////////////// + + public static final class RootCACustomTrustManager implements X509TrustManager { + private String clientAddress = "Unknown"; + private boolean authStrictness = true; + private boolean allowExpiredCertificate = true; + private CrlDao crlDao; + private X509Certificate caCertificate; + private Map activeCertMap; + + public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, final Map activeCertMap, final X509Certificate caCertificate, final CrlDao crlDao) { + if (!Strings.isNullOrEmpty(clientAddress)) { + this.clientAddress = clientAddress.replace("/", "").split(":")[0]; + } + this.authStrictness = authStrictness; + this.allowExpiredCertificate = allowExpiredCertificate; + this.activeCertMap = activeCertMap; + this.caCertificate = caCertificate; + this.crlDao = crlDao; + } + + private void printCertificateChain(final X509Certificate[] certificates, final String s) throws CertificateException { + if (certificates == null) { + return; + } + final StringBuilder builder = new StringBuilder(); + builder.append("A client/agent attempting connection from address=").append(clientAddress).append(" has presented these certificate(s):"); + int counter = 1; + for (final X509Certificate certificate: certificates) { + builder.append("\nCertificate [").append(counter++).append("] :"); + builder.append(String.format("\n Serial: %x", certificate.getSerialNumber())); + builder.append("\n Not Before:" + certificate.getNotBefore()); + builder.append("\n Not After:" + certificate.getNotAfter()); + builder.append("\n Signature Algorithm:" + certificate.getSigAlgName()); + builder.append("\n Version:" + certificate.getVersion()); + builder.append("\n Subject DN:" + certificate.getSubjectDN()); + builder.append("\n Issuer DN:" + certificate.getIssuerDN()); + builder.append("\n Alternative Names:" + certificate.getSubjectAlternativeNames()); + } + LOG.debug(builder.toString()); + } + + @Override + public void checkClientTrusted(final X509Certificate[] certificates, final String s) throws CertificateException { + if (LOG.isDebugEnabled()) { + printCertificateChain(certificates, s); + } + if (!authStrictness) { + return; + } + if (certificates == null || certificates.length < 1 || certificates[0] == null) { + throw new CertificateException("In strict auth mode, certificate(s) are expected from client:" + clientAddress); + } + final X509Certificate primaryClientCertificate = certificates[0]; + + // Revocation check + final BigInteger serialNumber = primaryClientCertificate.getSerialNumber(); + if (serialNumber == null || crlDao.findBySerial(serialNumber) != null) { + final String errorMsg = String.format("Client is using revoked certificate of serial=%x, subject=%s from address=%s", + primaryClientCertificate.getSerialNumber(), primaryClientCertificate.getSubjectDN(), clientAddress); + LOG.error(errorMsg); + throw new CertificateException(errorMsg); + } + + // Validity check + if (!allowExpiredCertificate) { + try { + primaryClientCertificate.checkValidity(); + } catch (final CertificateExpiredException | CertificateNotYetValidException e) { + final String errorMsg = String.format("Client certificate has expired with serial=%x, subject=%s from address=%s", + primaryClientCertificate.getSerialNumber(), primaryClientCertificate.getSubjectDN(), clientAddress); + LOG.error(errorMsg); + throw new CertificateException(errorMsg); } + } + + // Ownership check + boolean certMatchesOwnership = false; + if (primaryClientCertificate.getSubjectAlternativeNames() != null) { + for (final List list : primaryClientCertificate.getSubjectAlternativeNames()) { + if (list != null && list.size() == 2 && list.get(1) instanceof String) { + final String alternativeName = (String) list.get(1); + if (clientAddress.equals(alternativeName)) { + certMatchesOwnership = true; + } + } + } + } + if (!certMatchesOwnership) { + final String errorMsg = "Certificate ownership verification failed for client: " + clientAddress; + LOG.error(errorMsg); + throw new CertificateException(errorMsg); + } + if (activeCertMap != null && !Strings.isNullOrEmpty(clientAddress)) { + activeCertMap.put(clientAddress, primaryClientCertificate); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Client/agent connection from ip=" + clientAddress + " has been validated and trusted."); + } + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + if (!authStrictness) { + return null; + } + return new X509Certificate[]{caCertificate}; + } + } + + private char[] getCaKeyStorePassphrase() { + return KeyStoreUtils.defaultKeystorePassphrase; + } + + private KeyStore getCaKeyStore() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException { + final KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + if (caKeyPair != null && caCertificate != null) { + ks.setKeyEntry(caAlias, caKeyPair.getPrivate(), getCaKeyStorePassphrase(), new X509Certificate[]{caCertificate}); + } else { + return null; + } + return ks; + } + + @Override + public SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress, final Map certMap) throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException { + final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + + final KeyStore ks = getCaKeyStore(); + kmf.init(ks, getCaKeyStorePassphrase()); + tmf.init(ks); + + final boolean authStrictness = rootCAAuthStrictness.value(); + final boolean allowExpiredCertificate = rootCAAllowExpiredCert.value(); + + TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certMap, caCertificate, crlDao)}; + sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom()); + final SSLEngine sslEngine = sslContext.createSSLEngine(); + sslEngine.setWantClientAuth(authStrictness); + return sslEngine; + } + + ////////////////////////////////////////////////// + /////////////// Root CA Config /////////////////// + ////////////////////////////////////////////////// + + private char[] findKeyStorePassphrase() { + char[] passphrase = KeyStoreUtils.defaultKeystorePassphrase; + final String configuredPassphrase = DbProperties.getDbProperties().getProperty("db.cloud.keyStorePassphrase"); + if (configuredPassphrase != null) { + passphrase = configuredPassphrase.toCharArray(); + } + return passphrase; + } + + private boolean createManagementServerKeystore(final String keyStoreFilePath, final char[] passphrase) { + final Certificate managementServerCertificate = issueCertificate(Collections.singletonList(NetUtils.getHostName()), + Collections.singletonList(NetUtils.getDefaultHostIp()), caValidityYears * 365); + if (managementServerCertificate == null || managementServerCertificate.getPrivateKey() == null) { + throw new CloudRuntimeException("Failed to generate certificate and setup management server keystore"); + } + LOG.info("Creating new management server certificate and keystore"); + try { + final KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + keyStore.setCertificateEntry(caAlias, caCertificate); + keyStore.setKeyEntry(managementAlias, managementServerCertificate.getPrivateKey(), passphrase, + new X509Certificate[]{managementServerCertificate.getClientCertificate(), caCertificate}); + final String tmpFile = KeyStoreUtils.defaultTmpKeyStoreFile; + final FileOutputStream stream = new FileOutputStream(tmpFile); + keyStore.store(stream, passphrase); + stream.close(); + KeyStoreUtils.copyKeystore(keyStoreFilePath, tmpFile); + LOG.debug("Saved default root CA (server) keystore file at:" + keyStoreFilePath); + } catch (final CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e) { + LOG.error("Failed to save root CA (server) keystore due to exception: ", e); + return false; + } + return true; + } + + private boolean checkManagementServerKeystore() { + final File confFile = PropertiesUtil.findConfigFile("db.properties"); + if (confFile == null) { + return false; + } + final char[] passphrase = findKeyStorePassphrase(); + final String keystorePath = confFile.getParent() + KeyStoreUtils.defaultKeystoreFile; + final File keystoreFile = new File(keystorePath); + if (keystoreFile.exists()) { + try { + final KeyStore msKeystore = Link.loadKeyStore(new FileInputStream(keystorePath), passphrase); + try { + final java.security.cert.Certificate[] msCertificates = msKeystore.getCertificateChain(managementAlias); + if (msCertificates != null && msCertificates.length > 1) { + msCertificates[0].verify(caKeyPair.getPublic()); + ((X509Certificate)msCertificates[0]).checkValidity(); + return true; + } + } catch (final CertificateException | NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException | SignatureException e) { + LOG.info("Renewing management server keystore, current certificate has expired"); + return createManagementServerKeystore(keystoreFile.getAbsolutePath(), passphrase); + } + } catch (final GeneralSecurityException | IOException e) { + LOG.error("Failed to read current management server keystore, renewing keystore!"); + } + } + return createManagementServerKeystore(keystoreFile.getAbsolutePath(), passphrase); + } + + ///////////////////////////////////////////////// + /////////////// Root CA Setup /////////////////// + ///////////////////////////////////////////////// + + private boolean saveNewRootCAKeypair() { + try { + LOG.debug("Generating root CA public/private keys"); + final KeyPair keyPair = CertUtils.generateRandomKeyPair(2 * CAManager.CertKeySize.value()); + if (!configDao.update(rootCAPublicKey.key(), rootCAPublicKey.category(), CertUtils.publicKeyToPem(keyPair.getPublic()))) { + LOG.error("Failed to save RootCA public key"); + } + if (!configDao.update(rootCAPrivateKey.key(), rootCAPrivateKey.category(), CertUtils.privateKeyToPem(keyPair.getPrivate()))) { + LOG.error("Failed to save RootCA private key"); + } + } catch (final NoSuchProviderException | NoSuchAlgorithmException | IOException e) { + LOG.error("Failed to generate/save RootCA private/public keys due to exception:", e); + } + return loadRootCAKeyPair(); + } + + private boolean saveNewRootCACertificate() { + if (caKeyPair == null) { + throw new CloudRuntimeException("Cannot issue self-signed root CA certificate as CA keypair is not initialized"); + } + try { + LOG.debug("Generating root CA certificate"); + final X509Certificate rootCaCertificate = CertUtils.generateV1Certificate( + caKeyPair, + rootCAIssuerDN.value(), + rootCAIssuerDN.value(), + caValidityYears, + CAManager.CertSignatureAlgorithm.value()); + if (!configDao.update(rootCACertificate.key(), rootCACertificate.category(), CertUtils.x509CertificateToPem(rootCaCertificate))) { + LOG.error("Failed to update RootCA public/x509 certificate"); + } + } catch (final NoSuchAlgorithmException | NoSuchProviderException | CertificateEncodingException | SignatureException | InvalidKeyException | IOException e) { + LOG.error("Failed to generate RootCA certificate from private/public keys due to exception:", e); + return false; + } + return loadRootCACertificate(); + } + + private boolean loadRootCAKeyPair() { + if (Strings.isNullOrEmpty(rootCAPublicKey.value()) || Strings.isNullOrEmpty(rootCAPrivateKey.value())) { + return false; + } + try { + caKeyPair = new KeyPair(CertUtils.pemToPublicKey(rootCAPublicKey.value()), CertUtils.pemToPrivateKey(rootCAPrivateKey.value())); + } catch (InvalidKeySpecException | IOException e) { + LOG.error("Failed to load saved RootCA private/public keys due to exception:", e); + return false; + } + return caKeyPair.getPrivate() != null && caKeyPair.getPublic() != null; + } + + private boolean loadRootCACertificate() { + if (Strings.isNullOrEmpty(rootCACertificate.value())) { + return false; + } + try { + caCertificate = CertUtils.pemToX509Certificate(rootCACertificate.value()); + caCertificate.verify(caKeyPair.getPublic()); + } catch (final IOException | CertificateException | NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) { + LOG.error("Failed to load saved RootCA certificate due to exception:", e); + return false; + } + return caCertificate != null; + } + + private boolean setupCA() { + Security.addProvider(new BouncyCastleProvider()); + if (!loadRootCAKeyPair() && !saveNewRootCAKeypair()) { + LOG.error("Failed to save and load root CA keypair"); + return false; + } + if (!loadRootCACertificate() && !saveNewRootCACertificate()) { + LOG.error("Failed to save and load root CA certificate"); + return false; + } + if (!checkManagementServerKeystore()) { + LOG.error("Failed to check and configure management server keystore"); + return false; + } + return true; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + final GlobalLock caLock = GlobalLock.getInternLock("Root.CAProvider.Configure.Lock"); + try { + if (caLock.lock(60)) { + try { + return setupCA(); + } finally { + caLock.unlock(); + } + } + } finally { + caLock.releaseRef(); + } + return false; + } + + /////////////////////////////////////////////////////// + /////////////// Root CA Descriptors /////////////////// + /////////////////////////////////////////////////////// + + @Override + public String getConfigComponentName() { + return RootCAProvider.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + rootCAPrivateKey, + rootCAPublicKey, + rootCACertificate, + rootCAIssuerDN, + rootCAAuthStrictness, + rootCAAllowExpiredCert + }; + } + + @Override + public String getProviderName() { + return "root"; + } + + @Override + public String getDescription() { + return "CloudStack's Root CA provider plugin"; + } +} diff --git a/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java new file mode 100644 index 00000000000..5e36bd4e3f9 --- /dev/null +++ b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java @@ -0,0 +1,111 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca.provider; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.utils.security.CertUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.certificate.CrlVO; +import com.cloud.certificate.dao.CrlDao; + +@RunWith(MockitoJUnitRunner.class) +public class RootCACustomTrustManagerTest { + + @Mock + private CrlDao crlDao; + private KeyPair caKeypair; + private KeyPair clientKeypair; + private X509Certificate caCertificate; + private X509Certificate expiredClientCertificate; + private String clientIp = "1.2.3.4"; + private Map certMap = new HashMap<>(); + + @Before + public void setUp() throws Exception { + certMap.clear(); + caKeypair = CertUtils.generateRandomKeyPair(1024); + clientKeypair = CertUtils.generateRandomKeyPair(1024); + caCertificate = CertUtils.generateV1Certificate(caKeypair, "CN=ca", "CN=ca", 1, + "SHA256withRSA"); + expiredClientCertificate = CertUtils.generateV3Certificate(caCertificate, caKeypair.getPrivate(), clientKeypair.getPublic(), + "CN=cloudstack.apache.org", "SHA256withRSA", 0, Collections.singletonList("cloudstack.apache.org"), Collections.singletonList(clientIp)); + } + + @Test + public void testAuthNotStrict() throws Exception { + final RootCAProvider.RootCACustomTrustManager trustManager = new RootCAProvider.RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao); + trustManager.checkClientTrusted(null, null); + Assert.assertNull(trustManager.getAcceptedIssuers()); + } + + @Test(expected = CertificateException.class) + public void testAuthStrictWithInvalidCert() throws Exception { + final RootCAProvider.RootCACustomTrustManager trustManager = new RootCAProvider.RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + trustManager.checkClientTrusted(null, null); + } + + @Test(expected = CertificateException.class) + public void testAuthStrictWithRevokedCert() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(new CrlVO()); + final RootCAProvider.RootCACustomTrustManager trustManager = new RootCAProvider.RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA"); + } + + @Test(expected = CertificateException.class) + public void testAuthStrictWithInvalidCertOwnership() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final RootCAProvider.RootCACustomTrustManager trustManager = new RootCAProvider.RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA"); + } + + @Test(expected = CertificateException.class) + public void testAuthStrictWithDenyExpiredCertAndOwnership() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final RootCAProvider.RootCACustomTrustManager trustManager = new RootCAProvider.RootCACustomTrustManager(clientIp, true, false, certMap, caCertificate, crlDao); + trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA"); + } + + @Test + public void testAuthStrictWithAllowExpiredCertAndOwnership() throws Exception { + Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); + final RootCAProvider.RootCACustomTrustManager trustManager = new RootCAProvider.RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + Assert.assertTrue(trustManager.getAcceptedIssuers() != null); + Assert.assertTrue(trustManager.getAcceptedIssuers().length == 1); + Assert.assertEquals(trustManager.getAcceptedIssuers()[0], caCertificate); + trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA"); + Assert.assertTrue(certMap.containsKey(clientIp)); + Assert.assertEquals(certMap.get(clientIp), expiredClientCertificate); + } + +} \ No newline at end of file diff --git a/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCAProviderTest.java b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCAProviderTest.java new file mode 100644 index 00000000000..bdd3f08ddfe --- /dev/null +++ b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCAProviderTest.java @@ -0,0 +1,155 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca.provider; + +import java.lang.reflect.Field; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import javax.net.ssl.SSLEngine; + +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.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.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class RootCAProviderTest { + + private KeyPair caKeyPair; + private X509Certificate caCertificate; + + private RootCAProvider provider; + + private void addField(final RootCAProvider provider, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = RootCAProvider.class.getDeclaredField(name); + f.setAccessible(true); + f.set(provider, o); + } + + private void overrideDefaultConfigValue(final ConfigKey configKey, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = ConfigKey.class.getDeclaredField(name); + f.setAccessible(true); + f.set(configKey, o); + } + + @Before + public void setUp() throws Exception { + caKeyPair = CertUtils.generateRandomKeyPair(1024); + caCertificate = CertUtils.generateV1Certificate(caKeyPair, "CN=ca", "CN=ca", 1, "SHA256withRSA"); + + provider = new RootCAProvider(); + + addField(provider, "caKeyPair", caKeyPair); + addField(provider, "caCertificate", caCertificate); + addField(provider, "caKeyPair", caKeyPair); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testCanProvisionCertificates() { + Assert.assertTrue(provider.canProvisionCertificates()); + } + + @Test + public void testGetCaCertificate() { + Assert.assertTrue(provider.getCaCertificate().size() == 1); + Assert.assertEquals(provider.getCaCertificate().get(0), caCertificate); + } + + @Test + public void testIssueCertificateWithoutCsr() throws NoSuchProviderException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { + final Certificate certificate = provider.issueCertificate(Arrays.asList("domain1.com", "domain2.com"), null, 1); + Assert.assertTrue(certificate != null); + Assert.assertTrue(certificate.getPrivateKey() != null); + Assert.assertEquals(certificate.getCaCertificates().get(0), caCertificate); + Assert.assertEquals(certificate.getClientCertificate().getIssuerDN(), caCertificate.getIssuerDN()); + Assert.assertTrue(certificate.getClientCertificate().getNotAfter().before(new DateTime().plusDays(1).toDate())); + certificate.getClientCertificate().verify(caCertificate.getPublicKey()); + } + + @Test + public void testIssueCertificateWithCsr() throws NoSuchProviderException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { + final String csr = "-----BEGIN NEW CERTIFICATE REQUEST-----\n" + + "MIICxTCCAa0CAQAwUDETMBEGA1UEBhMKY2xvdWRzdGFjazETMBEGA1UEChMKY2xvdWRzdGFjazET\n" + + "MBEGA1UECxMKY2xvdWRzdGFjazEPMA0GA1UEAxMGdi0xLVZNMIIBIjANBgkqhkiG9w0BAQEFAAOC\n" + + "AQ8AMIIBCgKCAQEAhi3hOrt/p0hUmoW2A+2gFAMxSINItRrHfQ6VUnHhYKZGcTN9honVFuu30tz7\n" + + "oSLUUx1laWEWLlIozpUcPSjOuPa5a0JS8kjplMd8DLfLNeQ6gcuEWznMRJqCaKM72qn/FAK3r11l\n" + + "2NofEfWbHU5QVQ5CsYF0JndspLcnmf0tnmreAzz6vlSEPQd4g2hTSsPb72eAqYd0eJnl2oXe7cF3\n" + + "iemg6/lWoxlh8njVFDKJ5ibNQA/RSc5syzzaQ8fn/AkZlChR5pml47elfC3GuqetfZPAEP4rebXV\n" + + "zEw+UVbMo5bWx4AYm1S2HxhmsWC/1J5oxluZDtC6tjMqnkKQze8HbQIDAQABoDAwLgYJKoZIhvcN\n" + + "AQkOMSEwHzAdBgNVHQ4EFgQUdgA1C/7vW3lUcb/dnolGjZB55/AwDQYJKoZIhvcNAQELBQADggEB\n" + + "AH6ynWbyW5o4h2yEvmcr+upmu/LZYkpfwIWIo+dfrHX9OHu0rhHDIgMgqEStWzrOfhAkcEocQo21\n" + + "E4Q39nECO+cgTCQ1nfH5BVqaMEg++n6tqXBwLmAQJkftEmB+YUPFB9OGn5TQY9Pcnof95Y8xnvtR\n" + + "0DvVQa9RM9IsqxgvU4wQCcaNHuEC46Wzo7lyYJ6p//GLw8UQnHxsWktt8U+vyaqXjOvz0+nJobUz\n" + + "Jv7r7DFkOwgS6ObBczaZsv1yx2YklcKfbsI7xVsvZAXFey2RsvSJi1QPEJC5XbwDenWnCSrPfjJg\n" + + "SLJ0p9tV70D6v07r1OOmBtvU5AH4N+vioAZA0BE=\n" + + "-----END NEW CERTIFICATE REQUEST-----\n"; + final Certificate certificate = provider.issueCertificate(csr, Arrays.asList("v-1-VM", "domain1.com", "domain2.com"), null, 1); + Assert.assertTrue(certificate != null); + Assert.assertTrue(certificate.getPrivateKey() == null); + Assert.assertEquals(certificate.getCaCertificates().get(0), caCertificate); + Assert.assertTrue(certificate.getClientCertificate().getSubjectDN().toString().startsWith("CN=v-1-VM,")); + certificate.getClientCertificate().verify(caCertificate.getPublicKey()); + } + + @Test + public void testRevokeCertificate() throws Exception { + Assert.assertTrue(provider.revokeCertificate(CertUtils.generateRandomBigInt(), "anyString")); + } + + @Test + public void testCreateSSLEngineWithoutAuthStrictness() throws Exception { + overrideDefaultConfigValue(RootCAProvider.rootCAAuthStrictness, "_defaultValue", "false"); + final SSLEngine e = provider.createSSLEngine(SSLUtils.getSSLContext(), "/1.2.3.4:5678", null); + Assert.assertFalse(e.getUseClientMode()); + Assert.assertFalse(e.getWantClientAuth()); + } + + @Test + public void testCreateSSLEngineWithAuthStrictness() throws Exception { + overrideDefaultConfigValue(RootCAProvider.rootCAAuthStrictness, "_defaultValue", "true"); + final SSLEngine e = provider.createSSLEngine(SSLUtils.getSSLContext(), "/1.2.3.4:5678", null); + Assert.assertFalse(e.getUseClientMode()); + Assert.assertTrue(e.getWantClientAuth()); + } + + @Test + public void testGetProviderName() throws Exception { + Assert.assertEquals(provider.getProviderName(), "root"); + } + +} \ No newline at end of file diff --git a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java index 0851023a7ef..3a315509b49 100644 --- a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java +++ b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java @@ -20,6 +20,9 @@ import java.util.Map; import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; + import com.cloud.agent.api.Answer; import com.cloud.agent.api.CheckHealthCommand; import com.cloud.agent.api.CheckNetworkCommand; @@ -52,6 +55,10 @@ public interface MockAgentManager extends Manager { Answer pingTest(PingTestCommand cmd); + Answer setupKeyStore(SetupKeyStoreCommand cmd); + + Answer setupCertificate(SetupCertificateCommand cmd); + MockHost getHost(String guid); Answer maintain(MaintainCommand cmd); diff --git a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java index 0eeef82c055..d071e2fd186 100644 --- a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java +++ b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java @@ -32,7 +32,10 @@ import javax.ejb.Local; import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.user.AccountManager; +import org.apache.cloudstack.ca.SetupCertificateAnswer; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; +import org.apache.cloudstack.ca.SetupKeystoreAnswer; import org.apache.cloudstack.context.CallContext; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -63,6 +66,7 @@ import com.cloud.simulator.MockHostVO; import com.cloud.simulator.MockVMVO; import com.cloud.simulator.dao.MockHostDao; import com.cloud.simulator.dao.MockVMDao; +import com.cloud.user.AccountManager; import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; @@ -461,6 +465,24 @@ public class MockAgentManagerImpl extends ManagerBase implements MockAgentManage return new Answer(cmd); } + @Override + public Answer setupKeyStore(SetupKeyStoreCommand cmd) { + return new SetupKeystoreAnswer( + "-----BEGIN CERTIFICATE REQUEST-----\n" + + "MIIBHjCByQIBADBkMQswCQYDVQQGEwJJTjELMAkGA1UECAwCSFIxETAPBgNVBAcM\n" + + "CEd1cnVncmFtMQ8wDQYDVQQKDAZBcGFjaGUxEzARBgNVBAsMCkNsb3VkU3RhY2sx\n" + + "DzANBgNVBAMMBnYtMS1WTTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD46KFWKYrJ\n" + + "F43Y1oqWUfrl4mj4Qm05Bgsi6nuigZv7ufiAKK0nO4iJKdRa2hFMUvBi2/bU3IyY\n" + + "Nvg7cdJsn4K9AgMBAAGgADANBgkqhkiG9w0BAQUFAANBAIta9glu/ZSjA/ncyXix\n" + + "yDOyAKmXXxsRIsdrEuIzakUuJS7C8IG0FjUbDyIaiwWQa5x+Lt4oMqCmpNqRzaGP\n" + + "fOo=\n" + "-----END CERTIFICATE REQUEST-----"); + } + + @Override + public Answer setupCertificate(SetupCertificateCommand cmd) { + return new SetupCertificateAnswer(true); + } + @Override public boolean start() { for (Discoverer discoverer : discoverers) { diff --git a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java index ab0ed475d9b..48fa7d6ebb4 100644 --- a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java +++ b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java @@ -27,16 +27,14 @@ import javax.ejb.Local; import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.agent.api.routing.SetMonitorServiceCommand; - -import com.cloud.api.commands.ConfigureSimulatorHAProviderState; -import com.cloud.api.commands.ListSimulatorHAStateTransitions; -import org.apache.log4j.Logger; -import org.springframework.stereotype.Component; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; import org.apache.cloudstack.storage.command.DeleteCommand; import org.apache.cloudstack.storage.command.DownloadCommand; import org.apache.cloudstack.storage.command.DownloadProgressCommand; import org.apache.cloudstack.storage.command.StorageSubSystemCommand; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; import com.cloud.agent.api.Answer; import com.cloud.agent.api.AttachIsoCommand; @@ -77,6 +75,7 @@ import com.cloud.agent.api.PvlanSetupCommand; import com.cloud.agent.api.RebootCommand; import com.cloud.agent.api.RevertToVMSnapshotCommand; import com.cloud.agent.api.ScaleVmCommand; +import com.cloud.agent.api.SecStorageFirewallCfgCommand; import com.cloud.agent.api.SecStorageSetupCommand; import com.cloud.agent.api.SecStorageVMSetupCommand; import com.cloud.agent.api.SecurityGroupRulesCmd; @@ -97,6 +96,7 @@ import com.cloud.agent.api.routing.LoadBalancerConfigCommand; import com.cloud.agent.api.routing.RemoteAccessVpnCfgCommand; import com.cloud.agent.api.routing.SavePasswordCommand; import com.cloud.agent.api.routing.SetFirewallRulesCommand; +import com.cloud.agent.api.routing.SetMonitorServiceCommand; import com.cloud.agent.api.routing.SetNetworkACLCommand; import com.cloud.agent.api.routing.SetPortForwardingRulesCommand; import com.cloud.agent.api.routing.SetPortForwardingRulesVpcCommand; @@ -114,8 +114,9 @@ import com.cloud.agent.api.storage.ListVolumeCommand; import com.cloud.agent.api.storage.PrimaryStorageDownloadCommand; import com.cloud.api.commands.CleanupSimulatorMockCmd; import com.cloud.api.commands.ConfigureSimulatorCmd; +import com.cloud.api.commands.ConfigureSimulatorHAProviderState; +import com.cloud.api.commands.ListSimulatorHAStateTransitions; import com.cloud.api.commands.QuerySimulatorMockCmd; -import com.cloud.agent.api.SecStorageFirewallCfgCommand; import com.cloud.resource.SimulatorStorageProcessor; import com.cloud.serializer.GsonHelper; import com.cloud.simulator.MockConfigurationVO; @@ -288,6 +289,10 @@ public class SimulatorManagerImpl extends ManagerBase implements SimulatorManage answer = _mockAgentMgr.checkHealth((CheckHealthCommand)cmd); } else if (cmd instanceof PingTestCommand) { answer = _mockAgentMgr.pingTest((PingTestCommand)cmd); + } else if (cmd instanceof SetupKeyStoreCommand) { + answer = _mockAgentMgr.setupKeyStore((SetupKeyStoreCommand)cmd); + } else if (cmd instanceof SetupCertificateCommand) { + answer = _mockAgentMgr.setupCertificate((SetupCertificateCommand)cmd); } else if (cmd instanceof PrepareForMigrationCommand) { answer = _mockVmMgr.prepareForMigrate((PrepareForMigrationCommand)cmd); } else if (cmd instanceof MigrateCommand) { diff --git a/plugins/pom.xml b/plugins/pom.xml index 4ef8ebb2f5e..15ea5ebb130 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -44,6 +44,7 @@ acl/dynamic-role-based affinity-group-processors/host-anti-affinity affinity-group-processors/explicit-dedication + ca/root-ca deployment-planners/user-concentrated-pod deployment-planners/user-dispersing deployment-planners/implicit-dedication diff --git a/scripts/common/keys/ssl-keys.py b/scripts/common/keys/ssl-keys.py deleted file mode 100644 index d6804cca19d..00000000000 --- a/scripts/common/keys/ssl-keys.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# 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. - - -# Copies keys that enable SSH communication with system vms -# $1 = new public key -# $2 = new private key -''' -All imports go here... -''' -from subprocess import call -import socket -import sys -import os -import subprocess -import traceback - -def generateSSLKey(outputPath): - logf = open("ssl-keys.log", "w") - hostName = socket.gethostbyname(socket.gethostname()) - keyFile = outputPath + os.sep + "cloudmanagementserver.keystore" - logf.write("HostName = %s\n" % hostName) - logf.write("OutputPath = %s\n" % keyFile) - dname='cn="Cloudstack User",ou="' + hostName + '",o="' + hostName + '",c="Unknown"'; - logf.write("dname = %s\n" % dname) - logf.flush() - try : - return_code = subprocess.Popen(["keytool", "-genkey", "-keystore", keyFile, "-storepass", "vmops.com", "-keypass", "vmops.com", "-keyalg", "RSA", "-validity", "3650", "-dname", dname],shell=True,stdout=logf, stderr=logf) - return_code.wait() - except OSError as e: - logf.flush() - traceback.print_exc(file=logf) - logf.flush() - logf.write("SSL key generated is : %s" % return_code) - logf.flush() - -argsSize=len(sys.argv) -if argsSize != 2: - print("Usage: ssl-keys.py ") - sys.exit(None) -sslKeyPath=sys.argv[1] - -generateSSLKey(sslKeyPath) \ No newline at end of file diff --git a/scripts/installer/windows/acs.wxs b/scripts/installer/windows/acs.wxs index bf09afcd6eb..3d7c6166714 100644 --- a/scripts/installer/windows/acs.wxs +++ b/scripts/installer/windows/acs.wxs @@ -255,9 +255,6 @@ - - \ No newline at end of file + diff --git a/scripts/installer/windows/client.wxs b/scripts/installer/windows/client.wxs index 3563f7800b1..c40fcb3d930 100644 --- a/scripts/installer/windows/client.wxs +++ b/scripts/installer/windows/client.wxs @@ -592,9 +592,6 @@ - - - diff --git a/scripts/network/domr/router_proxy.sh b/scripts/network/domr/router_proxy.sh index bcac4120ece..a24929f202b 100755 --- a/scripts/network/domr/router_proxy.sh +++ b/scripts/network/domr/router_proxy.sh @@ -37,22 +37,11 @@ check_gw() { cert="/root/.ssh/id_rsa.cloud" -script=$1 -shift - -domRIp=$1 -shift +script="$1" +domRIp="$2" check_gw "$domRIp" -ssh -p 3922 -q -o StrictHostKeyChecking=no -i $cert root@$domRIp "/opt/cloud/bin/$script $*" +ssh -p 3922 -q -o StrictHostKeyChecking=no -i $cert root@$domRIp "/opt/cloud/bin/$script ${@:3}" + exit $? - - - - - - - - - diff --git a/scripts/util/keystore-cert-import b/scripts/util/keystore-cert-import new file mode 100755 index 00000000000..bb03b6f68b4 --- /dev/null +++ b/scripts/util/keystore-cert-import @@ -0,0 +1,100 @@ +#!/bin/bash +# 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. + +PROPS_FILE="$1" +KS_FILE="$2" +MODE="$3" +CERT_FILE="$4" +CERT=$(echo "$5" | tr '^' '\n' | tr '~' ' ') +CACERT_FILE="$6" +CACERT=$(echo "$7" | tr '^' '\n' | tr '~' ' ') +PRIVKEY_FILE="$8" +PRIVKEY=$(echo "$9" | tr '^' '\n' | tr '~' ' ') + +ALIAS="cloud" +SYSTEM_FILE="/var/cache/cloud/cmdline" + +# Find keystore password +KS_PASS=$(sed -n '/keystore.passphrase/p' "$PROPS_FILE" 2>/dev/null | sed 's/keystore.passphrase=//g' 2>/dev/null) + +if [ -z "${KS_PASS// }" ]; then + echo "Failed to find keystore passphrase from file: $PROPS_FILE, quiting!" + exit 1 +fi + +# Use a new keystore file +NEW_KS_FILE="$KS_FILE.new" + +# Import certificate +if [ ! -z "${CERT// }" ]; then + echo "$CERT" > "$CERT_FILE" +fi + +# Import ca certs +if [ ! -z "${CACERT// }" ]; then + echo "$CACERT" > "$CACERT_FILE" +fi + +# Import cacerts into the keystore +awk '/-----BEGIN CERTIFICATE-----?/{n++}{print > "cloudca." n }' "$CACERT_FILE" +for caChain in $(ls cloudca.*); do + keytool -delete -noprompt -alias "$caChain" -keystore "$NEW_KS_FILE" -storepass "$KS_PASS" > /dev/null 2>&1 || true + keytool -import -noprompt -storepass "$KS_PASS" -trustcacerts -alias "$caChain" -file "$caChain" -keystore "$NEW_KS_FILE" > /dev/null 2>&1 +done +rm -f cloudca.* + +# Import private key if available +if [ ! -z "${PRIVKEY// }" ]; then + echo "$PRIVKEY" > "$PRIVKEY_FILE" + # Re-initialize keystore when private key is provided + keytool -delete -noprompt -alias "$ALIAS" -keystore "$NEW_KS_FILE" -storepass "$KS_PASS" 2>/dev/null || true + openssl pkcs12 -export -name "$ALIAS" -in "$CERT_FILE" -inkey "$PRIVKEY_FILE" -out "$NEW_KS_FILE.p12" -password pass:"$KS_PASS" > /dev/null 2>&1 + keytool -importkeystore -srckeystore "$NEW_KS_FILE.p12" -destkeystore "$NEW_KS_FILE" -srcstoretype PKCS12 -alias "$ALIAS" -deststorepass "$KS_PASS" -destkeypass "$KS_PASS" -srcstorepass "$KS_PASS" -srckeypass "$KS_PASS" > /dev/null 2>&1 +else + # Import certificate into the keystore + keytool -import -storepass "$KS_PASS" -alias "$ALIAS" -file "$CERT_FILE" -keystore "$NEW_KS_FILE" > /dev/null 2>&1 || true + # Export private key from keystore + rm -f "$PRIVKEY_FILE" + keytool -importkeystore -srckeystore "$NEW_KS_FILE" -destkeystore "$NEW_KS_FILE.p12" -deststoretype PKCS12 -srcalias "$ALIAS" -deststorepass "$KS_PASS" -destkeypass "$KS_PASS" -srcstorepass "$KS_PASS" -srckeypass "$KS_PASS" > /dev/null 2>&1 + openssl pkcs12 -in "$NEW_KS_FILE.p12" -nodes -nocerts -nomac -password pass:"$KS_PASS" 2>/dev/null | openssl rsa -out "$PRIVKEY_FILE" > /dev/null 2>&1 +fi + +# Commit the new keystore +rm -f "$NEW_KS_FILE.p12" +mv -f "$NEW_KS_FILE" "$KS_FILE" + +# Update ca-certs if we're in systemvm +if [ -f "$SYSTEM_FILE" ]; then + mkdir -p /usr/local/share/ca-certificates/cloudstack + cp "$CACERT_FILE" /usr/local/share/ca-certificates/cloudstack/ca.crt + chmod 755 /usr/local/share/ca-certificates/cloudstack + chmod 644 /usr/local/share/ca-certificates/cloudstack/ca.crt + update-ca-certificates > /dev/null 2>&1 || true +fi + +# Restart cloud service if we're in systemvm +if [ "$MODE" == "ssh" ] && [ -f $SYSTEM_FILE ]; then + /etc/init.d/cloud stop > /dev/null 2>&1 + sleep 2 + /etc/init.d/cloud start > /dev/null 2>&1 +fi + +# Fix file permission +chmod 600 $CACERT_FILE +chmod 600 $CERT_FILE +chmod 600 $PRIVKEY_FILE diff --git a/scripts/util/keystore-setup b/scripts/util/keystore-setup new file mode 100755 index 00000000000..28ce61c846a --- /dev/null +++ b/scripts/util/keystore-setup @@ -0,0 +1,51 @@ +#!/bin/bash +# 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. + +PROPS_FILE="$1" +KS_FILE="$2.new" +KS_PASS="$3" +KS_VALIDITY="$4" +CSR_FILE="$5" + +ALIAS="cloud" + +# Re-use existing password or use the one provided +if [ -f "$PROPS_FILE" ]; then + OLD_PASS=$(sed -n '/keystore.passphrase/p' "$PROPS_FILE" 2>/dev/null | sed 's/keystore.passphrase=//g' 2>/dev/null) + if [ ! -z "${OLD_PASS// }" ]; then + KS_PASS="$OLD_PASS" + else + sed -i "/keystore.passphrase.*/d" $PROPS_FILE 2> /dev/null || true + echo "keystore.passphrase=$KS_PASS" >> $PROPS_FILE + fi +fi + +# Generate keystore +rm -f "$KS_FILE" +CN=$(hostname --fqdn) +keytool -genkey -storepass "$KS_PASS" -keypass "$KS_PASS" -alias "$ALIAS" -keyalg RSA -validity "$KS_VALIDITY" -dname cn="$CN",ou="cloudstack",o="cloudstack",c="cloudstack" -keystore "$KS_FILE" + +# Generate CSR +rm -f "$CSR_FILE" +keytool -certreq -storepass "$KS_PASS" -alias "$ALIAS" -file $CSR_FILE -keystore "$KS_FILE" +cat "$CSR_FILE" + +# Fix file permissions +chmod 600 $KS_FILE +chmod 600 $PROPS_FILE +chmod 600 $CSR_FILE diff --git a/server/pom.xml b/server/pom.xml index d176e49d980..97dd076a7db 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -50,6 +50,11 @@ org.apache.httpcomponents httpcore + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + org.apache.cloudstack cloud-framework-jobs diff --git a/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 57ca17647a6..64c77ada395 100644 --- a/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -92,6 +92,11 @@ + + + + + diff --git a/server/src/com/cloud/alert/AlertManagerImpl.java b/server/src/com/cloud/alert/AlertManagerImpl.java index 4038be54fcf..3a61e28ba0c 100755 --- a/server/src/com/cloud/alert/AlertManagerImpl.java +++ b/server/src/com/cloud/alert/AlertManagerImpl.java @@ -761,7 +761,8 @@ public class AlertManagerImpl extends ManagerBase implements AlertManager, Confi (alertType != AlertManager.AlertType.ALERT_TYPE_MANAGMENT_NODE) && (alertType != AlertManager.AlertType.ALERT_TYPE_RESOURCE_LIMIT_EXCEEDED) && (alertType != AlertManager.AlertType.ALERT_TYPE_OOBM_AUTH_ERROR) && - (alertType != AlertManager.AlertType.ALERT_TYPE_HA_ACTION)) { + (alertType != AlertManager.AlertType.ALERT_TYPE_HA_ACTION) && + (alertType != AlertManager.AlertType.ALERT_TYPE_CA_CERT)) { alert = _alertDao.getLastAlert(alertType.getType(), dataCenterId, podId, clusterId); } diff --git a/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index df338c441de..3a53a5fd414 100755 --- a/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -114,6 +114,7 @@ import com.cloud.user.AccountManager; import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.DB; import com.cloud.utils.db.GlobalLock; @@ -1302,7 +1303,7 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy StringBuilder buf = profile.getBootArgsBuilder(); buf.append(" template=domP type=consoleproxy"); - buf.append(" host=").append(ApiServiceConfiguration.ManagementHostIPAdr.value()); + buf.append(" host=").append(StringUtils.shuffleCSVList(ApiServiceConfiguration.ManagementHostIPAdr.value())); buf.append(" port=").append(_mgmtPort); buf.append(" name=").append(profile.getVirtualMachine().getHostName()); if (_sslEnabled) { diff --git a/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java b/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java index 48be8f28516..79b4f38c752 100644 --- a/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java +++ b/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java @@ -16,6 +16,23 @@ // under the License. package com.cloud.hypervisor.kvm.discoverer; +import java.net.InetAddress; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.KeyStoreUtils; +import org.apache.log4j.Logger; + import com.cloud.agent.AgentManager; import com.cloud.agent.Listener; import com.cloud.agent.api.AgentControlAnswer; @@ -42,17 +59,10 @@ import com.cloud.resource.DiscovererBase; import com.cloud.resource.ResourceStateAdapter; import com.cloud.resource.ServerResource; import com.cloud.resource.UnableDeleteHostException; +import com.cloud.utils.PasswordGenerator; +import com.cloud.utils.StringUtils; import com.cloud.utils.ssh.SSHCmdHelper; -import org.apache.log4j.Logger; - -import javax.inject.Inject; -import javax.naming.ConfigurationException; -import java.net.InetAddress; -import java.net.URI; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import com.trilead.ssh2.Connection; public abstract class LibvirtServerDiscoverer extends DiscovererBase implements Discoverer, Listener, ResourceStateAdapter { private static final Logger s_logger = Logger.getLogger(LibvirtServerDiscoverer.class); @@ -62,7 +72,9 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements private String _kvmPublicNic; private String _kvmGuestNic; @Inject - AgentManager _agentMgr; + private AgentManager agentMgr; + @Inject + private CAManager caManager; @Override public abstract Hypervisor.HypervisorType getHypervisorType(); @@ -113,6 +125,73 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements return false; } + private void setupAgentSecurity(final Connection sshConnection, final String agentIp, final String agentHostname) { + if (!caManager.canProvisionCertificates()) { + s_logger.warn("Cannot secure agent communication because configure CA plugin cannot provision client certificate"); + return; + } + + if (sshConnection == null) { + s_logger.warn("Cannot secure agent communication because ssh connection is invalid for host ip=" + agentIp); + return; + } + + Integer validityPeriod = CAManager.CertValidityPeriod.value(); + if (validityPeriod < 1) { + validityPeriod = 1; + } + + final SSHCmdHelper.SSHCmdResult keystoreSetupResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection, + String.format("/usr/share/cloudstack-common/scripts/util/%s " + + "/etc/cloudstack/agent/agent.properties " + + "/etc/cloudstack/agent/%s " + + "%s %d " + + "/etc/cloudstack/agent/%s", + KeyStoreUtils.keyStoreSetupScript, + KeyStoreUtils.defaultKeystoreFile, + PasswordGenerator.generateRandomPassword(16), + validityPeriod, + KeyStoreUtils.defaultCsrFile)); + + if (!keystoreSetupResult.isSuccess()) { + s_logger.error("Failing, the keystore setup script failed execution on the KVM host: " + agentIp); + return; + } + + final Certificate certificate = caManager.issueCertificate(keystoreSetupResult.getStdOut(), Collections.singletonList(agentHostname), Collections.singletonList(agentIp), null, null); + if (certificate == null || certificate.getClientCertificate() == null) { + s_logger.error("Failing, the configured CA plugin failed to issue certificates for KVM host agent: " + agentIp); + return; + } + + final SetupCertificateCommand certificateCommand = new SetupCertificateCommand(certificate); + final SSHCmdHelper.SSHCmdResult setupCertResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection, + String.format("/usr/share/cloudstack-common/scripts/util/%s " + + "/etc/cloudstack/agent/agent.properties " + + "/etc/cloudstack/agent/%s %s " + + "/etc/cloudstack/agent/%s \"%s\" " + + "/etc/cloudstack/agent/%s \"%s\" " + + "/etc/cloudstack/agent/%s \"%s\"", + KeyStoreUtils.keyStoreImportScript, + KeyStoreUtils.defaultKeystoreFile, + KeyStoreUtils.sshMode, + KeyStoreUtils.defaultCertFile, + certificateCommand.getEncodedCertificate(), + KeyStoreUtils.defaultCaCertFile, + certificateCommand.getEncodedCaCertificates(), + KeyStoreUtils.defaultPrivateKeyFile, + certificateCommand.getEncodedPrivateKey())); + + if (setupCertResult != null && !setupCertResult.isSuccess()) { + s_logger.error("Failed to setup certificate in the KVM agent's keystore file, please configure manually!"); + return; + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Succeeded to import certificate in the keystore for agent on the KVM host: " + agentIp + ". Agent secured and trusted."); + } + } + @Override public Map> find(long dcId, Long podId, Long clusterId, URI uri, String username, String password, List hostTags) throws DiscoveryException { @@ -131,7 +210,7 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements s_logger.debug(msg); return null; } - com.trilead.ssh2.Connection sshConnection = null; + Connection sshConnection = null; String agentIp = null; try { @@ -150,7 +229,7 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements } } - sshConnection = new com.trilead.ssh2.Connection(agentIp, 22); + sshConnection = new Connection(agentIp, 22); sshConnection.connect(null, 60000, 60000); if (!sshConnection.authenticateWithPassword(username, password)) { @@ -158,7 +237,7 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements throw new DiscoveredWithErrorException("Authentication error"); } - if (!SSHCmdHelper.sshExecuteCmd(sshConnection, "lsmod|grep kvm", 3)) { + if (!SSHCmdHelper.sshExecuteCmd(sshConnection, "lsmod|grep kvm")) { s_logger.debug("It's not a KVM enabled machine"); return null; } @@ -198,7 +277,9 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements kvmGuestNic = (kvmPublicNic != null) ? kvmPublicNic : kvmPrivateNic; } - String parameters = " -m " + _hostIp + " -z " + dcId + " -p " + podId + " -c " + clusterId + " -g " + guid + " -a"; + setupAgentSecurity(sshConnection, agentIp, hostname); + + String parameters = " -m " + StringUtils.shuffleCSVList(_hostIp) + " -z " + dcId + " -p " + podId + " -c " + clusterId + " -g " + guid + " -a"; parameters += " --pubNic=" + kvmPublicNic; parameters += " --prvNic=" + kvmPrivateNic; @@ -209,8 +290,7 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements if (!username.equals("root")) { setupAgentCommand = "sudo cloudstack-setup-agent "; } - if (!SSHCmdHelper.sshExecuteCmd(sshConnection, - setupAgentCommand + parameters, 3)) { + if (!SSHCmdHelper.sshExecuteCmd(sshConnection, setupAgentCommand + parameters)) { s_logger.info("cloudstack agent setup command failed: " + setupAgentCommand + parameters); return null; @@ -380,7 +460,7 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements _resourceMgr.deleteRoutingHost(host, isForced, isForceDeleteStorage); try { ShutdownCommand cmd = new ShutdownCommand(ShutdownCommand.DeleteHost, null); - _agentMgr.send(host.getId(), cmd); + agentMgr.send(host.getId(), cmd); } catch (AgentUnavailableException e) { s_logger.warn("Sending ShutdownCommand failed: ", e); } catch (OperationTimedoutException e) { diff --git a/server/src/com/cloud/resource/ResourceManagerImpl.java b/server/src/com/cloud/resource/ResourceManagerImpl.java index 147a28a67c8..d966a8c30c5 100755 --- a/server/src/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/com/cloud/resource/ResourceManagerImpl.java @@ -2150,7 +2150,8 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, } try { - SSHCmdHelper.sshExecuteCmdOneShot(connection, "service cloudstack-agent restart"); + SSHCmdHelper.SSHCmdResult result = SSHCmdHelper.sshExecuteCmdOneShot(connection, "service cloudstack-agent restart"); + s_logger.debug(result.toString()); } catch (SshException e) { return false; } diff --git a/server/src/com/cloud/server/ConfigurationServerImpl.java b/server/src/com/cloud/server/ConfigurationServerImpl.java index ca313aa8af0..f32d75bb54d 100755 --- a/server/src/com/cloud/server/ConfigurationServerImpl.java +++ b/server/src/com/cloud/server/ConfigurationServerImpl.java @@ -23,8 +23,6 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.security.NoSuchAlgorithmException; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -36,24 +34,20 @@ import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.UUID; -import java.util.regex.Pattern; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.utils.nio.Link; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; -import org.apache.log4j.Logger; - import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.framework.config.ConfigDepot; import org.apache.cloudstack.framework.config.ConfigDepotAdmin; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.config.impl.ConfigurationVO; +import org.apache.commons.codec.binary.Base64; +import org.apache.log4j.Logger; import com.cloud.configuration.Config; import com.cloud.configuration.ConfigurationManager; @@ -155,7 +149,6 @@ public class ConfigurationServerImpl extends ManagerBase implements Configuratio @Inject protected ConfigurationManager _configMgr; - public ConfigurationServerImpl() { setRunLevel(ComponentLifecycle.RUN_LEVEL_FRAMEWORK_BOOTSTRAP); } @@ -300,9 +293,6 @@ public class ConfigurationServerImpl extends ManagerBase implements Configuratio // Update resource count if needed updateResourceCount(); - // keystore for SSL/TLS connection - updateSSLKeystore(); - // store the public and private keys in the database updateKeyPairs(); @@ -559,117 +549,6 @@ public class ConfigurationServerImpl extends ManagerBase implements Configuratio } } - static String getBase64Keystore(String keystorePath) throws IOException { - byte[] storeBytes = FileUtils.readFileToByteArray(new File(keystorePath)); - if (storeBytes.length > 3000) { // Base64 codec would enlarge data by 1/3, and we have 4094 bytes in database entry at most - throw new IOException("KeyStore is too big for database! Length " + storeBytes.length); - } - - return new String(Base64.encodeBase64(storeBytes)); - } - - private void generateDefaultKeystore(String keystorePath) throws IOException { - String cn = "Cloudstack User"; - String ou; - - try { - ou = InetAddress.getLocalHost().getCanonicalHostName(); - String[] group = ou.split("\\."); - - // Simple check to see if we got IP Address... - boolean isIPAddress = Pattern.matches("[0-9]$", group[group.length - 1]); - if (isIPAddress) { - ou = "cloud.com"; - } else { - ou = group[group.length - 1]; - for (int i = group.length - 2; i >= 0 && i >= group.length - 3; i--) - ou = group[i] + "." + ou; - } - } catch (UnknownHostException ex) { - s_logger.info("Fail to get user's domain name. Would use cloud.com. ", ex); - ou = "cloud.com"; - } - - String o = ou; - String c = "Unknown"; - String dname = "cn=\"" + cn + "\",ou=\"" + ou + "\",o=\"" + o + "\",c=\"" + c + "\""; - Script script = new Script(true, "keytool", 5000, null); - script.add("-genkey"); - script.add("-keystore", keystorePath); - script.add("-storepass", "vmops.com"); - script.add("-keypass", "vmops.com"); - script.add("-keyalg", "RSA"); - script.add("-validity", "3650"); - script.add("-dname", dname); - String result = script.execute(); - if (result != null) { - throw new IOException("Fail to generate certificate!: " + result); - } - } - - protected void updateSSLKeystore() { - if (s_logger.isInfoEnabled()) { - s_logger.info("Processing updateSSLKeyStore"); - } - - String dbString = _configDao.getValue("ssl.keystore"); - - File confFile = PropertiesUtil.findConfigFile("db.properties"); - String confPath = null; - String keystorePath = null; - File keystoreFile = null; - - if (null != confFile) { - confPath = confFile.getParent(); - keystorePath = confPath + Link.keystoreFile; - keystoreFile = new File(keystorePath); - } - - boolean dbExisted = (dbString != null && !dbString.isEmpty()); - - s_logger.info("SSL keystore located at " + keystorePath); - try { - if (!dbExisted && null != confFile) { - if (!keystoreFile.exists()) { - generateDefaultKeystore(keystorePath); - s_logger.info("Generated SSL keystore."); - } - String base64Keystore = getBase64Keystore(keystorePath); - ConfigurationVO configVO = - new ConfigurationVO("Hidden", "DEFAULT", "management-server", "ssl.keystore", base64Keystore, - "SSL Keystore for the management servers"); - _configDao.persist(configVO); - s_logger.info("Stored SSL keystore to database."); - } else { // !keystoreFile.exists() and dbExisted - // Export keystore to local file - byte[] storeBytes = Base64.decodeBase64(dbString); - try { - String tmpKeystorePath = "/tmp/tmpkey"; - FileOutputStream fo = new FileOutputStream(tmpKeystorePath); - fo.write(storeBytes); - fo.close(); - Script script = new Script(true, "cp", 5000, null); - script.add("-f"); - script.add(tmpKeystorePath); - - //There is a chance, although small, that the keystorePath is null. In that case, do not add it to the script. - if (null != keystorePath) { - script.add(keystorePath); - } - String result = script.execute(); - if (result != null) { - throw new IOException(); - } - } catch (Exception e) { - throw new IOException("Fail to create keystore file!", e); - } - s_logger.info("Stored database keystore to local."); - } - } catch (Exception ex) { - s_logger.warn("Would use fail-safe keystore to continue.", ex); - } - } - @DB protected void updateSystemvmPassword() { String userid = System.getProperty("user.name"); diff --git a/server/src/org/apache/cloudstack/ca/CAManagerImpl.java b/server/src/org/apache/cloudstack/ca/CAManagerImpl.java new file mode 100644 index 00000000000..b3fb259f76c --- /dev/null +++ b/server/src/org/apache/cloudstack/ca/CAManagerImpl.java @@ -0,0 +1,427 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.ca; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.ca.IssueCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.ListCAProvidersCmd; +import org.apache.cloudstack.api.command.admin.ca.ListCaCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.ProvisionCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.RevokeCertificateCmd; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.ca.CAProvider; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.poll.BackgroundPollManager; +import org.apache.cloudstack.poll.BackgroundPollTask; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.log4j.Logger; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import com.cloud.agent.AgentManager; +import com.cloud.alert.AlertManager; +import com.cloud.certificate.CrlVO; +import com.cloud.certificate.dao.CrlDao; +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.exception.CloudRuntimeException; +import com.google.common.base.Strings; + +public class CAManagerImpl extends ManagerBase implements CAManager { + public static final Logger LOG = Logger.getLogger(CAManagerImpl.class); + + @Inject + private CrlDao crlDao; + @Inject + private HostDao hostDao; + @Inject + private AgentManager agentManager; + @Inject + private BackgroundPollManager backgroundPollManager; + @Inject + private AlertManager alertManager; + + private static CAProvider configuredCaProvider; + private static Map caProviderMap = new HashMap<>(); + private static Map alertMap = new ConcurrentHashMap<>(); + private static Map activeCertMap = new ConcurrentHashMap<>(); + + private List caProviders; + + private CAProvider getConfiguredCaProvider() { + if (configuredCaProvider == null && caProviderMap.containsKey(CAProviderPlugin.value())) { + configuredCaProvider = caProviderMap.get(CAProviderPlugin.value()); + } + if (configuredCaProvider == null) { + throw new CloudRuntimeException("Failed to find default configured CA provider plugin"); + } + return configuredCaProvider; + } + + private CAProvider getCAProvider(final String provider) { + if (Strings.isNullOrEmpty(provider)) { + return getConfiguredCaProvider(); + } + final String caProviderName = provider.toLowerCase(); + if (!caProviderMap.containsKey(caProviderName)) { + throw new CloudRuntimeException(String.format("CA provider plugin '%s' not found", caProviderName)); + } + final CAProvider caProvider = caProviderMap.get(caProviderName); + if (caProvider == null) { + throw new CloudRuntimeException(String.format("CA provider plugin '%s' returned is null", caProviderName)); + } + return caProvider; + } + + /////////////////////////////////////////////////////////// + /////////////// CA Manager API Handlers /////////////////// + /////////////////////////////////////////////////////////// + + @Override + public List getCaProviders() { + return caProviders; + } + + @Override + public Map getActiveCertificatesMap() { + return activeCertMap; + } + + @Override + public boolean canProvisionCertificates() { + return getConfiguredCaProvider().canProvisionCertificates(); + } + + @Override + public String getCaCertificate(final String caProvider) throws IOException { + final CAProvider provider = getCAProvider(caProvider); + return CertUtils.x509CertificatesToPem(provider.getCaCertificate()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_ISSUE, eventDescription = "issuing certificate", async = true) + public Certificate issueCertificate(final String csr, final List domainNames, final List ipAddresses, final Integer validityDuration, final String caProvider) { + CallContext.current().setEventDetails("domain(s): " + domainNames + " addresses: " + ipAddresses); + final CAProvider provider = getCAProvider(caProvider); + Integer validity = CAManager.CertValidityPeriod.value(); + if (validityDuration != null) { + validity = validityDuration; + } + if (Strings.isNullOrEmpty(csr)) { + if (domainNames == null || domainNames.isEmpty()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "No domains or CSR provided"); + } + return provider.issueCertificate(domainNames, ipAddresses, validity); + } + return provider.issueCertificate(csr, domainNames, ipAddresses, validity); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_REVOKE, eventDescription = "revoking certificate", async = true) + public boolean revokeCertificate(final BigInteger certSerial, final String certCn, final String caProvider) { + CallContext.current().setEventDetails("cert serial: " + certSerial); + final CrlVO crl = crlDao.revokeCertificate(certSerial, certCn); + if (crl != null && crl.getCertSerial().equals(certSerial)) { + final CAProvider provider = getCAProvider(caProvider); + return provider.revokeCertificate(certSerial, certCn); + } + return false; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_PROVISION, eventDescription = "provisioning certificate for host", async = true) + public boolean provisionCertificate(final Host host, final Boolean reconnect, final String caProvider) { + if (host == null) { + throw new CloudRuntimeException("Unable to find valid host to renew certificate for"); + } + CallContext.current().setEventDetails("host id: " + host.getId()); + CallContext.current().putContextParameter(Host.class, host.getUuid()); + final String csr; + try { + csr = generateKeyStoreAndCsr(host, null); + if (Strings.isNullOrEmpty(csr)) { + return false; + } + final Certificate certificate = issueCertificate(csr, Collections.singletonList(host.getName()), Arrays.asList(host.getPrivateIpAddress(), host.getPublicIpAddress(), host.getStorageIpAddress()), CAManager.CertValidityPeriod.value(), caProvider); + return deployCertificate(host, certificate, reconnect, null); + } catch (final AgentUnavailableException | OperationTimedoutException e) { + LOG.error("Host/agent is not available or operation timed out, failed to setup keystore and generate CSR for host/agent id=" + host.getId() + ", due to: ", e); + throw new CloudRuntimeException("Failed to generate keystore and get CSR from the host/agent id=" + host.getId()); + } + } + + @Override + public String generateKeyStoreAndCsr(final Host host, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException { + final SetupKeyStoreCommand cmd = new SetupKeyStoreCommand(CertValidityPeriod.value()); + if (sshAccessDetails != null && !sshAccessDetails.isEmpty()) { + cmd.setAccessDetail(sshAccessDetails); + } + CallContext.current().setEventDetails("generating keystore and CSR for host id: " + host.getId()); + final SetupKeystoreAnswer answer = (SetupKeystoreAnswer) agentManager.send(host.getId(), cmd); + return answer.getCsr(); + } + + @Override + public boolean deployCertificate(final Host host, final Certificate certificate, final Boolean reconnect, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException { + final SetupCertificateCommand cmd = new SetupCertificateCommand(certificate); + if (sshAccessDetails != null && !sshAccessDetails.isEmpty()) { + cmd.setAccessDetail(sshAccessDetails); + } + CallContext.current().setEventDetails("deploying certificate for host id: " + host.getId()); + final SetupCertificateAnswer answer = (SetupCertificateAnswer) agentManager.send(host.getId(), cmd); + if (answer.getResult()) { + CallContext.current().setEventDetails("successfully deployed certificate for host id: " + host.getId()); + } else { + CallContext.current().setEventDetails("failed to deploy certificate for host id: " + host.getId()); + } + + if (answer.getResult()) { + getActiveCertificatesMap().put(host.getPrivateIpAddress(), certificate.getClientCertificate()); + if (sshAccessDetails == null && reconnect != null && reconnect) { + LOG.info(String.format("Successfully setup certificate on host, reconnecting with agent with id=%d, name=%s, address=%s", + host.getId(), host.getName(), host.getPublicIpAddress())); + return agentManager.reconnect(host.getId()); + } + return true; + } + return false; + } + + @Override + public void purgeHostCertificate(final Host host) { + if (host == null) { + return; + } + final String privateAddress = host.getPrivateIpAddress(); + final String publicAddress = host.getPublicIpAddress(); + final Map activeCertsMap = getActiveCertificatesMap(); + if (!Strings.isNullOrEmpty(privateAddress) && activeCertsMap.containsKey(privateAddress)) { + activeCertsMap.remove(privateAddress); + } + if (!Strings.isNullOrEmpty(publicAddress) && activeCertsMap.containsKey(publicAddress)) { + activeCertsMap.remove(publicAddress); + } + } + + @Override + public void sendAlert(final Host host, final String subject, final String message) { + if (host == null) { + return; + } + alertManager.sendAlert(AlertManager.AlertType.ALERT_TYPE_CA_CERT, + host.getDataCenterId(), host.getPodId(), subject, message); + } + + @Override + public SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress) throws GeneralSecurityException, IOException { + if (sslContext == null) { + throw new CloudRuntimeException("SSLContext provided to create SSLEngine is null, aborting"); + } + if (Strings.isNullOrEmpty(remoteAddress)) { + throw new CloudRuntimeException("Remote client address connecting to mgmt server cannot be empty/null"); + } + return getConfiguredCaProvider().createSSLEngine(sslContext, remoteAddress, getActiveCertificatesMap()); + } + + //////////////////////////////////////////////////// + /////////////// CA Manager Setup /////////////////// + //////////////////////////////////////////////////// + + public static final class CABackgroundTask extends ManagedContextRunnable implements BackgroundPollTask { + private CAManager caManager; + private HostDao hostDao; + + public CABackgroundTask(final CAManager caManager, final HostDao hostDao) { + this.caManager = caManager; + this.hostDao = hostDao; + } + + @Override + protected void runInContext() { + try { + if (LOG.isTraceEnabled()) { + LOG.trace("CA background task is running..."); + } + final DateTime now = DateTime.now(DateTimeZone.UTC); + final Map certsMap = caManager.getActiveCertificatesMap(); + for (final Iterator> it = certsMap.entrySet().iterator(); it.hasNext(); ) { + final Map.Entry entry = it.next(); + if (entry == null) { + continue; + } + final String hostIp = entry.getKey(); + final X509Certificate certificate = entry.getValue(); + if (certificate == null) { + it.remove(); + continue; + } + final Host host = hostDao.findByIp(hostIp); + if (host == null || host.getManagementServerId() == null || + host.getManagementServerId() != ManagementServerNode.getManagementServerId() || + host.getStatus() != Status.Up) { + if (host == null || + (host.getManagementServerId() != null && + host.getManagementServerId() != ManagementServerNode.getManagementServerId())) { + it.remove(); + } + continue; + } + + final String hostDescription = String.format("host id=%d, uuid=%s, name=%s, ip=%s, zone id=%d", + host.getId(), host.getUuid(), host.getName(), hostIp, host.getDataCenterId()); + + try { + certificate.checkValidity(now.plusDays(CertExpiryAlertPeriod.valueIn(host.getClusterId())).toDate()); + } catch (final CertificateExpiredException | CertificateNotYetValidException e) { + LOG.warn("Certificate is going to expire for " + hostDescription); + if (AutomaticCertRenewal.valueIn(host.getClusterId())) { + try { + LOG.debug("Attempting certificate auto-renewal for " + hostDescription); + boolean result = caManager.provisionCertificate(host, false, null); + if (result) { + LOG.debug("Succeeded in auto-renewing certificate for " + hostDescription); + } else { + LOG.debug("Failed in auto-renewing certificate for " + hostDescription); + } + } catch (final Throwable ex) { + LOG.warn("Failed to auto-renew certificate for " + hostDescription + ", with error=", ex); + caManager.sendAlert(host, "Certificate auto-renewal failed for " + hostDescription, + String.format("Certificate is going to expire for %s. Auto-renewal failed to renew the certificate, please renew it manually. It is not valid after %s.", hostDescription, certificate.getNotAfter())); + } + } else { + if (alertMap.containsKey(hostIp)) { + final Date lastSentDate = alertMap.get(hostIp); + if (now.minusDays(1).toDate().before(lastSentDate)) { + continue; + } + } + caManager.sendAlert(host, "Certificate expiring soon for " + hostDescription, + String.format("Certificate is going to expire for %s. Please renew it, it is not valid after %s.", + hostDescription, certificate.getNotAfter())); + alertMap.put(hostIp, new Date()); + } + } + } + } catch (final Throwable t) { + LOG.error("Error trying to run CA background task", t); + } + } + + @Override + public Long getDelay() { + return CABackgroundJobDelay.value() * 1000L; + } + } + + public void setCaProviders(final List caProviders) { + this.caProviders = caProviders; + initializeCaProviderMap(); + } + + private void initializeCaProviderMap() { + if (caProviderMap != null && caProviderMap.size() != caProviders.size()) { + for (final CAProvider caProvider : caProviders) { + caProviderMap.put(caProvider.getProviderName().toLowerCase(), caProvider); + } + } + } + + @Override + public boolean start() { + super.start(); + initializeCaProviderMap(); + if (caProviderMap.containsKey(CAProviderPlugin.value())) { + configuredCaProvider = caProviderMap.get(CAProviderPlugin.value()); + } + if (configuredCaProvider == null) { + LOG.error("Failed to find valid configured CA provider, please check!"); + return false; + } + return true; + } + + @Override + public boolean configure(final String name, final Map params) throws ConfigurationException { + backgroundPollManager.submitTask(new CABackgroundTask(this, hostDao)); + return true; + } + + ////////////////////////////////////////////////////////// + /////////////// CA Manager Descriptors /////////////////// + ////////////////////////////////////////////////////////// + + @Override + public List> getCommands() { + final List> cmdList = new ArrayList>(); + cmdList.add(ListCAProvidersCmd.class); + cmdList.add(ListCaCertificateCmd.class); + cmdList.add(IssueCertificateCmd.class); + cmdList.add(ProvisionCertificateCmd.class); + cmdList.add(RevokeCertificateCmd.class); + return cmdList; + } + + @Override + public String getConfigComponentName() { + return CAManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + CAProviderPlugin, + CertKeySize, + CertSignatureAlgorithm, + CertValidityPeriod, + AutomaticCertRenewal, + CABackgroundJobDelay, + CertExpiryAlertPeriod + }; + } +} diff --git a/server/src/org/apache/cloudstack/ha/HAManagerImpl.java b/server/src/org/apache/cloudstack/ha/HAManagerImpl.java index 0229f0a0266..fe4065401fe 100644 --- a/server/src/org/apache/cloudstack/ha/HAManagerImpl.java +++ b/server/src/org/apache/cloudstack/ha/HAManagerImpl.java @@ -612,6 +612,12 @@ public final class HAManagerImpl extends ManagerBase implements HAManager, Clust LOG.error("Error trying to perform health checks in HA manager", t); } } + + @Override + public Long getDelay() { + return null; + } + } private final class ActivityCheckPollTask extends ManagedContextRunnable implements BackgroundPollTask { @@ -648,6 +654,12 @@ public final class HAManagerImpl extends ManagerBase implements HAManager, Clust LOG.error("Error trying to perform activity checks in HA manager", t); } } + + @Override + public Long getDelay() { + return null; + } + } private final class RecoveryPollTask extends ManagedContextRunnable implements BackgroundPollTask { @@ -700,6 +712,12 @@ public final class HAManagerImpl extends ManagerBase implements HAManager, Clust LOG.error("Error trying to perform recovery operation in HA manager", t); } } + + @Override + public Long getDelay() { + return null; + } + } private final class FencingPollTask extends ManagedContextRunnable implements BackgroundPollTask { @@ -739,5 +757,11 @@ public final class HAManagerImpl extends ManagerBase implements HAManager, Clust LOG.error("Error trying to perform fencing operation in HA manager", t); } } + + @Override + public Long getDelay() { + return null; + } + } } diff --git a/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java b/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java index 3a860df3080..60113e95df9 100644 --- a/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java +++ b/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java @@ -571,5 +571,11 @@ public class OutOfBandManagementServiceImpl extends ManagerBase implements OutOf LOG.error("Error trying to retrieve host out-of-band management stats", t); } } + + @Override + public Long getDelay() { + return null; + } + } } diff --git a/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java b/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java index c0a7f1c3957..f4a634032d4 100644 --- a/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java +++ b/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java @@ -52,7 +52,11 @@ public final class BackgroundPollManagerImpl extends ManagerBase implements Back } backgroundPollTaskScheduler = Executors.newScheduledThreadPool(submittedTasks.size() + 1, new NamedThreadFactory("BackgroundTaskPollManager")); for (final BackgroundPollTask task : submittedTasks) { - backgroundPollTaskScheduler.scheduleWithFixedDelay(task, getInitialDelay(), getRoundDelay(), TimeUnit.MILLISECONDS); + Long delay = task.getDelay(); + if (delay == null) { + delay = getRoundDelay(); + } + backgroundPollTaskScheduler.scheduleWithFixedDelay(task, getInitialDelay(), delay, TimeUnit.MILLISECONDS); LOG.debug("Scheduled background poll task: " + task.getClass().getName()); } isConfiguredAndStarted = true; diff --git a/server/test/com/cloud/server/ConfigurationServerImplTest.java b/server/test/com/cloud/server/ConfigurationServerImplTest.java index 38dc1bc0e09..b68a771a274 100644 --- a/server/test/com/cloud/server/ConfigurationServerImplTest.java +++ b/server/test/com/cloud/server/ConfigurationServerImplTest.java @@ -16,11 +16,6 @@ // under the License. package com.cloud.server; -import java.io.File; -import java.io.IOException; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; import org.junit.Assert; import org.junit.Test; import org.mockito.Spy; @@ -41,41 +36,6 @@ public class ConfigurationServerImplTest { } }; - final static String TEST = "the quick brown fox jumped over the lazy dog"; - - @Test(expected = IOException.class) - public void testGetBase64KeystoreNoSuchFile() throws IOException { - ConfigurationServerImpl.getBase64Keystore("notexisting" + System.currentTimeMillis()); - } - - @Test(expected = IOException.class) - public void testGetBase64KeystoreTooBigFile() throws IOException { - File temp = File.createTempFile("keystore", ""); - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < 1000; i++) { - builder.append("way too long...\n"); - } - FileUtils.writeStringToFile(temp, builder.toString()); - try { - ConfigurationServerImpl.getBase64Keystore(temp.getPath()); - } finally { - temp.delete(); - } - } - - @Test - public void testGetBase64Keystore() throws IOException { - File temp = File.createTempFile("keystore", ""); - try { - FileUtils.writeStringToFile(temp, Base64.encodeBase64String(TEST.getBytes())); - final String keystore = ConfigurationServerImpl.getBase64Keystore(temp.getPath()); - // let's decode it to make sure it makes sense - Base64.decodeBase64(keystore); - } finally { - temp.delete(); - } - } - @Test public void testWindowsScript() { Assert.assertTrue(windowsImpl.isOnWindows()); diff --git a/server/test/com/cloud/vpc/MockNetworkManagerImpl.java b/server/test/com/cloud/vpc/MockNetworkManagerImpl.java index e4c97a0558e..545718e0dfb 100644 --- a/server/test/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/test/com/cloud/vpc/MockNetworkManagerImpl.java @@ -572,6 +572,11 @@ public class MockNetworkManagerImpl extends ManagerBase implements NetworkOrches return null; } + @Override + public Map getSystemVMAccessDetails(VirtualMachine vm) { + return null; + } + /* (non-Javadoc) * @see com.cloud.network.NetworkManager#implementNetwork(long, com.cloud.deploy.DeployDestination, com.cloud.vm.ReservationContext) */ diff --git a/server/test/org/apache/cloudstack/ca/CABackgroundTaskTest.java b/server/test/org/apache/cloudstack/ca/CABackgroundTaskTest.java new file mode 100644 index 00000000000..d2c800d8272 --- /dev/null +++ b/server/test/org/apache/cloudstack/ca/CABackgroundTaskTest.java @@ -0,0 +1,152 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import static org.apache.cloudstack.ca.CAManager.AutomaticCertRenewal; + +import java.lang.reflect.Field; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.cloudstack.utils.security.CertUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.storage.Storage; +import com.cloud.utils.exception.CloudRuntimeException; + +@RunWith(MockitoJUnitRunner.class) +public class CABackgroundTaskTest { + + @Mock + private CAManager caManager; + @Mock + private HostDao hostDao; + + private String hostIp = "1.2.3.4"; + private HostVO host = new HostVO(1L, "some.host",Host.Type.Routing, hostIp, "255.255.255.0", null, null, null, null, null, null, null, null, null, null, + UUID.randomUUID().toString(), Status.Up, "1.0", null, null, 1L, null, 0, 0, "aa", 0, Storage.StoragePoolType.NetworkFilesystem); + + private X509Certificate expiredCertificate; + private Map certMap = new HashMap<>(); + private CAManagerImpl.CABackgroundTask task; + + @Before + public void setUp() throws Exception { + host.setManagementServerId(ManagementServerNode.getManagementServerId()); + task = new CAManagerImpl.CABackgroundTask(caManager, hostDao); + final KeyPair keypair = CertUtils.generateRandomKeyPair(1024); + expiredCertificate = CertUtils.generateV1Certificate(keypair, "CN=ca", "CN=ca", 0, + "SHA256withRSA"); + + Mockito.when(hostDao.findByIp(Mockito.anyString())).thenReturn(host); + Mockito.when(caManager.getActiveCertificatesMap()).thenReturn(certMap); + } + + @After + public void tearDown() throws Exception { + certMap.clear(); + Mockito.reset(caManager); + Mockito.reset(hostDao); + } + + private void overrideDefaultConfigValue(final ConfigKey configKey, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = ConfigKey.class.getDeclaredField(name); + f.setAccessible(true); + f.set(configKey, o); + } + + @Test + public void testNullCert() throws Exception { + certMap.put(hostIp, null); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Assert.assertTrue(certMap.size() == 0); + } + + @Test + public void testNullHost() throws Exception { + Mockito.when(hostDao.findByIp(Mockito.anyString())).thenReturn(null); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Assert.assertTrue(certMap.size() == 0); + } + + @Test + public void testAutoRenewalEnabledWithNoExceptionsOnProvisioning() throws Exception { + overrideDefaultConfigValue(AutomaticCertRenewal, "_defaultValue", "true"); + host.setManagementServerId(ManagementServerNode.getManagementServerId()); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, false, null); + Mockito.verify(caManager, Mockito.times(0)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void testAutoRenewalEnabledWithExceptionsOnProvisioning() throws Exception { + overrideDefaultConfigValue(AutomaticCertRenewal, "_defaultValue", "true"); + Mockito.when(caManager.provisionCertificate(Mockito.any(Host.class), Mockito.anyBoolean(), Mockito.anyString())).thenThrow(new CloudRuntimeException("some error")); + host.setManagementServerId(ManagementServerNode.getManagementServerId()); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, false, null); + Mockito.verify(caManager, Mockito.times(1)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void testAutoRenewalDisabled() throws Exception { + overrideDefaultConfigValue(AutomaticCertRenewal, "_defaultValue", "false"); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + // First round + task.runInContext(); + Mockito.verify(caManager, Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), Mockito.anyBoolean(), Mockito.anyString()); + Mockito.verify(caManager, Mockito.times(1)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString()); + Mockito.reset(caManager); + // Second round + task.runInContext(); + Mockito.verify(caManager, Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), Mockito.anyBoolean(), Mockito.anyString()); + Mockito.verify(caManager, Mockito.times(0)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void testGetDelay() throws Exception { + Assert.assertTrue(task.getDelay() == CAManager.CABackgroundJobDelay.value() * 1000L); + } + +} \ No newline at end of file diff --git a/server/test/org/apache/cloudstack/ca/CAManagerImplTest.java b/server/test/org/apache/cloudstack/ca/CAManagerImplTest.java new file mode 100644 index 00000000000..87e128c772e --- /dev/null +++ b/server/test/org/apache/cloudstack/ca/CAManagerImplTest.java @@ -0,0 +1,119 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import java.lang.reflect.Field; +import java.math.BigInteger; +import java.security.cert.X509Certificate; +import java.util.Collections; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.framework.ca.CAProvider; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.CertUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.certificate.CrlVO; +import com.cloud.certificate.dao.CrlDao; +import com.cloud.host.Host; +import com.cloud.host.dao.HostDao; + +@RunWith(MockitoJUnitRunner.class) +public class CAManagerImplTest { + + @Mock + private HostDao hostDao; + @Mock + private CrlDao crlDao; + @Mock + private AgentManager agentManager; + @Mock + private CAProvider caProvider; + + private CAManagerImpl caManager; + + private void addField(final CAManagerImpl provider, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = CAManagerImpl.class.getDeclaredField(name); + f.setAccessible(true); + f.set(provider, o); + } + + @Before + public void setUp() throws Exception { + caManager = new CAManagerImpl(); + addField(caManager, "crlDao", crlDao); + addField(caManager, "hostDao", hostDao); + addField(caManager, "agentManager", agentManager); + addField(caManager, "configuredCaProvider", caProvider); + + Mockito.when(caProvider.getProviderName()).thenReturn("root"); + caManager.setCaProviders(Collections.singletonList(caProvider)); + } + + @After + public void tearDown() throws Exception { + Mockito.reset(crlDao); + Mockito.reset(agentManager); + Mockito.reset(caProvider); + } + + @Test(expected = ServerApiException.class) + public void testIssueCertificateThrowsException() throws Exception { + caManager.issueCertificate(null, null, null, 1, null); + } + + @Test + public void testIssueCertificate() throws Exception { + caManager.issueCertificate(null, Collections.singletonList("domain.example"), null, 1, null); + Mockito.verify(caProvider, Mockito.times(1)).issueCertificate(Mockito.anyList(), Mockito.anyList(), Mockito.anyInt()); + Mockito.verify(caProvider, Mockito.times(0)).issueCertificate(Mockito.anyString(), Mockito.anyList(), Mockito.anyList(), Mockito.anyInt()); + } + + @Test + public void testRevokeCertificate() throws Exception { + final CrlVO crl = new CrlVO(CertUtils.generateRandomBigInt(), "some.domain", "some-uuid"); + Mockito.when(crlDao.revokeCertificate(Mockito.any(BigInteger.class), Mockito.anyString())).thenReturn(crl); + Mockito.when(caProvider.revokeCertificate(Mockito.any(BigInteger.class), Mockito.anyString())).thenReturn(true); + Assert.assertTrue(caManager.revokeCertificate(crl.getCertSerial(), crl.getCertCn(), null)); + Mockito.verify(caProvider, Mockito.times(1)).revokeCertificate(Mockito.any(BigInteger.class), Mockito.anyString()); + } + + @Test + public void testProvisionCertificate() throws Exception { + final Host host = Mockito.mock(Host.class); + Mockito.when(host.getPrivateIpAddress()).thenReturn("1.2.3.4"); + final X509Certificate certificate = CertUtils.generateV1Certificate(CertUtils.generateRandomKeyPair(1024), "CN=ca", "CN=ca", 1, "SHA256withRSA"); + Mockito.when(caProvider.issueCertificate(Mockito.anyString(), Mockito.anyList(), Mockito.anyList(), Mockito.anyInt())).thenReturn(new Certificate(certificate, null, Collections.singletonList(certificate))); + Mockito.when(agentManager.send(Mockito.anyLong(), Mockito.any(SetupKeyStoreCommand.class))).thenReturn(new SetupKeystoreAnswer("someCsr")); + Mockito.when(agentManager.reconnect(Mockito.anyLong())).thenReturn(true); + Assert.assertTrue(caManager.provisionCertificate(host, true, null)); + Mockito.verify(agentManager, Mockito.times(2)).send(Mockito.anyLong(), Mockito.any(Answer.class)); + Mockito.verify(agentManager, Mockito.times(1)).reconnect(Mockito.anyLong()); + } +} \ No newline at end of file diff --git a/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java b/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java index 3304abaf611..f35d49cefed 100644 --- a/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java +++ b/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java @@ -45,6 +45,12 @@ public class BackgroundPollManagerImplTest { didIRun = true; counter++; } + + @Override + public Long getDelay() { + return null; + } + } @Before diff --git a/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index 382a0f8757f..56d2c6aed5b 100755 --- a/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -121,6 +121,7 @@ import com.cloud.user.AccountService; import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.GlobalLock; import com.cloud.utils.db.QueryBuilder; @@ -1058,7 +1059,7 @@ public class SecondaryStorageManagerImpl extends ManagerBase implements Secondar StringBuilder buf = profile.getBootArgsBuilder(); buf.append(" template=domP type=secstorage"); - buf.append(" host=").append(ApiServiceConfiguration.ManagementHostIPAdr.value()); + buf.append(" host=").append(StringUtils.shuffleCSVList(ApiServiceConfiguration.ManagementHostIPAdr.value())); buf.append(" port=").append(_mgmtPort); buf.append(" name=").append(profile.getVirtualMachine().getHostName()); diff --git a/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java b/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java index 230ef84db37..66f51f834ca 100755 --- a/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java +++ b/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java @@ -46,19 +46,6 @@ import java.util.UUID; import javax.naming.ConfigurationException; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.log4j.Logger; - -import com.amazonaws.services.s3.model.S3ObjectSummary; - import org.apache.cloudstack.framework.security.keystore.KeystoreManager; import org.apache.cloudstack.storage.command.CopyCmdAnswer; import org.apache.cloudstack.storage.command.CopyCommand; @@ -73,7 +60,18 @@ import org.apache.cloudstack.storage.template.UploadManagerImpl; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.log4j.Logger; +import com.amazonaws.services.s3.model.S3ObjectSummary; import com.cloud.agent.api.Answer; import com.cloud.agent.api.CheckHealthAnswer; import com.cloud.agent.api.CheckHealthCommand; @@ -1970,9 +1968,10 @@ public class NfsSecondaryStorageResource extends ServerResourceBase implements S if (_inSystemVM) { _localgw = (String)params.get("localgw"); if (_localgw != null) { // can only happen inside service vm - String mgmtHost = (String)params.get("host"); - addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); - + String mgmtHosts = (String)params.get("host"); + for (final String mgmtHost : mgmtHosts.split(",")) { + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); + } String internalDns1 = (String)params.get("internaldns1"); if (internalDns1 == null) { s_logger.warn("No DNS entry found during configuration of NfsSecondaryStorage"); diff --git a/setup/db/db/schema-452to453.sql b/setup/db/db/schema-452to453.sql index a3000930c12..9b580e62860 100644 --- a/setup/db/db/schema-452to453.sql +++ b/setup/db/db/schema-452to453.sql @@ -512,4 +512,19 @@ CREATE VIEW `cloud`.`host_view` AS `cloud`.`last_annotation_view` ON `last_annotation_view`.`entity_uuid` = `host`.`uuid` LEFT JOIN `cloud`.`user` ON `user`.`uuid` = `last_annotation_view`.`user_uuid`; --- End Of Annotations specific changes \ No newline at end of file +-- End Of Annotations specific changes + +-- CA framework changes +DELETE from `cloud`.`configuration` where name='ssl.keystore'; + +-- Certificate Revocation List +CREATE TABLE IF NOT EXISTS `cloud`.`crl` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `serial` varchar(255) UNIQUE NOT NULL COMMENT 'certificate\'s serial number as hex string', + `cn` varchar(255) COMMENT 'certificate\'s common name', + `revoker_uuid` varchar(40) COMMENT 'revoker user account uuid', + `revoked` datetime COMMENT 'date of revocation', + PRIMARY KEY (`id`), + KEY (`serial`), + UNIQUE KEY (`serial`, `cn`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/setup/db/server-setup.sql b/setup/db/server-setup.sql index df2c9248552..1c4635c72ca 100644 --- a/setup/db/server-setup.sql +++ b/setup/db/server-setup.sql @@ -27,3 +27,6 @@ INSERT INTO `cloud`.`configuration` (category, instance, component, name, value, -- Enable dynamic RBAC by default for fresh deployments INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'RoleService', 'dynamic.apichecker.enabled', 'true'); + +-- Enable RootCA auth strictness for fresh deployments +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'RootCAProvider', 'ca.plugin.root.auth.strictness', 'true'); diff --git a/setup/db/server-setup.xml b/setup/db/server-setup.xml index 178f29aea35..955a3a51806 100755 --- a/setup/db/server-setup.xml +++ b/setup/db/server-setup.xml @@ -246,6 +246,13 @@ under the License. true + + ca.plugin.root.auth.strictness + true + + diff --git a/ui/scripts/ui-custom/ca.js b/ui/scripts/ui-custom/ca.js new file mode 100644 index 00000000000..c5298292371 --- /dev/null +++ b/ui/scripts/ui-custom/ca.js @@ -0,0 +1,53 @@ +// 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. + +(function($, cloudStack) { + $(window).bind('cloudStack.ready', function() { + var caCert = ""; + var downloadCaCert = function() { + var blob = new Blob([caCert], {type: 'application/x-x509-ca-cert'}); + var filename = "cloud-ca.pem"; + if(window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob, filename); + } else{ + var elem = window.document.createElement('a'); + elem.href = window.URL.createObjectURL(blob); + elem.download = filename; + document.body.appendChild(elem) + elem.click(); + document.body.removeChild(elem); + } + }; + + $.ajax({ + url: createURL('listCaCertificate'), + success: function(json) { + caCert = json.listcacertificateresponse.cacertificates.certificate; + if (caCert) { + var $caCertDownloadButton = $('
').addClass('cacert-download'); + $caCertDownloadButton.append($('').addClass('icon').html(' ').attr('title', 'Download CA Certificate')); + $caCertDownloadButton.click(function() { + downloadCaCert(); + }); + $('#header .controls .view-switcher:last').after($caCertDownloadButton); + } + }, + error: function(data) { + } + }); + }); +}(jQuery, cloudStack)); diff --git a/utils/pom.xml b/utils/pom.xml index 1f363aca62c..96b6de28ed9 100755 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -34,6 +34,11 @@ cloud-framework-managed-context ${project.version} + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + org.springframework spring-context @@ -178,7 +183,6 @@ com/cloud/utils/testcase/*TestCase* com/cloud/utils/db/*Test* - com/cloud/utils/testcase/NioTest.java diff --git a/utils/src/com/cloud/utils/AutoCloseableUtil.java b/utils/src/com/cloud/utils/AutoCloseableUtil.java new file mode 100644 index 00000000000..0c5d43e7df8 --- /dev/null +++ b/utils/src/com/cloud/utils/AutoCloseableUtil.java @@ -0,0 +1,39 @@ +// +// 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; + +import org.apache.log4j.Logger; + +public class AutoCloseableUtil { + private final static Logger s_logger = Logger.getLogger(AutoCloseableUtil.class); + + public static void closeAutoCloseable(AutoCloseable ac, String message) { + try { + + if (ac != null) { + ac.close(); + } + + } catch (Exception e) { + s_logger.warn("[ignored] " + message, e); + } + } + +} diff --git a/utils/src/com/cloud/utils/PropertiesUtil.java b/utils/src/com/cloud/utils/PropertiesUtil.java index 5340cd5b174..124adc2d965 100755 --- a/utils/src/com/cloud/utils/PropertiesUtil.java +++ b/utils/src/com/cloud/utils/PropertiesUtil.java @@ -180,4 +180,17 @@ public class PropertiesUtil { IOUtils.closeQuietly(stream); } } + + public static Properties getProperties(final File file) { + final Properties properties = new Properties(); + if (file == null || !file.exists()) { + return properties; + } + try { + PropertiesUtil.loadFromFile(properties, file); + } catch (final IOException ex) { + s_logger.error("Unable to load properties from file: " + file.getAbsolutePath() + ", due to :", ex); + } + return properties; + } } diff --git a/utils/src/com/cloud/utils/SerialVersionUID.java b/utils/src/com/cloud/utils/SerialVersionUID.java index e4ea2174fbd..0683c8c1519 100755 --- a/utils/src/com/cloud/utils/SerialVersionUID.java +++ b/utils/src/com/cloud/utils/SerialVersionUID.java @@ -66,4 +66,6 @@ public interface SerialVersionUID { public static final long UnableDeleteHostException = Base | 0x29; public static final long AffinityConflictException = Base | 0x2a; public static final long JobCancellationException = Base | 0x2b; + public static final long NioConnectionException = Base | 0x2c; + public static final long TaskExecutionException = Base | 0x2d; } diff --git a/utils/src/com/cloud/utils/StringUtils.java b/utils/src/com/cloud/utils/StringUtils.java index 74dbd4dcc4c..9ee73233d34 100644 --- a/utils/src/com/cloud/utils/StringUtils.java +++ b/utils/src/com/cloud/utils/StringUtils.java @@ -19,11 +19,14 @@ package com.cloud.utils; +import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -32,6 +35,29 @@ import org.owasp.esapi.StringUtilities; public class StringUtils { private static final char[] hexChar = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + private static Charset preferredACSCharset; + private static final String UTF8 = "UTF-8"; + + static { + if (isUtf8Supported()) { + preferredACSCharset = Charset.forName(UTF8); + } else { + preferredACSCharset = Charset.defaultCharset(); + } + } + + public static Charset getPreferredCharset() { + return preferredACSCharset; + } + + public static boolean isUtf8Supported() { + return Charset.isSupported(UTF8); + } + + protected static Charset getDefaultCharset() { + return Charset.defaultCharset(); + } + public static String join(Iterable iterable, String delim) { StringBuilder sb = new StringBuilder(); if (iterable != null) { @@ -296,4 +322,10 @@ public class StringUtils { } return listOfChunks; } + + public static String shuffleCSVList(final String csvList) { + List list = csvTagsToList(csvList); + Collections.shuffle(list, new Random(System.nanoTime())); + return join(list, ","); + } } diff --git a/utils/src/com/cloud/utils/exception/NioConnectionException.java b/utils/src/com/cloud/utils/exception/NioConnectionException.java new file mode 100644 index 00000000000..e89b3d8b065 --- /dev/null +++ b/utils/src/com/cloud/utils/exception/NioConnectionException.java @@ -0,0 +1,48 @@ +// +// 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.exception; + +import com.cloud.utils.SerialVersionUID; + +/** + * Used by the NioConnection class to wrap-up its exceptions. + */ +public class NioConnectionException extends Exception { + private static final long serialVersionUID = SerialVersionUID.NioConnectionException; + + protected int csErrorCode; + + public NioConnectionException(final String msg, final Throwable cause) { + super(msg, cause); + setCSErrorCode(CSExceptionErrorCode.getCSErrCode(this.getClass().getName())); + } + + public NioConnectionException(final String msg) { + super(msg); + } + + public void setCSErrorCode(final int cserrcode) { + csErrorCode = cserrcode; + } + + public int getCSErrorCode() { + return csErrorCode; + } +} diff --git a/utils/src/com/cloud/utils/exception/TaskExecutionException.java b/utils/src/com/cloud/utils/exception/TaskExecutionException.java new file mode 100644 index 00000000000..635874e9c81 --- /dev/null +++ b/utils/src/com/cloud/utils/exception/TaskExecutionException.java @@ -0,0 +1,48 @@ +// +// 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.exception; + +import com.cloud.utils.SerialVersionUID; + +/** + * Used by the Task class to wrap-up its exceptions. + */ +public class TaskExecutionException extends Exception { + private static final long serialVersionUID = SerialVersionUID.TaskExecutionException; + + protected int csErrorCode; + + public TaskExecutionException(final String msg, final Throwable cause) { + super(msg, cause); + setCSErrorCode(CSExceptionErrorCode.getCSErrCode(this.getClass().getName())); + } + + public TaskExecutionException(final String msg) { + super(msg); + } + + public void setCSErrorCode(final int cserrcode) { + csErrorCode = cserrcode; + } + + public int getCSErrorCode() { + return csErrorCode; + } +} diff --git a/utils/src/com/cloud/utils/nio/Link.java b/utils/src/com/cloud/utils/nio/Link.java index 30c3888012c..023d33ed590 100755 --- a/utils/src/com/cloud/utils/nio/Link.java +++ b/utils/src/com/cloud/utils/nio/Link.java @@ -30,6 +30,7 @@ import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.SecureRandom; import java.util.concurrent.ConcurrentLinkedQueue; import javax.net.ssl.KeyManagerFactory; @@ -42,6 +43,8 @@ import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; +import org.apache.cloudstack.framework.ca.CAService; +import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.cloudstack.utils.security.SSLUtils; import org.apache.log4j.Logger; @@ -64,7 +67,6 @@ public class Link { private boolean _gotFollowingPacket; private SSLEngine _sslEngine; - public static String keystoreFile = "/cloudmanagementserver.keystore"; public Link(InetSocketAddress addr, NioConnection connection) { _addr = addr; @@ -99,7 +101,6 @@ public class Link { _sslEngine = sslEngine; } - private static void doWrite(SocketChannel ch, ByteBuffer[] buffers, SSLEngine sslEngine) throws IOException { SSLSession sslSession = sslEngine.getSession(); ByteBuffer pkgBuf = ByteBuffer.allocate(sslSession.getPacketBufferSize() + 40); @@ -119,11 +120,7 @@ public class Link { engResult = sslEngine.wrap(buffers, pkgBuf); if (engResult.getHandshakeStatus() != HandshakeStatus.FINISHED && engResult.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING && engResult.getStatus() != SSLEngineResult.Status.OK) { - String msg = "SSL: SSLEngine returns a bad result on writing! " + engResult; - if (s_logger.isDebugEnabled()) { - s_logger.debug(msg); - } - throw new IOException(msg); + throw new IOException("SSL: SSLEngine return bad result! " + engResult); } processedLen = 0; @@ -184,11 +181,7 @@ public class Link { } if (ch.read(_readBuffer) == -1) { - String msg = "Connection closed with -1 on reading header."; - if(s_logger.isDebugEnabled()) { - s_logger.debug(msg + ch.toString()); - } - throw new IOException(msg); + throw new IOException("Connection closed with -1 on reading size."); } if (_readBuffer.hasRemaining()) { @@ -203,11 +196,7 @@ public class Link { } if (readSize > MAX_SIZE_PER_PACKET) { - String msg = "Wrong packet size: " + readSize; - if (s_logger.isDebugEnabled()) { - s_logger.debug(msg + ": " + ch.toString()); - } - throw new IOException(msg); + throw new IOException("Wrong packet size: " + readSize); } if (!_gotFollowingPacket) { @@ -233,11 +222,7 @@ public class Link { } if (ch.read(_readBuffer) == -1) { - String msg = "Connection closed with -1 on reading data."; - if(s_logger.isDebugEnabled()) { - s_logger.debug(msg + ch.toString()); - } - throw new IOException(msg); + throw new IOException("Connection closed with -1 on read."); } if (_readBuffer.hasRemaining()) { // We're not done yet. @@ -261,18 +246,10 @@ public class Link { engResult = _sslEngine.unwrap(_readBuffer, appBuf); if (engResult.getHandshakeStatus() != HandshakeStatus.FINISHED && engResult.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING && engResult.getStatus() != SSLEngineResult.Status.OK) { - String msg = "SSL: SSLEngine returns a bad result on reading! " + engResult; - if(s_logger.isDebugEnabled()) { - s_logger.debug(msg + ch.toString()); - } - throw new IOException(msg); + throw new IOException("SSL: SSLEngine return bad result! " + engResult); } if (remaining == _readBuffer.remaining()) { - String msg = "SSL: Unable to unwrap received data! still remaining " + remaining + "bytes!"; - if(s_logger.isDebugEnabled()) { - s_logger.debug(msg + ch.toString()); - } - throw new IOException(msg); + throw new IOException("SSL: Unable to unwrap received data! still remaining " + remaining + "bytes!"); } appBuf.flip(); @@ -388,44 +365,79 @@ public class Link { _connection.scheduleTask(task); } - public static SSLContext initSSLContext(boolean isClient) throws GeneralSecurityException, IOException { - InputStream stream; - SSLContext sslContext = null; - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); - KeyStore ks = KeyStore.getInstance("JKS"); - TrustManager[] tms; + public static SSLEngine initServerSSLEngine(final CAService caService, final String clientAddress) throws GeneralSecurityException, IOException { + final SSLContext sslContext = SSLUtils.getSSLContext(); + if (caService != null) { + return caService.createSSLEngine(sslContext, clientAddress); + } + s_logger.error("CA service is not configured, by-passing CA manager to create SSL engine"); + char[] passphrase = KeyStoreUtils.defaultKeystorePassphrase; + final KeyStore ks = loadKeyStore(NioConnection.class.getResourceAsStream("/cloud.keystore"), passphrase); + final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + tmf.init(ks); + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); + return sslContext.createSSLEngine(); + } - File confFile = PropertiesUtil.findConfigFile("db.properties"); - if (null != confFile && !isClient) { - final String pass = DbProperties.getDbProperties().getProperty("db.cloud.keyStorePassphrase"); - char[] passphrase = "vmops.com".toCharArray(); + public static KeyStore loadKeyStore(final InputStream stream, final char[] passphrase) throws GeneralSecurityException, IOException { + final KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(stream, passphrase); + return ks; + } + + public static SSLContext initClientSSLContext() throws GeneralSecurityException, IOException { + final SSLContext sslContext = SSLUtils.getSSLContext(); + + char[] passphrase = KeyStoreUtils.defaultKeystorePassphrase; + File confFile = PropertiesUtil.findConfigFile("agent.properties"); + if (confFile != null) { + s_logger.info("Conf file found: " + confFile.getAbsolutePath()); + final String pass = PropertiesUtil.getProperties(confFile).getProperty(KeyStoreUtils.passphrasePropertyName); if (pass != null) { passphrase = pass.toCharArray(); } - String confPath = confFile.getParent(); - String keystorePath = confPath + keystoreFile; - if (new File(keystorePath).exists()) { - stream = new FileInputStream(keystorePath); - } else { - s_logger.warn("SSL: Fail to find the generated keystore. Loading fail-safe one to continue."); - stream = NioConnection.class.getResourceAsStream("/cloud.keystore"); - passphrase = "vmops.com".toCharArray(); - } - ks.load(stream, passphrase); - stream.close(); - kmf.init(ks, passphrase); - tmf.init(ks); - tms = tmf.getTrustManagers(); } else { - ks.load(null, null); - kmf.init(ks, null); - tms = new TrustManager[1]; - tms[0] = new TrustAllManager(); + confFile = PropertiesUtil.findConfigFile("db.properties"); + if (confFile != null) { + final String pass = DbProperties.getDbProperties().getProperty("db.cloud.keyStorePassphrase"); + if (pass != null) { + passphrase = pass.toCharArray(); + } + } } - sslContext = SSLUtils.getSSLContext(); - sslContext.init(kmf.getKeyManagers(), tms, null); + InputStream stream = null; + if (confFile != null) { + final String confPath = confFile.getParent(); + final String keystorePath = confPath + KeyStoreUtils.defaultKeystoreFile; + if (new File(keystorePath).exists()) { + stream = new FileInputStream(keystorePath); + } + } + + final KeyStore ks = loadKeyStore(stream, passphrase); + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ks); + TrustManager[] tms; + if (stream != null) { + // This enforces a two-way SSL authentication + tms = tmf.getTrustManagers(); + } else { + // This enforces a one-way SSL authentication + tms = new TrustManager[]{new TrustAllManager()}; + s_logger.warn("Failed to load keystore, using trust all manager"); + } + + if (stream != null) { + stream.close(); + } + + final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom()); + if (s_logger.isTraceEnabled()) { s_logger.trace("SSL: SSLcontext has been initialized"); } @@ -467,14 +479,10 @@ public class Link { if (sslEngine.isInboundDone() && sslEngine.isOutboundDone()) { return false; } - s_logger.warn("This SSL engine was forced to close inbound due to end of stream."); try { sslEngine.closeInbound(); } catch (SSLException e) { - // we still need to clean as much as possible but let's report this issue - if(s_logger.isDebugEnabled()) { - s_logger.debug("This SSL engine failed to close inbound : " + e.getLocalizedMessage(),e); - } + s_logger.warn("This SSL engine was forced to close inbound due to end of stream."); } sslEngine.closeOutbound(); // After closeOutbound the engine will be set to WRAP state, @@ -486,8 +494,9 @@ public class Link { try { result = sslEngine.unwrap(peerNetData, peerAppData); peerNetData.compact(); - } catch (SSLException sslException) { - s_logger.error("SSL error occurred while processing unwrap data: " + sslException.getMessage()); + } catch (final SSLException sslException) { + s_logger.error(String.format("SSL error caught during unwrap data: %s, for local address=%s, remote address=%s. The client may have invalid ca-certificates.", + sslException.getMessage(), socketChannel.getLocalAddress(), socketChannel.getRemoteAddress())); sslEngine.closeOutbound(); return true; } @@ -511,11 +520,7 @@ public class Link { break; } default: - String msg = "Invalid SSL status: " + result.getStatus(); - if(s_logger.isDebugEnabled()) { - s_logger.debug(msg + ": " + socketChannel.toString()); - } - throw new IllegalStateException(msg); + throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); } return true; } @@ -531,8 +536,9 @@ public class Link { SSLEngineResult result = null; try { result = sslEngine.wrap(myAppData, myNetData); - } catch (SSLException sslException) { - s_logger.error("SSL error occurred while processing wrap data: " + sslException.getMessage()); + } catch (final SSLException sslException) { + s_logger.error(String.format("SSL error caught during wrap data: %s, for local address=%s, remote address=%s.", + sslException.getMessage(), socketChannel.getLocalAddress(), socketChannel.getRemoteAddress())); sslEngine.closeOutbound(); return true; } @@ -551,11 +557,7 @@ public class Link { myNetData = enlargeBuffer(myNetData, netBufferSize); break; case BUFFER_UNDERFLOW: - String msg = "Buffer underflow occurred after a wrap. We should not reach here."; - if(s_logger.isDebugEnabled()) { - s_logger.debug(msg + socketChannel.toString()); - } - throw new SSLException(msg); + throw new SSLException("Buffer underflow occurred after a wrap. We should not reach here."); case CLOSED: try { myNetData.flip(); @@ -611,6 +613,9 @@ public class Link { case NEED_TASK: Runnable task; while ((task = sslEngine.getDelegatedTask()) != null) { + if (s_logger.isTraceEnabled()) { + s_logger.trace("SSL: Running delegated task!"); + } task.run(); } break; @@ -619,11 +624,7 @@ public class Link { case NOT_HANDSHAKING: break; default: - String msg = "Invalid SSL status: " + handshakeStatus; - if(s_logger.isDebugEnabled()) { - s_logger.debug(msg + ": " + socketChannel.toString()); - } - throw new IllegalStateException(msg); + throw new IllegalStateException("Invalid SSL status: " + handshakeStatus); } handshakeStatus = sslEngine.getHandshakeStatus(); } diff --git a/utils/src/com/cloud/utils/nio/NioClient.java b/utils/src/com/cloud/utils/nio/NioClient.java index a8db8e8e5c1..1c29b0c1a2d 100755 --- a/utils/src/com/cloud/utils/nio/NioClient.java +++ b/utils/src/com/cloud/utils/nio/NioClient.java @@ -29,9 +29,8 @@ import java.security.GeneralSecurityException; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; -import org.apache.log4j.Logger; - import org.apache.cloudstack.utils.security.SSLUtils; +import org.apache.log4j.Logger; public class NioClient extends NioConnection { private static final Logger s_logger = Logger.getLogger(NioClient.class); @@ -39,7 +38,7 @@ public class NioClient extends NioConnection { protected String _host; protected SocketChannel _clientConnection; - public NioClient(String name, String host, int port, int workers, HandlerFactory factory) { + public NioClient(final String name, final String host, final int port, final int workers, final HandlerFactory factory) { super(name, port, workers, factory); _host = host; } @@ -51,17 +50,16 @@ public class NioClient extends NioConnection { try { _clientConnection = SocketChannel.open(); - s_logger.info("Connecting to " + _host + ":" + _port); - InetSocketAddress peerAddr = new InetSocketAddress(_host, _port); + s_logger.info("Connecting to " + _host + ":" + _port); + final InetSocketAddress peerAddr = new InetSocketAddress(_host, _port); _clientConnection.connect(peerAddr); _clientConnection.configureBlocking(false); - final SSLContext sslContext = Link.initSSLContext(true); + final SSLContext sslContext = Link.initClientSSLContext(); SSLEngine sslEngine = sslContext.createSSLEngine(_host, _port); sslEngine.setUseClientMode(true); sslEngine.setEnabledProtocols(SSLUtils.getSupportedProtocols(sslEngine.getEnabledProtocols())); - sslEngine.beginHandshake(); if (!Link.doHandshake(_clientConnection, sslEngine, true)) { s_logger.error("SSL Handshake failed while connecting to host: " + _host + " port: " + _port); @@ -71,34 +69,31 @@ public class NioClient extends NioConnection { s_logger.info("SSL: Handshake done"); s_logger.info("Connected to " + _host + ":" + _port); - _clientConnection.configureBlocking(false); - Link link = new Link(peerAddr, this); + final Link link = new Link(peerAddr, this); link.setSSLEngine(sslEngine); - SelectionKey key = _clientConnection.register(_selector, SelectionKey.OP_READ); + final SelectionKey key = _clientConnection.register(_selector, SelectionKey.OP_READ); link.setKey(key); key.attach(link); // Notice we've already connected due to the handshake, so let's get the // remaining task done task = _factory.create(Task.Type.CONNECT, link, null); - } catch (GeneralSecurityException e) { - s_logger.error("Failed to initialize security, connecting to host: " + _host + " port: " + _port); + } catch (final GeneralSecurityException e) { _selector.close(); throw new IOException("Failed to initialise security", e); - } catch (IOException e) { + } catch (final IOException e) { _selector.close(); throw e; } - - _executor.execute(task); + _executor.submit(task); } @Override - protected void registerLink(InetSocketAddress saddr, Link link) { + protected void registerLink(final InetSocketAddress saddr, final Link link) { // don't do anything. } @Override - protected void unregisterLink(InetSocketAddress saddr) { + protected void unregisterLink(final InetSocketAddress saddr) { // don't do anything. } @@ -110,4 +105,4 @@ public class NioClient extends NioConnection { } s_logger.info("NioClient connection closed"); } -} +} \ No newline at end of file diff --git a/utils/src/com/cloud/utils/nio/NioConnection.java b/utils/src/com/cloud/utils/nio/NioConnection.java index d78ee3a4909..30000cf618b 100755 --- a/utils/src/com/cloud/utils/nio/NioConnection.java +++ b/utils/src/com/cloud/utils/nio/NioConnection.java @@ -19,12 +19,15 @@ package com.cloud.utils.nio; +import static com.cloud.utils.AutoCloseableUtil.closeAutoCloseable; + import java.io.IOException; import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; +import java.nio.channels.ClosedSelectorException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; @@ -33,30 +36,34 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; -import java.util.concurrent.Executors; +import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; +import org.apache.cloudstack.framework.ca.CAService; import org.apache.cloudstack.utils.security.SSLUtils; - import org.apache.log4j.Logger; import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.exception.NioConnectionException; /** * NioConnection abstracts the NIO socket operations. The Java implementation * provides that. */ -public abstract class NioConnection implements Runnable { - private static final Logger s_logger = Logger.getLogger(NioConnection.class); +public abstract class NioConnection implements Callable { + private static final Logger s_logger = Logger.getLogger(NioConnection.class);; protected Selector _selector; - protected Thread _thread; + protected ExecutorService _threadExecutor; + protected Future _futureTask; + protected boolean _isRunning; protected boolean _isStartup; protected int _port; @@ -65,11 +72,11 @@ public abstract class NioConnection implements Runnable { protected String _name; protected ExecutorService _executor; protected ExecutorService _sslHandshakeExecutor; + protected CAService caService; - public NioConnection(String name, int port, int workers, HandlerFactory factory) { + public NioConnection(final String name, final int port, final int workers, final HandlerFactory factory) { _name = name; _isRunning = false; - _thread = null; _selector = null; _port = port; _factory = factory; @@ -77,32 +84,43 @@ public abstract class NioConnection implements Runnable { _sslHandshakeExecutor = Executors.newCachedThreadPool(new NamedThreadFactory(name + "-SSLHandshakeHandler")); } - public void start() { + public void setCAService(final CAService caService) { + this.caService = caService; + } + + public void start() throws NioConnectionException { _todos = new ArrayList(); - _thread = new Thread(this, _name + "-Selector"); - _isRunning = true; - _thread.start(); - // Wait until we got init() done - synchronized (_thread) { - try { - _thread.wait(); - } catch (InterruptedException e) { - s_logger.warn("Interrupted start thread ", e); - } + try { + init(); + } catch (final ConnectException e) { + s_logger.warn("Unable to connect to remote: is there a server running on port " + _port); + return; + } catch (final IOException e) { + s_logger.error("Unable to initialize the threads.", e); + throw new NioConnectionException(e.getMessage(), e); + } catch (final Exception e) { + s_logger.error("Unable to initialize the threads due to unknown exception.", e); + throw new NioConnectionException(e.getMessage(), e); } + _isStartup = true; + + _threadExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory(this._name + "-NioConnectionHandler")); + _isRunning = true; + _futureTask = _threadExecutor.submit(this); } public void stop() { _executor.shutdown(); _isRunning = false; - if (_thread != null) { - _thread.interrupt(); + if (_threadExecutor != null) { + _futureTask.cancel(false); + _threadExecutor.shutdown(); } } public boolean isRunning() { - return _thread.isAlive(); + return !_futureTask.isDone(); } public boolean isStartup() { @@ -110,45 +128,28 @@ public abstract class NioConnection implements Runnable { } @Override - public void run() { - synchronized (_thread) { - try { - init(); - } catch (ConnectException e) { - s_logger.warn("Unable to connect to remote: is there a server running on port " + _port); - return; - } catch (IOException e) { - s_logger.error("Unable to initialize the threads.", e); - return; - } catch (Exception e) { - s_logger.error("Unable to initialize the threads due to unknown exception.", e); - return; - } - _isStartup = true; - _thread.notifyAll(); - } - + public Boolean call() throws NioConnectionException { while (_isRunning) { try { - _selector.select(1000); + _selector.select(50); // Someone is ready for I/O, get the ready keys - Set readyKeys = _selector.selectedKeys(); - Iterator i = readyKeys.iterator(); + final Set readyKeys = _selector.selectedKeys(); + final Iterator i = readyKeys.iterator(); if (s_logger.isTraceEnabled()) { s_logger.trace("Keys Processing: " + readyKeys.size()); } // Walk through the ready keys collection. while (i.hasNext()) { - SelectionKey sk = i.next(); + final SelectionKey sk = i.next(); i.remove(); if (!sk.isValid()) { if (s_logger.isTraceEnabled()) { s_logger.trace("Selection Key is invalid: " + sk.toString()); } - Link link = (Link)sk.attachment(); + final Link link = (Link)sk.attachment(); if (link != null) { link.terminated(); } else { @@ -168,13 +169,18 @@ public abstract class NioConnection implements Runnable { s_logger.trace("Keys Done Processing."); processTodos(); - } catch (Throwable e) { - s_logger.warn("Caught an exception but continuing on.", e); + } catch (final ClosedSelectorException e) { + /* + * Exception occurred when calling java.nio.channels.Selector.selectedKeys() method. It means the connection has not yet been established. Let's continue trying + * We do not log it here otherwise we will fill the disk with messages. + */ + } catch (final IOException e) { + s_logger.error("Agent will die due to this IOException!", e); + throw new NioConnectionException(e.getMessage(), e); } } - synchronized (_thread) { - _isStartup = false; - } + _isStartup = false; + return true; } abstract void init() throws IOException; @@ -185,20 +191,21 @@ public abstract class NioConnection implements Runnable { protected void accept(final SelectionKey key) throws IOException { final ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel(); - final SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); + final Socket socket = socketChannel.socket(); socket.setKeepAlive(true); + if (s_logger.isTraceEnabled()) { + s_logger.trace("Connection accepted for " + socket); + } + final SSLEngine sslEngine; try { - final SSLContext sslContext = Link.initSSLContext(false); - sslEngine = sslContext.createSSLEngine(); + sslEngine = Link.initServerSSLEngine(caService, socketChannel.getRemoteAddress().toString()); sslEngine.setUseClientMode(false); - sslEngine.setNeedClientAuth(false); sslEngine.setEnabledProtocols(SSLUtils.getSupportedProtocols(sslEngine.getEnabledProtocols())); - final NioConnection nioConnection = this; _sslHandshakeExecutor.submit(new Runnable() { @Override @@ -207,11 +214,7 @@ public abstract class NioConnection implements Runnable { try { sslEngine.beginHandshake(); if (!Link.doHandshake(socketChannel, sslEngine, false)) { - String msg = "SSL handshake timed out with " + socketChannel.getRemoteAddress(); - if(s_logger.isDebugEnabled()) { - s_logger.debug(msg + ": " + socketChannel.toString()); - } - throw new IOException(msg); + throw new IOException("SSL handshake timed out with " + socketChannel.getRemoteAddress()); } if (s_logger.isTraceEnabled()) { s_logger.trace("SSL: Handshake done"); @@ -227,74 +230,72 @@ public abstract class NioConnection implements Runnable { if (s_logger.isTraceEnabled()) { s_logger.trace("Connection closed due to failure: " + e.getMessage()); } - try { - socketChannel.close(); - socket.close(); - } catch (IOException toBeLogged) { - if(s_logger.isDebugEnabled()) { - s_logger.debug("closing a channel failed during ssl handshake task " + socketChannel,toBeLogged); - } - } + closeAutoCloseable(socket, "accepting socket"); + closeAutoCloseable(socketChannel, "accepting socketChannel"); } finally { _selector.wakeup(); } } }); - } catch (final Exception e) { - if (s_logger.isTraceEnabled()) { + } catch (final Exception e) { + if (s_logger.isTraceEnabled()) { s_logger.trace("Connection closed due to failure: " + e.getMessage()); - } - try { - socketChannel.close(); - socket.close(); - } catch (IOException ignore) { - if(s_logger.isDebugEnabled()) { - s_logger.debug("closing a channel failed due to handshake " + socketChannel,e); - } } + closeAutoCloseable(socket, "accepting socket"); + closeAutoCloseable(socketChannel, "accepting socketChannel"); } finally { _selector.wakeup(); } } - protected void terminate(SelectionKey key) { - Link link = (Link)key.attachment(); + protected void terminate(final SelectionKey key) { + final Link link = (Link)key.attachment(); closeConnection(key); if (link != null) { link.terminated(); - Task task = _factory.create(Task.Type.DISCONNECT, link, null); + final Task task = _factory.create(Task.Type.DISCONNECT, link, null); unregisterLink(link.getSocketAddress()); - _executor.execute(task); + + try { + _executor.submit(task); + } catch (final Exception e) { + s_logger.warn("Exception occurred when submitting the task", e); + } } } - protected void read(SelectionKey key) throws IOException { - Link link = (Link)key.attachment(); + protected void read(final SelectionKey key) throws IOException { + final Link link = (Link)key.attachment(); try { - SocketChannel socketChannel = (SocketChannel)key.channel(); + final SocketChannel socketChannel = (SocketChannel)key.channel(); if (s_logger.isTraceEnabled()) { s_logger.trace("Reading from: " + socketChannel.socket().toString()); } - byte[] data = link.read(socketChannel); + final byte[] data = link.read(socketChannel); if (data == null) { if (s_logger.isTraceEnabled()) { s_logger.trace("Packet is incomplete. Waiting for more."); } return; } - Task task = _factory.create(Task.Type.DATA, link, data); - _executor.execute(task); - } catch (Exception e) { + final Task task = _factory.create(Task.Type.DATA, link, data); + + try { + _executor.submit(task); + } catch (final Exception e) { + s_logger.warn("Exception occurred when submitting the task", e); + } + } catch (final Exception e) { logDebug(e, key, 1); terminate(key); } } - protected void logTrace(Exception e, SelectionKey key, int loc) { + protected void logTrace(final Exception e, final SelectionKey key, final int loc) { if (s_logger.isTraceEnabled()) { Socket socket = null; if (key != null) { - SocketChannel ch = (SocketChannel)key.channel(); + final SocketChannel ch = (SocketChannel)key.channel(); if (ch != null) { socket = ch.socket(); } @@ -304,11 +305,11 @@ public abstract class NioConnection implements Runnable { } } - protected void logDebug(Exception e, SelectionKey key, int loc) { + protected void logDebug(final Exception e, final SelectionKey key, final int loc) { if (s_logger.isDebugEnabled()) { Socket socket = null; if (key != null) { - SocketChannel ch = (SocketChannel)key.channel(); + final SocketChannel ch = (SocketChannel)key.channel(); if (ch != null) { socket = ch.socket(); } @@ -333,112 +334,122 @@ public abstract class NioConnection implements Runnable { s_logger.trace("Todos Processing: " + todos.size()); } SelectionKey key; - for (ChangeRequest todo : todos) { + for (final ChangeRequest todo : todos) { switch (todo.type) { - case ChangeRequest.CHANGEOPS: - try { - key = (SelectionKey)todo.key; - if (key != null && key.isValid()) { - if (todo.att != null) { - key.attach(todo.att); - Link link = (Link)todo.att; - link.setKey(key); - } - key.interestOps(todo.ops); - } - } catch (CancelledKeyException e) { - s_logger.debug("key has been cancelled"); - } - break; - case ChangeRequest.REGISTER: - try { - key = ((SocketChannel)(todo.key)).register(_selector, todo.ops, todo.att); + case ChangeRequest.CHANGEOPS: + try { + key = (SelectionKey)todo.key; + if (key != null && key.isValid()) { if (todo.att != null) { - Link link = (Link)todo.att; + key.attach(todo.att); + final Link link = (Link)todo.att; link.setKey(key); } - } catch (ClosedChannelException e) { - s_logger.warn("Couldn't register socket: " + todo.key); - try { - ((SocketChannel)(todo.key)).close(); - } catch (IOException ignore) { - } finally { - Link link = (Link)todo.att; - link.terminated(); - } + key.interestOps(todo.ops); } - break; - case ChangeRequest.CLOSE: - if (s_logger.isTraceEnabled()) { - s_logger.trace("Trying to close " + todo.key); + } catch (final CancelledKeyException e) { + s_logger.debug("key has been cancelled"); + } + break; + case ChangeRequest.REGISTER: + try { + key = ((SocketChannel)todo.key).register(_selector, todo.ops, todo.att); + if (todo.att != null) { + final Link link = (Link)todo.att; + link.setKey(key); } - key = (SelectionKey)todo.key; - closeConnection(key); - if (key != null) { - Link link = (Link)key.attachment(); - if (link != null) { - link.terminated(); - } + } catch (final ClosedChannelException e) { + s_logger.warn("Couldn't register socket: " + todo.key); + try { + ((SocketChannel)todo.key).close(); + } catch (final IOException ignore) { + s_logger.info("[ignored] socket channel"); + } finally { + final Link link = (Link)todo.att; + link.terminated(); } - break; - default: - s_logger.warn("Shouldn't be here"); - throw new RuntimeException("Shouldn't be here"); + } + break; + case ChangeRequest.CLOSE: + if (s_logger.isTraceEnabled()) { + s_logger.trace("Trying to close " + todo.key); + } + key = (SelectionKey)todo.key; + closeConnection(key); + if (key != null) { + final Link link = (Link)key.attachment(); + if (link != null) { + link.terminated(); + } + } + break; + default: + s_logger.warn("Shouldn't be here"); + throw new RuntimeException("Shouldn't be here"); } } s_logger.trace("Todos Done processing"); } - protected void connect(SelectionKey key) throws IOException { - SocketChannel socketChannel = (SocketChannel)key.channel(); + protected void connect(final SelectionKey key) throws IOException { + final SocketChannel socketChannel = (SocketChannel)key.channel(); try { socketChannel.finishConnect(); key.interestOps(SelectionKey.OP_READ); - Socket socket = socketChannel.socket(); + final Socket socket = socketChannel.socket(); if (!socket.getKeepAlive()) { socket.setKeepAlive(true); } if (s_logger.isDebugEnabled()) { s_logger.debug("Connected to " + socket); } - Link link = new Link((InetSocketAddress)socket.getRemoteSocketAddress(), this); + final Link link = new Link((InetSocketAddress)socket.getRemoteSocketAddress(), this); link.setKey(key); key.attach(link); - Task task = _factory.create(Task.Type.CONNECT, link, null); - _executor.execute(task); - } catch (IOException e) { + final Task task = _factory.create(Task.Type.CONNECT, link, null); + + try { + _executor.submit(task); + } catch (final Exception e) { + s_logger.warn("Exception occurred when submitting the task", e); + } + } catch (final IOException e) { logTrace(e, key, 2); terminate(key); } } - protected void scheduleTask(Task task) { - _executor.execute(task); + protected void scheduleTask(final Task task) { + try { + _executor.submit(task); + } catch (final Exception e) { + s_logger.warn("Exception occurred when submitting the task", e); + } } - protected void write(SelectionKey key) throws IOException { - Link link = (Link)key.attachment(); + protected void write(final SelectionKey key) throws IOException { + final Link link = (Link)key.attachment(); try { if (s_logger.isTraceEnabled()) { s_logger.trace("Writing to " + link.getSocketAddress().toString()); } - boolean close = link.write((SocketChannel)key.channel()); + final boolean close = link.write((SocketChannel)key.channel()); if (close) { closeConnection(key); link.terminated(); } else { key.interestOps(SelectionKey.OP_READ); } - } catch (Exception e) { + } catch (final Exception e) { logDebug(e, key, 3); terminate(key); } } - protected void closeConnection(SelectionKey key) { + protected void closeConnection(final SelectionKey key) { if (key != null) { - SocketChannel channel = (SocketChannel)key.channel(); + final SocketChannel channel = (SocketChannel)key.channel(); key.cancel(); try { if (channel != null) { @@ -447,29 +458,30 @@ public abstract class NioConnection implements Runnable { } channel.close(); } - } catch (IOException ignore) { + } catch (final IOException ignore) { + s_logger.info("[ignored] channel"); } } } - public void register(int ops, SocketChannel key, Object att) { - ChangeRequest todo = new ChangeRequest(key, ChangeRequest.REGISTER, ops, att); + public void register(final int ops, final SocketChannel key, final Object att) { + final ChangeRequest todo = new ChangeRequest(key, ChangeRequest.REGISTER, ops, att); synchronized (this) { _todos.add(todo); } _selector.wakeup(); } - public void change(int ops, SelectionKey key, Object att) { - ChangeRequest todo = new ChangeRequest(key, ChangeRequest.CHANGEOPS, ops, att); + public void change(final int ops, final SelectionKey key, final Object att) { + final ChangeRequest todo = new ChangeRequest(key, ChangeRequest.CHANGEOPS, ops, att); synchronized (this) { _todos.add(todo); } _selector.wakeup(); } - public void close(SelectionKey key) { - ChangeRequest todo = new ChangeRequest(key, ChangeRequest.CLOSE, 0, null); + public void close(final SelectionKey key) { + final ChangeRequest todo = new ChangeRequest(key, ChangeRequest.CLOSE, 0, null); synchronized (this) { _todos.add(todo); } @@ -493,7 +505,7 @@ public abstract class NioConnection implements Runnable { public int ops; public Object att; - public ChangeRequest(Object key, int type, int ops, Object att) { + public ChangeRequest(final Object key, final int type, final int ops, final Object att) { this.key = key; this.type = type; this.ops = ops; diff --git a/utils/src/com/cloud/utils/nio/NioServer.java b/utils/src/com/cloud/utils/nio/NioServer.java index adcecda4e64..ff54165841e 100755 --- a/utils/src/com/cloud/utils/nio/NioServer.java +++ b/utils/src/com/cloud/utils/nio/NioServer.java @@ -27,6 +27,7 @@ import java.nio.channels.ServerSocketChannel; import java.nio.channels.spi.SelectorProvider; import java.util.WeakHashMap; +import org.apache.cloudstack.framework.ca.CAService; import org.apache.log4j.Logger; public class NioServer extends NioConnection { @@ -37,8 +38,9 @@ public class NioServer extends NioConnection { protected WeakHashMap _links; - public NioServer(String name, int port, int workers, HandlerFactory factory) { + public NioServer(final String name, final int port, final int workers, final HandlerFactory factory, final CAService caService) { super(name, port, workers, factory); + setCAService(caService); _localAddr = null; _links = new WeakHashMap(1024); } @@ -72,12 +74,12 @@ public class NioServer extends NioConnection { } @Override - protected void registerLink(InetSocketAddress addr, Link link) { + protected void registerLink(final InetSocketAddress addr, final Link link) { _links.put(addr, link); } @Override - protected void unregisterLink(InetSocketAddress saddr) { + protected void unregisterLink(final InetSocketAddress saddr) { _links.remove(saddr); } @@ -90,8 +92,8 @@ public class NioServer extends NioConnection { * @param data * @return null if not sent. attach object in link if sent. */ - public Object send(InetSocketAddress saddr, byte[] data) throws ClosedChannelException { - Link link = _links.get(saddr); + public Object send(final InetSocketAddress saddr, final byte[] data) throws ClosedChannelException { + final Link link = _links.get(saddr); if (link == null) { return null; } diff --git a/utils/src/com/cloud/utils/nio/Task.java b/utils/src/com/cloud/utils/nio/Task.java index c77c7035687..60228ef9412 100755 --- a/utils/src/com/cloud/utils/nio/Task.java +++ b/utils/src/com/cloud/utils/nio/Task.java @@ -19,14 +19,14 @@ package com.cloud.utils.nio; -import org.apache.log4j.Logger; +import java.util.concurrent.Callable; + +import com.cloud.utils.exception.TaskExecutionException; /** * Task represents one todo item for the AgentManager or the AgentManager - * */ -public abstract class Task implements Runnable { - private static final Logger s_logger = Logger.getLogger(Task.class); +public abstract class Task implements Callable { public enum Type { CONNECT, // Process a new connection. @@ -40,13 +40,13 @@ public abstract class Task implements Runnable { Type _type; Link _link; - public Task(Type type, Link link, byte[] data) { + public Task(final Type type, final Link link, final byte[] data) { _data = data; _type = type; _link = link; } - public Task(Type type, Link link, Object data) { + public Task(final Type type, final Link link, final Object data) { _data = data; _type = type; _link = link; @@ -76,14 +76,11 @@ public abstract class Task implements Runnable { return _type.toString(); } - abstract protected void doTask(Task task) throws Exception; + abstract protected void doTask(Task task) throws TaskExecutionException; @Override - public final void run() { - try { - doTask(this); - } catch (Throwable e) { - s_logger.warn("Caught the following exception but pushing on", e); - } + public Boolean call() throws TaskExecutionException { + doTask(this); + return true; } -} +} \ No newline at end of file diff --git a/utils/src/com/cloud/utils/script/Script.java b/utils/src/com/cloud/utils/script/Script.java index 49734ae808a..b8a9256889a 100755 --- a/utils/src/com/cloud/utils/script/Script.java +++ b/utils/src/com/cloud/utils/script/Script.java @@ -38,6 +38,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; @@ -183,7 +184,7 @@ public class Script implements Callable { String[] command = _command.toArray(new String[_command.size()]); if (_logger.isDebugEnabled()) { - _logger.debug("Executing: " + buildCommandLine(command)); + _logger.debug("Executing: " + buildCommandLine(command).split(KeyStoreUtils.defaultKeystoreFile)[0]); } try { diff --git a/utils/src/com/cloud/utils/ssh/SSHCmdHelper.java b/utils/src/com/cloud/utils/ssh/SSHCmdHelper.java index e35a3ea680b..60a27c37605 100644 --- a/utils/src/com/cloud/utils/ssh/SSHCmdHelper.java +++ b/utils/src/com/cloud/utils/ssh/SSHCmdHelper.java @@ -22,14 +22,54 @@ package com.cloud.utils.ssh; import java.io.IOException; import java.io.InputStream; +import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.log4j.Logger; +import com.google.common.base.Strings; import com.trilead.ssh2.ChannelCondition; import com.trilead.ssh2.Session; public class SSHCmdHelper { private static final Logger s_logger = Logger.getLogger(SSHCmdHelper.class); + public static class SSHCmdResult { + private int returnCode = -1; + private String stdOut; + private String stdErr; + + public SSHCmdResult(final int returnCode, final String stdOut, final String stdErr) { + this.returnCode = returnCode; + this.stdOut = stdOut; + this.stdErr = stdErr; + } + + @Override + public String toString() { + return String.format("SSH cmd result: return code=%d, stdout=%s, stderr=%s", + getReturnCode(), getStdOut().split("-----BEGIN")[0], getStdErr()); + } + + public boolean isSuccess() { + return returnCode == 0; + } + + public int getReturnCode() { + return returnCode; + } + + public void setReturnCode(int returnCode) { + this.returnCode = returnCode; + } + + public String getStdOut() { + return stdOut; + } + + public String getStdErr() { + return stdErr; + } + } + public static com.trilead.ssh2.Connection acquireAuthorizedConnection(String ip, String username, String password) { return acquireAuthorizedConnection(ip, 22, username, password); } @@ -63,36 +103,41 @@ public class SSHCmdHelper { public static boolean sshExecuteCmd(com.trilead.ssh2.Connection sshConnection, String cmd, int nTimes) { for (int i = 0; i < nTimes; i++) { try { - if (sshExecuteCmdOneShot(sshConnection, cmd)) + final SSHCmdResult result = sshExecuteCmdOneShot(sshConnection, cmd); + if (result.isSuccess()) { return true; - } catch (SshException e) { + } + } catch (SshException ignored) { continue; } } return false; } - public static int sshExecuteCmdWithExitCode(com.trilead.ssh2.Connection sshConnection, String cmd) { - return sshExecuteCmdWithExitCode(sshConnection, cmd, 3); - } - - public static int sshExecuteCmdWithExitCode(com.trilead.ssh2.Connection sshConnection, String cmd, int nTimes) { + public static SSHCmdResult sshExecuteCmdWithResult(com.trilead.ssh2.Connection sshConnection, String cmd, int nTimes) { for (int i = 0; i < nTimes; i++) { try { - return sshExecuteCmdOneShotWithExitCode(sshConnection, cmd); - } catch (SshException e) { + final SSHCmdResult result = sshExecuteCmdOneShot(sshConnection, cmd); + if (result.isSuccess()) { + return result; + } + } catch (SshException ignored) { continue; } } - return -1; + return new SSHCmdResult(-1, null, null); } public static boolean sshExecuteCmd(com.trilead.ssh2.Connection sshConnection, String cmd) { return sshExecuteCmd(sshConnection, cmd, 3); } - public static int sshExecuteCmdOneShotWithExitCode(com.trilead.ssh2.Connection sshConnection, String cmd) throws SshException { - s_logger.debug("Executing cmd: " + cmd); + public static SSHCmdResult sshExecuteCmdWithResult(com.trilead.ssh2.Connection sshConnection, String cmd) { + return sshExecuteCmdWithResult(sshConnection, cmd, 3); + } + + public static SSHCmdResult sshExecuteCmdOneShot(com.trilead.ssh2.Connection sshConnection, String cmd) throws SshException { + s_logger.debug("Executing cmd: " + cmd.split(KeyStoreUtils.defaultKeystoreFile)[0]); Session sshSession = null; try { sshSession = sshConnection.openSession(); @@ -110,7 +155,8 @@ public class SSHCmdHelper { InputStream stderr = sshSession.getStderr(); byte[] buffer = new byte[8192]; - StringBuffer sbResult = new StringBuffer(); + StringBuffer sbStdoutResult = new StringBuffer(); + StringBuffer sbStdErrResult = new StringBuffer(); int currentReadBytes = 0; while (true) { @@ -143,27 +189,30 @@ public class SSHCmdHelper { while (stdout.available() > 0) { currentReadBytes = stdout.read(buffer); - sbResult.append(new String(buffer, 0, currentReadBytes)); + sbStdoutResult.append(new String(buffer, 0, currentReadBytes)); } while (stderr.available() > 0) { currentReadBytes = stderr.read(buffer); - sbResult.append(new String(buffer, 0, currentReadBytes)); + sbStdErrResult.append(new String(buffer, 0, currentReadBytes)); } } - String result = sbResult.toString(); - if (result != null && !result.isEmpty()) - s_logger.debug(cmd + " output:" + result); + final SSHCmdResult result = new SSHCmdResult(-1, sbStdoutResult.toString(), sbStdErrResult.toString()); + if (!Strings.isNullOrEmpty(result.getStdOut()) || !Strings.isNullOrEmpty(result.getStdErr())) { + s_logger.debug("SSH command: " + cmd.split(KeyStoreUtils.defaultKeystoreFile)[0] + "\nSSH command output:" + result.getStdOut().split("-----BEGIN")[0] + "\n" + result.getStdErr()); + } + // exit status delivery might get delayed for(int i = 0 ; i<10 ; i++ ) { Integer status = sshSession.getExitStatus(); if( status != null ) { - return status; + result.setReturnCode(status); + return result; } Thread.sleep(100); } - return -1; + return result; } catch (Exception e) { s_logger.debug("Ssh executed failed", e); throw new SshException("Ssh executed failed " + e.getMessage()); @@ -172,8 +221,4 @@ public class SSHCmdHelper { sshSession.close(); } } - - public static boolean sshExecuteCmdOneShot(com.trilead.ssh2.Connection sshConnection, String cmd) throws SshException { - return sshExecuteCmdOneShotWithExitCode(sshConnection, cmd) == 0; - } } diff --git a/utils/src/org/apache/cloudstack/utils/security/CertUtils.java b/utils/src/org/apache/cloudstack/utils/security/CertUtils.java new file mode 100644 index 00000000000..0aafa9b9b52 --- /dev/null +++ b/utils/src/org/apache/cloudstack/utils/security/CertUtils.java @@ -0,0 +1,219 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.utils.security; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.List; + +import javax.security.auth.x500.X500Principal; + +import org.apache.log4j.Logger; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.X509Extensions; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMReader; +import org.bouncycastle.openssl.PEMWriter; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.x509.X509V1CertificateGenerator; +import org.bouncycastle.x509.X509V3CertificateGenerator; +import org.bouncycastle.x509.extension.AuthorityKeyIdentifierStructure; +import org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import com.google.common.base.Strings; + +public class CertUtils { + + private static final Logger LOG = Logger.getLogger(CertUtils.class); + + public static KeyPair generateRandomKeyPair(final int keySize) throws NoSuchProviderException, NoSuchAlgorithmException { + Security.addProvider(new BouncyCastleProvider()); + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(keySize, new SecureRandom()); + return keyPairGenerator.generateKeyPair(); + } + + private static KeyFactory getKeyFactory() { + KeyFactory keyFactory = null; + try { + Security.addProvider(new BouncyCastleProvider()); + keyFactory = KeyFactory.getInstance("RSA", "BC"); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + LOG.error("Unable to create KeyFactory:" + e.getMessage()); + } + return keyFactory; + } + + public static X509Certificate pemToX509Certificate(final String pem) throws IOException { + final PEMReader pr = new PEMReader(new StringReader(pem)); + return (X509Certificate) pr.readObject(); + } + + public static String x509CertificateToPem(final X509Certificate cert) throws IOException { + final StringWriter sw = new StringWriter(); + try (final PEMWriter pw = new PEMWriter(sw)) { + pw.writeObject(cert); + } + return sw.toString(); + } + + public static String x509CertificatesToPem(final List certificates) throws IOException { + if (certificates == null) { + return ""; + } + final StringBuilder buffer = new StringBuilder(); + for (final X509Certificate certificate: certificates) { + buffer.append(CertUtils.x509CertificateToPem(certificate)); + } + return buffer.toString(); + } + + public static PrivateKey pemToPrivateKey(final String pem) throws InvalidKeySpecException, IOException { + final PEMReader pr = new PEMReader(new StringReader(pem)); + final PemObject pemObject = pr.readPemObject(); + final KeyFactory keyFactory = getKeyFactory(); + return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(pemObject.getContent())); + } + + public static String privateKeyToPem(final PrivateKey key) throws IOException { + final PemObject pemObject = new PemObject("RSA PRIVATE KEY", key.getEncoded()); + final StringWriter sw = new StringWriter(); + try (final PEMWriter pw = new PEMWriter(sw)) { + pw.writeObject(pemObject); + } + return sw.toString(); + } + + public static PublicKey pemToPublicKey(final String pem) throws InvalidKeySpecException, IOException { + final PEMReader pr = new PEMReader(new StringReader(pem)); + final PemObject pemObject = pr.readPemObject(); + final KeyFactory keyFactory = getKeyFactory(); + return keyFactory.generatePublic(new X509EncodedKeySpec(pemObject.getContent())); + } + + public static String publicKeyToPem(final PublicKey key) throws IOException { + final PemObject pemObject = new PemObject("PUBLIC KEY", key.getEncoded()); + final StringWriter sw = new StringWriter(); + try (final PEMWriter pw = new PEMWriter(sw)) { + pw.writeObject(pemObject); + } + return sw.toString(); + } + + public static BigInteger generateRandomBigInt() { + return new BigInteger(64, new SecureRandom()); + } + + public static X509Certificate generateV1Certificate(final KeyPair keyPair, + final String subjectDN, + final String issuerDN, + final int validityYears, + final String signatureAlgorithm) throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException { + final DateTime now = DateTime.now(DateTimeZone.UTC); + final X500Principal subjectDn = new X500Principal(subjectDN); + final X500Principal issuerDn = new X500Principal(issuerDN); + final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); + certGen.setSerialNumber(generateRandomBigInt()); + certGen.setSubjectDN(subjectDn); + certGen.setIssuerDN(issuerDn); + certGen.setNotBefore(now.minusDays(1).toDate()); + certGen.setNotAfter(now.plusYears(validityYears).toDate()); + certGen.setPublicKey(keyPair.getPublic()); + certGen.setSignatureAlgorithm(signatureAlgorithm); + return certGen.generate(keyPair.getPrivate(), "BC"); + } + + public static X509Certificate generateV3Certificate(final X509Certificate caCert, + final PrivateKey caPrivateKey, + final PublicKey clientPublicKey, + final String subjectDN, + final String signatureAlgorithm, + final int validityDays, + final List dnsNames, + final List publicIPAddresses) throws IOException, NoSuchAlgorithmException, CertificateException, NoSuchProviderException, InvalidKeyException, SignatureException { + final DateTime now = DateTime.now(DateTimeZone.UTC); + final BigInteger serial = generateRandomBigInt(); + final X500Principal subject = new X500Principal(subjectDN); + + final X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();; + certGen.setSerialNumber(serial); + certGen.setIssuerDN(caCert.getSubjectX500Principal()); + certGen.setSubjectDN(subject); + certGen.setNotBefore(now.minusHours(12).toDate()); + certGen.setNotAfter(now.plusDays(validityDays).toDate()); + certGen.setPublicKey(clientPublicKey); + certGen.setSignatureAlgorithm(signatureAlgorithm); + certGen.addExtension(X509Extensions.AuthorityKeyIdentifier, false, + new AuthorityKeyIdentifierStructure(caCert)); + certGen.addExtension(X509Extensions.SubjectKeyIdentifier, false, + new SubjectKeyIdentifierStructure(clientPublicKey)); + + final List subjectAlternativeNames = new ArrayList(); + if (publicIPAddresses != null) { + for (final String publicIPAddress: publicIPAddresses) { + if (Strings.isNullOrEmpty(publicIPAddress)) { + continue; + } + subjectAlternativeNames.add(new GeneralName(GeneralName.iPAddress, publicIPAddress)); + } + } + if (dnsNames != null) { + for (final String dnsName : dnsNames) { + if (Strings.isNullOrEmpty(dnsName)) { + continue; + } + subjectAlternativeNames.add(new GeneralName(GeneralName.dNSName, dnsName)); + } + } + if (subjectAlternativeNames.size() > 0) { + final DERSequence subjectAlternativeNamesExtension = new DERSequence( + subjectAlternativeNames.toArray(new ASN1Encodable[subjectAlternativeNames.size()])); + certGen.addExtension(X509Extensions.SubjectAlternativeName, false, + subjectAlternativeNamesExtension); + } + final X509Certificate certificate = certGen.generate(caPrivateKey, "BC"); + certificate.verify(caCert.getPublicKey()); + return certificate; + } +} diff --git a/utils/src/org/apache/cloudstack/utils/security/KeyStoreUtils.java b/utils/src/org/apache/cloudstack/utils/security/KeyStoreUtils.java new file mode 100644 index 00000000000..8690d39f24a --- /dev/null +++ b/utils/src/org/apache/cloudstack/utils/security/KeyStoreUtils.java @@ -0,0 +1,70 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.utils.security; + +import java.io.File; +import java.io.IOException; + +import com.cloud.utils.script.Script; +import com.google.common.base.Strings; + +public class KeyStoreUtils { + + public static String defaultTmpKeyStoreFile = "/tmp/tmp.jks"; + public static String defaultKeystoreFile = "/cloud.jks"; + public static String defaultPrivateKeyFile = "/cloud.key"; + public static String defaultCsrFile = "/cloud.csr"; + public static String defaultCertFile = "/cloud.crt"; + public static String defaultCaCertFile = "/cloud.ca.crt"; + public static char[] defaultKeystorePassphrase = "vmops.com".toCharArray(); + + public static String certNewlineEncoder = "^"; + public static String certSpaceEncoder = "~"; + + public static String keyStoreSetupScript = "keystore-setup"; + public static String keyStoreImportScript = "keystore-cert-import"; + public static String passphrasePropertyName = "keystore.passphrase"; + + public static String sshMode = "ssh"; + public static String agentMode = "agent"; + + public static void copyKeystore(final String keystorePath, final String tmpKeystorePath) throws IOException { + if (Strings.isNullOrEmpty(keystorePath) || Strings.isNullOrEmpty(tmpKeystorePath)) { + throw new IOException("Invalid keystore path provided"); + } + try { + final Script script = new Script(true, "cp", 5000, null); + script.add("-f"); + script.add(tmpKeystorePath); + script.add(keystorePath); + final String result = script.execute(); + if (result != null) { + throw new IOException("Failed to execute cp to copy keystore file to mgmt server conf location"); + } + } catch (final Exception e) { + throw new IOException("Failed to create keystore file: " + keystorePath, e); + } + try { + new File(tmpKeystorePath).delete(); + } catch (Exception ignored) { + } + } + +} diff --git a/utils/test/com/cloud/utils/StringUtilsTest.java b/utils/test/com/cloud/utils/StringUtilsTest.java index 5a90300e654..37dbad60c35 100644 --- a/utils/test/com/cloud/utils/StringUtilsTest.java +++ b/utils/test/com/cloud/utils/StringUtilsTest.java @@ -230,4 +230,15 @@ public class StringUtilsTest { Assert.assertEquals("a,b,c", StringUtils.listToCsvTags(Arrays.asList("a","b", "c"))); Assert.assertEquals("", StringUtils.listToCsvTags(new ArrayList())); } + + @Test + public void testShuffleCSVList() { + String input = "one,two,three,four,five,six,seven,eight,nine,ten"; + String output = StringUtils.shuffleCSVList(input); + Assert.assertFalse(input.equals(output)); + + input = "only-one"; + output = StringUtils.shuffleCSVList("only-one"); + Assert.assertTrue(input.equals(output)); + } } diff --git a/utils/test/com/cloud/utils/testcase/NioTest.java b/utils/test/com/cloud/utils/testcase/NioTest.java index 515119d9e7e..84e0c5586d9 100644 --- a/utils/test/com/cloud/utils/testcase/NioTest.java +++ b/utils/test/com/cloud/utils/testcase/NioTest.java @@ -97,7 +97,7 @@ public class NioTest { testBytes = new byte[1000000]; randomGenerator.nextBytes(testBytes); - server = new NioServer("NioTestServer", 0, 1, new NioTestServer()); + server = new NioServer("NioTestServer", 0, 1, new NioTestServer(), null); try { server.start(); } catch (Exception e) { diff --git a/utils/test/org/apache/cloudstack/utils/security/CertUtilsTest.java b/utils/test/org/apache/cloudstack/utils/security/CertUtilsTest.java new file mode 100644 index 00000000000..de0e89ee536 --- /dev/null +++ b/utils/test/org/apache/cloudstack/utils/security/CertUtilsTest.java @@ -0,0 +1,118 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.utils.security; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; +import java.util.List; + +import org.bouncycastle.asn1.x509.GeneralName; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class CertUtilsTest { + KeyPair caKeyPair; + X509Certificate caCertificate; + + @Before + public void setUp() throws Exception { + caKeyPair = CertUtils.generateRandomKeyPair(1024); + caCertificate = CertUtils.generateV1Certificate(caKeyPair, "CN=test", "CN=test", 1, "SHA256WithRSAEncryption"); + } + + @Test + public void testGenerateRandomKeyPair() throws Exception { + final int size = 2048; + final KeyPair kp = CertUtils.generateRandomKeyPair(size); + Assert.assertEquals(((RSAPublicKey)kp.getPublic()).getModulus().bitLength(), size); + } + + @Test + public void testCertificateConversionMethods() throws Exception { + final X509Certificate in = caCertificate; + final String pem = CertUtils.x509CertificateToPem(in); + final X509Certificate out = CertUtils.pemToX509Certificate(pem); + Assert.assertTrue(pem.startsWith("-----BEGIN CERTIFICATE-----\n")); + Assert.assertTrue(pem.endsWith("-----END CERTIFICATE-----\n")); + Assert.assertEquals(in.getSerialNumber(), out.getSerialNumber()); + Assert.assertArrayEquals(in.getSignature(), out.getSignature()); + Assert.assertEquals(in.getSigAlgName(), out.getSigAlgName()); + Assert.assertEquals(in.getPublicKey(), out.getPublicKey()); + Assert.assertEquals(in.getNotBefore(), out.getNotBefore()); + Assert.assertEquals(in.getNotAfter(), out.getNotAfter()); + Assert.assertEquals(in.getIssuerDN().toString(), out.getIssuerDN().toString()); + } + + @Test + public void testKeysConversionMethods() throws Exception { + final KeyPair kp = CertUtils.generateRandomKeyPair(2048); + + final PrivateKey inPrivateKey = kp.getPrivate(); + final PrivateKey outPrivateKey = CertUtils.pemToPrivateKey(CertUtils.privateKeyToPem(inPrivateKey)); + Assert.assertEquals(inPrivateKey.getAlgorithm(), outPrivateKey.getAlgorithm()); + Assert.assertEquals(inPrivateKey.getFormat(), outPrivateKey.getFormat()); + Assert.assertArrayEquals(inPrivateKey.getEncoded(), outPrivateKey.getEncoded()); + + final PublicKey inPublicKey = kp.getPublic(); + final PublicKey outPublicKey = CertUtils.pemToPublicKey(CertUtils.publicKeyToPem(inPublicKey)); + Assert.assertEquals(inPublicKey.getAlgorithm(), outPublicKey.getAlgorithm()); + Assert.assertEquals(inPublicKey.getFormat(), inPublicKey.getFormat()); + Assert.assertArrayEquals(inPublicKey.getEncoded(), outPublicKey.getEncoded()); + } + + @Test + public void testGenerateRandomBigInt() throws Exception { + Assert.assertNotEquals(CertUtils.generateRandomBigInt(), CertUtils.generateRandomBigInt()); + } + + @Test + public void testGenerateCertificate() throws Exception { + final KeyPair clientKeyPair = CertUtils.generateRandomKeyPair(1024); + final List domainNames = Arrays.asList("domain1.com", "www.2.domain2.com", "3.domain3.com"); + final List addressList = Arrays.asList("1.2.3.4", "192.168.1.1", "2a02:120b:2c16:f6d0:d9df:8ebc:e44a:f181"); + + final X509Certificate clientCert = CertUtils.generateV3Certificate(caCertificate, caKeyPair.getPrivate(), clientKeyPair.getPublic(), + "CN=domain.example", "SHA256WithRSAEncryption", 10, domainNames, addressList); + + clientCert.verify(caKeyPair.getPublic()); + Assert.assertEquals(clientCert.getIssuerDN(), caCertificate.getIssuerDN()); + Assert.assertEquals(clientCert.getSigAlgName(), "SHA256WithRSAEncryption"); + Assert.assertArrayEquals(clientCert.getPublicKey().getEncoded(), clientKeyPair.getPublic().getEncoded()); + Assert.assertNotNull(clientCert.getSubjectAlternativeNames()); + + for (final List altNames : clientCert.getSubjectAlternativeNames()) { + Assert.assertTrue(altNames.size() == 2); + final Object first = altNames.get(0); + final Object second = altNames.get(1); + if (first instanceof Integer && ((Integer) first) == GeneralName.iPAddress) { + Assert.assertTrue(addressList.contains((String) second)); + } + if (first instanceof Integer && ((Integer) first) == GeneralName.dNSName) { + Assert.assertTrue(domainNames.contains((String) second)); + } + } + } + +} \ No newline at end of file