/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.enterprise.server.core; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.Query; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.clientapi.server.core.AgentNotSupportedException; import org.rhq.core.clientapi.server.core.AgentRegistrationException; import org.rhq.core.clientapi.server.core.AgentRegistrationRequest; import org.rhq.core.clientapi.server.core.AgentRegistrationResults; import org.rhq.core.clientapi.server.core.AgentVersion; import org.rhq.core.clientapi.server.core.ConnectAgentRequest; import org.rhq.core.clientapi.server.core.ConnectAgentResults; import org.rhq.core.clientapi.server.core.CoreServerService; import org.rhq.core.clientapi.server.core.PingRequest; import org.rhq.core.domain.cloud.PartitionEventType; import org.rhq.core.domain.cloud.Server; import org.rhq.core.domain.cloud.composite.FailoverListComposite; import org.rhq.core.domain.install.remote.AgentInstall; import org.rhq.core.domain.plugin.Plugin; import org.rhq.core.domain.resource.Agent; import org.rhq.core.util.Base64; import org.rhq.core.util.exception.WrappedRemotingException; import org.rhq.enterprise.communications.ServiceContainerConfiguration; import org.rhq.enterprise.communications.command.client.RemoteInputStream; import org.rhq.enterprise.communications.util.SecurityUtil; import org.rhq.enterprise.server.alert.engine.AlertConditionCacheManagerLocal; import org.rhq.enterprise.server.auth.SubjectManagerLocal; import org.rhq.enterprise.server.cloud.FailoverListManagerLocal; import org.rhq.enterprise.server.cloud.PartitionEventManagerLocal; import org.rhq.enterprise.server.cloud.instance.ServerManagerLocal; import org.rhq.enterprise.server.core.comm.ServerCommunicationsServiceMBean; import org.rhq.enterprise.server.core.comm.ServerCommunicationsServiceUtil; import org.rhq.enterprise.server.util.LookupUtil; /** * The service that receives agent requests to perform functionality such as registering agents. * * @author John Mazzitelli */ public class CoreServerServiceImpl implements CoreServerService { private final static String PROP_WEB_HTTP_PORT = "rhq.server.socket.binding.port.http"; private final static String PROP_WEB_HTTPS_PORT = "rhq.server.socket.binding.port.https"; private final Log log = LogFactory.getLog(CoreServerServiceImpl.class); private AgentManagerLocal agentManager; private AlertConditionCacheManagerLocal alertConditionCacheManager; private FailoverListManagerLocal failoverListManager; private PartitionEventManagerLocal partitionEventManager; private ServerManagerLocal serverManager; private SubjectManagerLocal subjectManager; private SecureRandom random; /** * @see CoreServerService#registerAgent(AgentRegistrationRequest) */ @Override public AgentRegistrationResults registerAgent(AgentRegistrationRequest request) throws AgentRegistrationException, AgentNotSupportedException { // fail-fast if we can't even support this agent if (!getAgentManager().isAgentVersionSupported(request.getAgentVersion()).isSupported()) { log.warn("Agent [" + request.getName() + "][" + request.getAddress() + ':' + request.getPort() + "][" + request.getAgentVersion() + "] would like to register with this server but it is not supported"); throw new AgentNotSupportedException("Agent [" + request.getName() + "] is an unsupported agent: " + request.getAgentVersion()); } // Make a very quick test to verify the agent's remote endpoint can be connected to. // If not, no point in continuing - the server won't be able to talk to the agent anyway. pingEndpoint(request.getRemoteEndpoint()); Agent agentByName = getAgentManager().getAgentByName(request.getName()); /* * As part of the registration request, a previously registered agent should send its current token (aka the * original token). We will check this original token in the database and see what agent name is associated with * it. If it is different than the name of the agent that is asking for this registration, we abort. In effect, * we are telling the agent "you are already registered under the name "A" - you cannot change your name to * something else". * * If there is no original token with the request, this is either a brand new agent never before registered, or it * is an agent that has been registered before but for some reason lost its token. * In this case, if there is no agent with the name being requested, we register this as a new agent. * If, however, the agent name is already in use, we abort the request. An agent cannot register with an * existing agent without sending that agent's security token. */ if (request.getOriginalToken() != null) { Agent agentByToken = getAgentManager().getAgentByAgentToken(request.getOriginalToken()); if (agentByToken != null) { if (!agentByToken.getName().equals(request.getName())) { String msg = "The agent asking for registration is already registered with the name [" + agentByToken.getName() + "], it cannot change its name to [" + request.getName() + "]"; throw new AgentRegistrationException(msg); } else { Agent agentByAddressPort = getAgentManager().getAgentByAddressAndPort(request.getAddress(), request.getPort()); if (agentByAddressPort != null && !agentByAddressPort.getName().equals(request.getName())) { // the agent request provided information about an authentic agent but it is trying to // steal another agent's host/port. Thus, we will abort this request. String msg = "The agent asking for registration [" + request.getName() + "] is trying to register the same address/port [" + request.getAddress() + ":" + request.getPort() + "] that is already registered under a different name [" + agentByAddressPort.getName() + "]"; throw new AgentRegistrationException(msg); } } } else { if (agentByName != null) { // the agent request provided a name that already is in use by an agent. However, the request // provided a security token that was not assigned to any agent! How can this be? Something is fishy. String msg = "The agent asking for registration under the name [" + request.getName() + "] provided an invalid security token. This request will fail. " + securityTokenMessage(); throw new AgentRegistrationException(msg); } Agent agentByAddressPort = getAgentManager().getAgentByAddressAndPort(request.getAddress(), request.getPort()); if (agentByAddressPort != null) { // The agent is requesting to register an unused agent name - so this is considered a new agent. // It provided a security token but it is an unknown/obsolete/bogus token (usually due to the // fact that someone purged the platform/agent from the server database but the old agent is // still around with its old token). // However, the IP/port it wants to use is already in-use. This sounds fishy. If we let this // go through, this agent with an unknown/bogus token will essentially hijack this IP/port // belonging to an existing agent. If the agent wants to reuse an IP/port already in existence, it should // already know its security token associated with that IP/port. Thus, we will abort this request. String msg = "The agent asking for registration under the name [" + request.getName() + "] is attempting to take another agent's address/port [" + request.getAddress() + ":" + request.getPort() + "] with an unknown security token. This request will fail."; throw new AgentRegistrationException(msg); } } } else { Agent agentByAddressPort = getAgentManager().getAgentByAddressAndPort(request.getAddress(), request.getPort()); if (agentByAddressPort != null) { if (!agentByAddressPort.getName().equals(request.getName())) { String msg = "The agent asking for registration is trying to register the same address/port [" + request.getAddress() + ":" + request.getPort() + "] that is already registered under a different name [" + agentByAddressPort.getName() + "]. If this new agent is actually the same as the original, then re-register with the same name" + " and same security token. Otherwise, you should uninventory the Platform resource for agent [" + agentByAddressPort.getName() + "] and re-register this new agent."; throw new AgentRegistrationException(msg); } else { String msg = "The agent [" + request.getName() + "] is attempting to re-register without a security token. " + securityTokenMessage(); throw new AgentRegistrationException(msg); } } else { if (agentByName != null) { // the name being registered already exists - but there is no security token to authenticate this request! // Therefore, because this agent name is already registered and because this current request // cannot authenticate itself with the proper security token, we fail. String msg = "An agent is trying to register with an existing agent name [" + request.getName() + "] without providing a valid security token. If you are attempting to re-register this agent, " + "you will need the agent's security token. " + securityTokenMessage(); throw new AgentRegistrationException(msg); } } } Server registeringServer = getServerManager().getServer(); // if the agent is registering with a loopback address, log a warning since you probably do not want this in production try { String address = request.getAddress(); String fullEndpoint = request.getRemoteEndpoint(); boolean loopbackRegistration = false; if (address.equals("127.0.0.1") || address.equalsIgnoreCase("localhost")) { loopbackRegistration = true; } else { // jboss/remoting transport params might be telling us to have our client connect to a different address if (fullEndpoint != null) { if (fullEndpoint.contains("127.0.0.1") || fullEndpoint.contains("localhost")) { loopbackRegistration = true; } } } if (loopbackRegistration) { log.warn("An agent [" + request.getName() + "] has registered with a loopback address. This should only be done " + "for testing or demo purposes - this agent can only ever interact with this server. " + request); } } catch (Exception ignore) { } if (agentByName != null) { log.info("Got agent registration request for existing agent: " + agentByName.getName() + "[" + agentByName.getAddress() + ":" + agentByName.getPort() + "][" + request.getAgentVersion() + "] - Will " + (request.getRegenerateToken() ? "" : "not") + " regenerate a new token"); final String oldAddress = agentByName.getAddress(); final int oldPort = agentByName.getPort(); final String oldRemoteEndpoint = agentByName.getRemoteEndpoint(); agentByName.setServer(registeringServer); agentByName.setAddress(request.getAddress()); agentByName.setPort(request.getPort()); agentByName.setRemoteEndpoint(request.getRemoteEndpoint()); if (request.getRegenerateToken()) { agentByName.setAgentToken(generateAgentToken()); } try { agentManager.updateAgent(agentByName); } catch (Exception e) { log.warn("Could not update the agent in database", e); throw new AgentRegistrationException(new WrappedRemotingException(e)); } // if agent is re-registering in order to change its remote endpoint, destroy our old client. if (!oldAddress.equals(request.getAddress()) || oldPort != request.getPort() || !oldRemoteEndpoint.equals(request.getRemoteEndpoint())) { try { final Agent oldAgent = new Agent(); oldAgent.setName(agentByName.getName()); oldAgent.setAddress(oldAddress); oldAgent.setPort(oldPort); oldAgent.setRemoteEndpoint(oldRemoteEndpoint); agentManager.destroyAgentClient(oldAgent); } catch (Exception e) { log.warn("Could not destroy the agent client - will continue but agent comm may be broken", e); } } } else { log.info("Got agent registration request for new agent: " + request.getName() + "[" + request.getAddress() + ":" + request.getPort() + "][" + request.getAgentVersion() + "]"); // the agent does not yet exist, we need to create it try { agentByName = new Agent(request.getName(), request.getAddress(), request.getPort(), request.getRemoteEndpoint(), generateAgentToken()); agentByName.setServer(registeringServer); agentManager.createAgent(agentByName); } catch (Exception e) { log.warn("Failed to create agent in database", e); throw new AgentRegistrationException(new WrappedRemotingException(e)); } } // get existing or generate new server list for the registering agent FailoverListComposite failoverList = getPartitionEventManager().agentPartitionEvent( getSubjectManager().getOverlord(), agentByName.getName(), PartitionEventType.AGENT_REGISTRATION, agentByName.getName() + " - " + registeringServer.getName()); AgentRegistrationResults results = new AgentRegistrationResults(); results.setAgentToken(agentByName.getAgentToken()); results.setFailoverList(failoverList); // Link this agent with its installation details. // If the agent was told about its install ID, then we'll use that to update the existing row. // (this happens if a remote install was performed and its SSH details were already persisted). // Otherwise, this will create or update an existing AgentInstall entity without any additional data // known about it other than its agent name and install location. // Note that any failures in here won't abort the registration - this isn't required to have a functioning // agent. Its just additional information that is useful to the user for doing things like remote start/stop. try { AgentInstall ai = new AgentInstall(); if (request.getInstallId() != null) { ai.setId(Integer.valueOf(request.getInstallId())); } ai.setAgentName(agentByName.getName()); ai.setInstallLocation(request.getInstallLocation()); ai = agentManager.updateAgentInstall(subjectManager.getOverlord(), ai); // We now have the persisted AgentInstall entity from the database - which may have additional information we didn't have before. // If, however, we still don't have the hostname, fill that in now with the address of the agent entity. // We do this now (rather than when we first updated above) because its possible the user, when remotely installing this agent, // provided a different host IP to connect over SSH to (probably for NAT reasons) and that was persisted before the agent was registered. // Therefore, we want to keep the user's host and not overwrite it. If, however, there is no host information at all, we will fill it in. if (ai.getSshHost() == null) { ai.setSshHost(agentByName.getAddress()); ai = agentManager.updateAgentInstall(subjectManager.getOverlord(), ai); } } catch (Exception e) { log.warn("Could not update the install information for agent [" + agentByName.getName() + "]", e); } return results; } private String securityTokenMessage() { return "Please consult an administrator to obtain the agent's proper security token " + "and restart the agent with the option \"-Drhq.agent.security-token=<the valid security token>\". " + "An administrator can find the agent's security token by navigating to the GUI page " + "\"Administration (Topology) > Agents\" and drilling down to this specific agent. " + "You will see the long security token string there. For more information, read: " + "https://docs.jboss.org/author/display/RHQ/Agent+Registration"; } /** * @see CoreServerService#connectAgent(ConnectAgentRequest) */ @Override public ConnectAgentResults connectAgent(ConnectAgentRequest request) throws AgentRegistrationException, AgentNotSupportedException { String agentName = request.getAgentName(); AgentVersion agentVersion = request.getAgentVersion(); log.info("Agent [" + agentName + "][" + agentVersion + "] would like to connect to this server"); AgentVersionCheckResults agentVersionCheckResults = getAgentManager().isAgentVersionSupported(agentVersion); if (!agentVersionCheckResults.isSupported()) { log.warn("Agent [" + agentName + "][" + agentVersion + "] would like to connect to this server but it is not supported"); throw new AgentNotSupportedException("Agent [" + agentName + "] is an unsupported agent: " + agentVersion); } Agent agent = getAgentManager().getAgentByName(agentName); if (agent == null) { throw new AgentRegistrationException("Agent [" + agentName + "] is not registered"); } Server server = getServerManager().getServer(); agent.setServer(server); agent.setLastAvailabilityPing(Long.valueOf(System.currentTimeMillis())); getAgentManager().updateAgent(agent); getAlertConditionCacheManager().reloadCachesForAgent(agent.getId()); getPartitionEventManager().auditPartitionEvent(getSubjectManager().getOverlord(), PartitionEventType.AGENT_CONNECT, agentName + " - " + server.getName()); log.info("Agent [" + agentName + "] has connected to this server at " + new Date()); return new ConnectAgentResults(System.currentTimeMillis(), agent.isBackFilled(), agentVersionCheckResults.getLatestAgentVersion()); } /** * @see CoreServerService#getLatestPlugins() */ @SuppressWarnings("unchecked") @Override public List<Plugin> getLatestPlugins() { EntityManager em = null; List<Plugin> plugins = new ArrayList<Plugin>(); try { em = LookupUtil.getEntityManager(); Query q = em.createNamedQuery(Plugin.QUERY_FIND_ALL_INSTALLED); List<Plugin> resultList = q.getResultList(); for (Plugin potentialPlugin : resultList) { if (potentialPlugin.isEnabled()) { plugins.add(potentialPlugin); } } } catch (Exception e) { log.warn("Failed to get the list of latest plugins", e); throw new WrappedRemotingException(e); } finally { if (em != null) { em.close(); } } return plugins; } /** * @see CoreServerService#getPluginArchive(String) */ @Override public InputStream getPluginArchive(String pluginName) { EntityManager em = null; try { em = LookupUtil.getEntityManager(); Query q = em.createNamedQuery(Plugin.QUERY_FIND_BY_NAME); q.setParameter("name", pluginName); Plugin plugin = (Plugin) q.getSingleResult(); // we know our plugins are in a subdirectory within our downloads location return getFileContents(new File("rhq-plugins", plugin.getPath()).getPath()); } finally { if (em != null) { em.close(); } } } /** * @see CoreServerService#getFileContents(String) */ @Override public InputStream getFileContents(String file) { // for security purposes, do not let the agent ask for any file outside of the agent-files location if (file.indexOf("..") >= 0) { String err_string = "WARNING: agent is attempting to download a file from an invalid location: " + file; log.error(err_string); throw new IllegalArgumentException(err_string); } try { ServerCommunicationsServiceMBean sc = ServerCommunicationsServiceUtil.getService(); String dir = sc.getConfiguration().getAgentFilesDirectory(); File file_to_stream = new File(dir, file); // Make sure file_to_stream exists - its possible another server deployed this // but our server hasn't had a chance to download it from the database yet. // If this file does not exist, we need to immediately perform an agent scan // which will pull down the plugin file from the database. if (!file_to_stream.exists()) { log.debug("Agent is asking for a plugin that isn't on file system [" + file_to_stream + "] - performing plugin scan"); LookupUtil.getPluginDeploymentScanner().scan(); } FileInputStream fis = new FileInputStream(file_to_stream); BufferedInputStream bis = new BufferedInputStream(fis, 1024 * 32); RemoteInputStream in = ServerCommunicationsServiceUtil.remoteInputStream(bis); return in; } catch (Exception e) { throw new WrappedRemotingException(e); } } /** * @see CoreServerService#agentIsShuttingDown(String) */ @Override public void agentIsShuttingDown(String agentName) { log.debug("Agent [" + agentName + "] is sending a notification that it is going down!"); getAgentManager().agentIsShuttingDown(agentName); getPartitionEventManager().auditPartitionEvent(getSubjectManager().getOverlord(), PartitionEventType.AGENT_SHUTDOWN, agentName); return; } /** * @see CoreServerService#getFailoverList(String) */ @Override public FailoverListComposite getFailoverList(String agentName) { return getFailoverListManager().getExistingForSingleAgent(agentName); } /** * @see CoreServerService#ping(PingRequest) */ @Override public PingRequest ping(PingRequest request) { return getAgentManager().handlePingRequest(request); } private AgentManagerLocal getAgentManager() { if (this.agentManager == null) { this.agentManager = LookupUtil.getAgentManager(); } return this.agentManager; } private AlertConditionCacheManagerLocal getAlertConditionCacheManager() { if (this.alertConditionCacheManager == null) { this.alertConditionCacheManager = LookupUtil.getAlertConditionCacheManager(); } return this.alertConditionCacheManager; } private FailoverListManagerLocal getFailoverListManager() { if (this.failoverListManager == null) { this.failoverListManager = LookupUtil.getFailoverListManager(); } return this.failoverListManager; } private PartitionEventManagerLocal getPartitionEventManager() { if (this.partitionEventManager == null) { this.partitionEventManager = LookupUtil.getPartitionEventManager(); } return this.partitionEventManager; } private ServerManagerLocal getServerManager() { if (this.serverManager == null) { this.serverManager = LookupUtil.getServerManager(); } return this.serverManager; } private SubjectManagerLocal getSubjectManager() { if (this.subjectManager == null) { this.subjectManager = LookupUtil.getSubjectManager(); } return this.subjectManager; } private void pingEndpoint(String endpoint) throws AgentRegistrationException { AgentRegistrationException failure = null; try { ServerCommunicationsServiceMBean sc = ServerCommunicationsServiceUtil.getService(); boolean ping = sc.pingEndpoint(endpoint, 10000L); if (!ping) { // this is mainly to support agentspawn environments but I suppose this could happen // on "real" systems. If the agent's machine is heavily loaded, it is possible that // the agent wasn't given enough time to respond to the ping. Let's at least try // one more time, because once this ping failure is confirmed, it means the agent // will be dead in the water and hang until an admin can reconfigure it (a second // failure probably means it really is a configuration problem) ping = sc.pingEndpoint(endpoint, 20000L); if (!ping) { failure = new AgentRegistrationException("Server cannot ping the agent's endpoint. " + "The agent's endpoint is probably invalid " + "or there is a firewall preventing the server from connecting to the agent. " + "Endpoint: " + endpoint); } } } catch (Exception e) { failure = new AgentRegistrationException("Cannot verify agent endpoint due to internal error.", new WrappedRemotingException(e)); } if (failure != null) { log.warn(failure); throw failure; } log.debug("A new agent has passed its endpoint verification test: " + endpoint); return; } private synchronized String generateAgentToken() { if (random == null) { try { random = SecureRandom.getInstance("SHA1PRNG"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Could not load SecureRandom algorithm", e); } } byte[] tokenBytes = new byte[50]; random.nextBytes(tokenBytes); return Base64.encode(tokenBytes); } /** * @see CoreServerService#getPublicAgentUpdateEndpointAddress() */ @Override public String getPublicAgentUpdateEndpointAddress() { ServiceContainerConfiguration scp = ServerCommunicationsServiceUtil.getService().getConfiguration().getServiceContainerPreferences(); String transport = scp.getConnectorTransport(); String port; if (SecurityUtil.isTransportSecure(transport)) { port = System.getProperty(PROP_WEB_HTTPS_PORT); transport = "https"; } else { port = System.getProperty(PROP_WEB_HTTP_PORT); transport = "http"; } return transport + "://" + scp.getConnectorBindAddress() + ":" + port; } }