/*
* 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.gui.agentupdate;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.domain.cloud.Server.OperationMode;
import org.rhq.core.domain.common.composite.SystemSetting;
import org.rhq.core.domain.common.composite.SystemSettings;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.enterprise.server.core.AgentManagerLocal;
import org.rhq.enterprise.server.util.LookupUtil;
/**
* Serves the agent update binary that is stored in the RHQ Server's download area.
* This servlet also provides version information regarding the version of the agent
* this servlet serves up as well as versions of agents the RHQ Server supports.
*/
@WebServlet(urlPatterns = {"/download", "/version"}, loadOnStartup = 1)
public class AgentUpdateServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// the system property that defines how many concurrent downloads we will allow
private static String SYSPROP_AGENT_DOWNLOADS_LIMIT = "rhq.server.agent-downloads-limit";
// if the system property is not set or invalid, this is the default limit for number of concurrent downloads
private static int DEFAULT_AGENT_DOWNLOADS_LIMIT = 45;
// the error code that will be returned if the server has been configured to disable agent updates
private static final int ERROR_CODE_AGENT_UPDATE_DISABLED = HttpServletResponse.SC_FORBIDDEN;
// the error code that will be returned if the server has too many agents downloading the agent update binary
private static final int ERROR_CODE_TOO_MANY_DOWNLOADS = HttpServletResponse.SC_SERVICE_UNAVAILABLE;
private AtomicInteger numActiveDownloads = null;
private Log log = LogFactory.getLog(this.getClass());
private AgentManagerLocal agentManager = null;
private boolean initialized = false;
@Override
public void init() throws ServletException {
log.info("Starting the RHQ agent update servlet");
numActiveDownloads = new AtomicInteger(0);
}
private synchronized void loadAgentUpdateBinaryInfo() throws ServletException {
if (!initialized) {
log.info("RHQ agent update servlet is looking up binary file information...");
// make sure we have a agent update binary file; log its location
try {
log.info("Agent Update Binary File: " + getAgentUpdateBinaryFile());
} catch (Throwable t) {
log.error("Missing agent update binary file - agents will not be able to update", t);
}
// make sure we create a version file if we have to by getting the version file now
try {
File versionFile = getAgentUpdateVersionFile();
// log the version info - this also makes sure we can read it back in
log.debug(versionFile + ": " + new String(StreamUtil.slurp(new FileInputStream(versionFile))));
} catch (Throwable t) {
log.error("Cannot determine the agent version information - agents will not be able to update.", t);
}
initialized = true;
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// lazily initialize the servlet - we do this because when we started deploying on AS7, our servlets
// init() method was being called before the agent SLSB is ready. So we don't init() this at startup,
// rather, we now init this servlet the first time someone requests the agent update binary file.
loadAgentUpdateBinaryInfo();
// seeing odd browser caching issues, even though we set Last-Modified. so force no caching for now
disableBrowserCache(resp);
String servletPath = req.getServletPath();
if (servletPath != null) {
if (isServerAcceptingRequests()) {
if (servletPath.endsWith("version")) {
getVersion(req, resp);
} else if (servletPath.endsWith("download")) {
try {
numActiveDownloads.incrementAndGet();
getDownload(req, resp);
} finally {
numActiveDownloads.decrementAndGet();
}
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid servlet path [" + servletPath
+ "] - please contact administrator");
}
} else {
sendErrorServerNotAcceptingRequests(resp);
}
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid servlet path - please contact administrator");
}
return;
}
private void getDownload(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
int limit = getDownloadLimit();
if (limit <= 0) {
sendErrorAgentUpdateDisabled(resp);
return;
} else if (limit < numActiveDownloads.get()) {
sendErrorTooManyDownloads(resp);
return;
}
try {
File agentJar = getAgentUpdateBinaryFile();
resp.setContentType("application/octet-stream");
resp.setHeader("Content-Disposition", "attachment; filename=" + agentJar.getName());
resp.setContentLength((int) agentJar.length());
resp.setDateHeader("Last-Modified", agentJar.lastModified());
FileInputStream agentJarStream = new FileInputStream(agentJar);
try {
StreamUtil.copy(agentJarStream, resp.getOutputStream(), false);
} finally {
agentJarStream.close();
}
} catch (Throwable t) {
String clientAddr = getClientAddress(req);
log.error("Failed to stream agent jar to remote client [" + clientAddr + "]: "
+ ThrowableUtil.getAllMessages(t));
disableBrowserCache(resp);
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to stream agent jar");
}
return;
}
private void getVersion(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
File versionFile = getAgentUpdateVersionFile();
resp.setContentType("text/plain");
resp.setDateHeader("Last-Modified", versionFile.lastModified());
FileInputStream stream = new FileInputStream(versionFile);
byte[] versionData = StreamUtil.slurp(stream);
resp.getOutputStream().write(versionData);
} catch (Throwable t) {
String clientAddr = getClientAddress(req);
log.error("Failed to stream version info to remote client [" + clientAddr + "]: "
+ ThrowableUtil.getAllMessages(t));
disableBrowserCache(resp);
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to stream version info");
}
return;
}
private int getDownloadLimit() {
// if the server cloud was configured to disallow updates, return 0
SystemSettings systemConfig = LookupUtil.getSystemManager().getUnmaskedSystemSettings(true);
if (!Boolean.parseBoolean(systemConfig.get(SystemSetting.AGENT_AUTO_UPDATE_ENABLED))) {
return 0;
}
String limitStr = System.getProperty(SYSPROP_AGENT_DOWNLOADS_LIMIT);
int limit;
try {
limit = Integer.parseInt(limitStr);
} catch (Exception e) {
limit = DEFAULT_AGENT_DOWNLOADS_LIMIT;
log.warn("Agent downloads limit system property [" + SYSPROP_AGENT_DOWNLOADS_LIMIT
+ "] is either not set or invalid [" + limitStr + "] - limit will be [" + limit + "].");
}
return limit;
}
private void disableBrowserCache(HttpServletResponse resp) {
resp.setHeader("Cache-Control", "no-cache, no-store");
resp.setHeader("Expires", "-1");
resp.setHeader("Pragma", "no-cache");
}
private void sendErrorServerNotAcceptingRequests(HttpServletResponse resp) throws IOException {
disableBrowserCache(resp);
resp.sendError(ERROR_CODE_AGENT_UPDATE_DISABLED, "Server Is Down For Maintenance");
}
private void sendErrorAgentUpdateDisabled(HttpServletResponse resp) throws IOException {
disableBrowserCache(resp);
resp.sendError(ERROR_CODE_AGENT_UPDATE_DISABLED, "Agent Updates Has Been Disabled");
}
private void sendErrorTooManyDownloads(HttpServletResponse resp) throws IOException {
disableBrowserCache(resp);
resp.setHeader("Retry-After", "30");
resp.sendError(ERROR_CODE_TOO_MANY_DOWNLOADS, "Maximum limit exceeded - download agent later");
}
private File getAgentUpdateVersionFile() throws Exception {
return getAgentManager().getAgentUpdateVersionFile();
}
private File getAgentUpdateBinaryFile() throws Exception {
return getAgentManager().getAgentUpdateBinaryFile();
}
private AgentManagerLocal getAgentManager() {
if (this.agentManager == null) {
this.agentManager = LookupUtil.getAgentManager();
}
return this.agentManager;
}
private boolean isServerAcceptingRequests() {
try {
OperationMode mode = LookupUtil.getServerManager().getServer().getOperationMode();
return mode == OperationMode.NORMAL;
} catch (Exception e) {
return false;
}
}
private String getClientAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = String.format("%s (%s)", request.getRemoteHost(), request.getRemoteAddr());
}
}
return ip;
}
}