package com.webobjects.monitor.application;
import java.util.Enumeration;
import com.webobjects.appserver.WOActionResults;
import com.webobjects.appserver.WOComponent;
import com.webobjects.appserver.WODirectAction;
import com.webobjects.appserver.WORequest;
import com.webobjects.appserver.WOResponse;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSSet;
import com.webobjects.monitor._private.MApplication;
import com.webobjects.monitor._private.MInstance;
import com.webobjects.monitor._private.MObject;
import com.webobjects.monitor._private.MSiteConfig;
import er.extensions.appserver.ERXHttpStatusCodes;
import er.extensions.appserver.ERXResponse;
/**
* <p>
* The following direct actions were added to Monitor. They might be useful for
* creating scripts to automate deployments of new WO application versions.
* (First time deployments and config changes would still require interactive
* sessions in Monitor.) Each direct action returns a short string (instead of a
* full HTML page) and an HTTP status code indicating whether the respective
* action was executed successfully. If Monitor is password-protected, the
* password must be passed on the URL with the name "pw", (e.g. &pw=foo). If the
* password is missing or incorrect, these direct actions are not permitted to be
* executed.
* <table cellspacing="0" cellpadding="5" border="1">
* <tr>
* <th>Direct Action</th>
* <th>Return Values</th>
* <th>Description</th>
* </tr>
* <tr>
* <td>running</td>
* <td rowspan="2">'YES', 'NO', or<br>
* error message</td>
* <td>checks whether instances are running (alive)</td>
* </tr>
* <tr>
* <td>stopped</td>
* <td>checks whether instances have stopped (are dead)</td>
* </tr>
* <tr>
* <td colspan="3"></td>
* </tr>
* <tr>
* <td>start</td>
* <td rowspan="3">'OK' or <br>
* error message</td>
* <td>attempts to start instances which have been stopped or are stopping</td>
* </tr>
* <tr>
* <td>stop</td>
* <td>attempts to stops instances which are running or starting</td>
* </tr>
* <tr>
* <td>forceQuit</td>
* <td>stops instances forcefully</td>
* </tr>
* <tr>
* <td colspan="3"></td>
* </tr>
* <tr>
* <td>turnAutoRecoverOn</td>
* <td rowspan="2">'OK' or <br>
* error message</td>
* <td>turns Auto Recover on</td>
* </tr>
* <tr>
* <td>turnAutoRecoverOff</td>
* <td>turns Auto Recover off</td>
* </tr>
* <tr>
* <td colspan="3"></td>
* </tr>
* <tr>
* <td>turnRefuseNewSessionsOn</td>
* <td rowspan="2">'OK' or <br>
* error message</td>
* <td>turns Refuse New Sessions on</td>
* </tr>
* <tr>
* <td>turnRefuseNewSessionsOff</td>
* <td>turns Refuse New Sessions off</td>
* </tr>
* <tr>
* <td colspan="3"></td>
* </tr>
* <tr>
* <td>turnScheduledOn</td>
* <td rowspan="2">'OK' or <br>
* error message</td>
* <td>turns Scheduled on</td>
* </tr>
* <tr>
* <td>turnScheduledOff</td>
* <td>turns Scheduled off</td>
* </tr>
* <tr>
* <td colspan="3"></td>
* </tr>
* <tr>
* <td>clearDeaths</td>
* <td>'OK' or <br>
* error message</td>
* <td>sets the number of deaths to 0</td>
* </tr>
* <tr>
* <td>bounce</td>
* <td>'OK' or <br>
* error message</td>
* <td>bounces the application (starts a few instances per hosts, set the rest to refusing sessions and auto-recover)</td>
* </tr>
* <tr>
* <td>info</td>
* <td>JSON or<br>
* error message</td>
* <td>returns a JSON encoded list of instances with all the data from the app detail page. Add form value info=full to also return the Additional Arguments.</td>
* </tr>
* </table>
* </p>
* <p>
* All direct actions must be invoked with a type:
* <table cellspacing="0" cellpadding="5" border="1">
* <tr>
* <th>Type</th>
* <th>Description</th>
* <th>Requires Names</th>
* </tr>
* <tr>
* <td>all</td>
* <td>all instances of all applications</td>
* <td>no</td>
* </tr>
* <tr>
* <td>app</td>
* <td>all instances of the specified applications</td>
* <td rowspan="2">yes</td>
* </tr>
* <tr>
* <td>ins</td>
* <td>all the specified instances</td>
* </tr>
* </table>
* </p>
* <p>
* The direct action 'running' can be invoked with a num argument:
* <table cellspacing="0" cellpadding="5" border="1">
* <tr>
* <th>Num</th>
* <th>Description</th>
* </tr>
* <tr>
* <td>all / -1</td>
* <td>all instances of the application must be running. this is the default if no num argument is set</td>
* </tr>
* <tr>
* <td><i>number</i></td>
* <td>a minimum of <i>number</i> instances of the specified application must be running. if there are less instances configured acts like 'all'</td>
* </tr>
* </table>
* </p>
* <p>
* The direct action 'bounce' can be invoked with additional arguments:
* <table cellspacing="0" cellpadding="5" border="1">
* <tr>
* <th>Argument</th>
* <th>Value</th>
* <th>Description</th>
* </tr>
* <tr>
* <td>bouncetype</td>
* <td>graceful | shutdown | rolling</td>
* <td>graceful bounces the application by starting a few instances per host and setting the rest to refusing sessions<br />
* shutdown bounces the application by stopping all instances and then restarting them (use this if your<br />
* application will migrate the database so the old application will crash)<br />
* rolling will start a few instances per host, then forcefully restart the existing instances one at a time<br/>
* The default bouncetype is graceful.</td>
* </tr>
* <tr>
* <td>maxwait</td>
* <td><i>secs</i></td>
* <td>number of seconds to wait for applications to shut down themselves before force quitting the instances.<br />
* The default is 30 seconds.</td>
* </tr>
* </table>
* </p>
* <p>
* Possible status codes:
* <table cellspacing="0" cellpadding="5" border="1">
* <tr>
* <th>Code</th>
* <th>Circumstance</th>
* </tr>
* <tr>
* <td>200 (OK)</td>
* <td>return value is 'OK' or 'YES'</td>
* </tr>
* <tr>
* <td>403 (Unauthorized)</td>
* <td>Monitor is password protected</td>
* </tr>
* <tr>
* <td>404 (Not Found)</td>
* <td>one or more of the supplied application or instance names can't be found</td>
* </tr>
* <tr>
* <td>406 (Not Acceptable)</td>
* <td>an unknown type is supplied, or names are required but missing</td>
* </tr>
* <tr>
* <td>417 (Not Expected)</td>
* <td>return value is 'NO'</td>
* </tr>
* <tr>
* <td>500 (Error)</td>
* <td>software defect (please <A HREF="mailto:christian@pekeler.org">send</A>
* stacktrace from Monitor's log)</td>
* </tr>
* </table>
* </p>
* <p>
* Examples:
* <table cellspacing="0" cellpadding="5" border="1">
* <tr>
* <th>URL</th>
* <th>Description</th>
* </tr>
* <tr>
* <td>
* .../JavaMonitor.woa/admin/start?type=app&name=AppleStore&name=MemberSite
* </td>
* <td>Starts all instances of the AppleStore and the MemberSite applications.
* Returns error if any of these applications are unknown to Monitor, OK
* otherwise.</td>
* </tr>
* <tr>
* <td>.../JavaMonitor.woa/admin/turnScheduledOff?type=all</td>
* <td>Turns scheduling off for all instances of all applications, then returns
* OK.</td>
* </tr>
* <tr>
* <td>
* .../JavaMonitor.woa/admin/stopped?type=ins&name=AppleStore-4&name=
* MemberSite-8&name=AppleStore-2</td>
* <td>Returns YES if the instances 2 and 4 of the AppleStore and instance 8 of
* the MemberSite are all dead. Returns NO if at least one of them has not
* stopped. Returns error if any of these instances are unknown to Monitor.</td>
* </tr>
* </table>
* </p>
* <p>
* A simple deployment script could look as follows:
* <table cellspacing="0" cellpadding="5" border="1">
* <tr>
* <td><tt>#!/bin/sh<br>
<br>
# clean build<br>
ant clean install <br>
<br>
# run unit tests<br>
ant test <br>
<br>
# stop application<br>
result=`curl -s
http://bigserver:1086/cgi-bin/WebObjects/JavaMonitor.woa/admin/stop\?type=app\&name=MemberSite`<br>
[ "$result" = OK ] || { echo $result; exit 1; }<br>
<br>
# deploy new application<br>
scp -rq /Library/WebObjects/Applications/MemberSite.woa
bigserver:/Library/WebObjects/Applications/<br>
<br>
# start application<br>
result=`curl -s
http://bigserver:1086/cgi-bin/WebObjects/JavaMonitor.woa/admin/start\?type=app\&name=MemberSite`<br>
[ "$result" = OK ] || { echo $result; exit 1; }<br>
<br>
echo "deployment completed"</tt><br>
* </td>
* </tr>
* </table>
* </p>
* <p>
* Invoking direct actions manually:
* <table cellspacing="0" cellpadding="5" border="1">
* <tr>
* <td><tt>curl -w " (status: %{http_code})\n"
http://bigserver:1086/cgi-bin/WebObjects/JavaMonitor.woa/admin/forceQuit\?type=ins\&name=AppleStore-3
* </td>
* </tr>
* </table>
*
* @author christian@pekeler.org
* @author ak
*
*/
public class AdminAction extends WODirectAction {
public class DirectActionException extends RuntimeException {
public int status;
public DirectActionException(String s, int i) {
super(s);
status = i;
}
}
protected static NSArray supportedActionNames = new NSArray(new String[] { "running", "bounce", "stopped", "start", "stop", "forceQuit", "turnAutoRecoverOn", "turnAutoRecoverOff",
"turnRefuseNewSessionsOn", "turnRefuseNewSessionsOff", "turnScheduledOn", "turnScheduledOff", "turnAutoRecoverOn", "turnAutoRecoverOff", "clearDeaths", "info" });
protected AdminApplicationsPage applicationsPage;
protected NSMutableArray instances;
protected NSMutableArray applications;
private WOTaskdHandler _handler;
public AdminAction(WORequest worequest) {
super(worequest);
instances = new NSMutableArray();
applications = new NSMutableArray();
_handler = new WOTaskdHandler(mySession());
}
public WOComponent MainAction() {
return pageWithName("Main");
}
protected AdminApplicationsPage applicationsPage() {
if (applicationsPage == null)
applicationsPage = new AdminApplicationsPage(context());
return applicationsPage;
}
public WOActionResults infoAction() {
ERXResponse woresponse = new ERXResponse();
String result = "";
for (Enumeration enumeration = instances.objectEnumerator(); enumeration.hasMoreElements();) {
MInstance minstance = (MInstance) enumeration.nextElement();
result += (result.length() == 0 ? "" : ", \n");
result += "{";
result += "\"name\": \"" + minstance.applicationName() + "\", ";
result += "\"id\": \"" + minstance.id() + "\", ";
result += "\"host\": \"" + minstance.hostName() + "\", ";
result += "\"port\": \"" + minstance.port() + "\", ";
result += "\"state\": \"" + MObject.stateArray[minstance.state] + "\", ";
result += "\"deaths\": \"" + minstance.deathCount() + "\", ";
result += "\"refusingNewSessions\": " + minstance.isRefusingNewSessions() + ", ";
result += "\"scheduled\": " + minstance.isScheduled() + ", ";
result += "\"schedulingHourlyStartTime\": " + minstance.schedulingHourlyStartTime() + ", ";
result += "\"schedulingDailyStartTime\": " + minstance.schedulingDailyStartTime() + ", ";
result += "\"schedulingWeeklyStartTime\": " + minstance.schedulingWeeklyStartTime() + ", ";
result += "\"schedulingType\": \"" + minstance.schedulingType() + "\", ";
result += "\"schedulingStartDay\": " + minstance.schedulingStartDay() + ", ";
result += "\"schedulingInterval\": " + minstance.schedulingInterval() + ", ";
result += "\"transactions\": \"" + minstance.transactions() + "\", ";
result += "\"activeSessions\": \"" + minstance.activeSessions() + "\", ";
result += "\"averageIdlePeriod\": \"" + minstance.averageIdlePeriod() + "\", ";
result += "\"avgTransactionTime\": \"" + minstance.avgTransactionTime() + "\",";
result += "\"autoRecover\": \"" + minstance.isAutoRecovering() + "\"";
String infoMode = (String) context().request().formValueForKey("info");
if ("full".equalsIgnoreCase(infoMode)) {
result += ", \"additionalArgs\": \"";
if (minstance.additionalArgs() != null) {
result += minstance.additionalArgs().replace("\"", "\\\"");
}
result += "\"";
}
result += "}";
}
woresponse.appendContentString("[" + result + "]");
return woresponse;
}
public WOActionResults runningAction() {
ERXResponse woresponse = new ERXResponse("YES");
String num = (String) context().request().formValueForKey("num");
int numberOfInstancesRequested = -1;
if (num != null && !num.equals("") && !num.equalsIgnoreCase("all")) {
try {
numberOfInstancesRequested = Integer.valueOf(num).intValue();
if (numberOfInstancesRequested > instances.count()) {
numberOfInstancesRequested = -1;
}
} catch (Exception e) {
// ignore
}
}
int instancesAlive = 0;
for (Enumeration enumeration = instances.objectEnumerator(); enumeration.hasMoreElements();) {
MInstance minstance = (MInstance) enumeration.nextElement();
if (minstance.state == MObject.ALIVE) {
instancesAlive++;
}
}
if ((numberOfInstancesRequested == -1 && instancesAlive < instances.count()) || instancesAlive < numberOfInstancesRequested) {
woresponse.setContent("NO");
woresponse.setStatus(ERXHttpStatusCodes.EXPECTATION_FAILED);
}
return woresponse;
}
public WOActionResults stoppedAction() {
ERXResponse woresponse = new ERXResponse("YES");
for (Enumeration enumeration = instances.objectEnumerator(); enumeration.hasMoreElements();) {
MInstance minstance = (MInstance) enumeration.nextElement();
if (minstance.state == MObject.DEAD)
continue;
woresponse.setContent("NO");
woresponse.setStatus(ERXHttpStatusCodes.EXPECTATION_FAILED);
break;
}
return woresponse;
}
public WOActionResults bounceAction() {
ERXResponse woresponse = new ERXResponse("OK");
String bouncetype = (String) context().request().formValueForKey("bouncetype");
String maxwaitString = (String) context().request().formValueForKey("maxwait");
if (bouncetype == null || bouncetype == "" || bouncetype.equalsIgnoreCase("graceful")) {
applicationsPage().bounceGraceful(applications);
} else if (bouncetype.equalsIgnoreCase("shutdown")) {
int maxwait = 30;
if (maxwaitString != null) {
try {
maxwait = Integer.valueOf(maxwaitString).intValue();
} catch (NumberFormatException e) {
// ignore
}
}
applicationsPage().bounceShutdown(applications, maxwait);
} else if (bouncetype.equalsIgnoreCase("rolling")) {
applicationsPage().bounceRolling(applications);
} else {
woresponse.setContent("Unknown bouncetype");
woresponse.setStatus(ERXHttpStatusCodes.NOT_ACCEPTABLE);
}
return woresponse;
}
public void clearDeathsAction() {
applicationsPage().clearDeaths(instances);
}
public void scheduleTypeAction() {
String scheduleType = (String) context().request().formValueForKey("scheduleType");
if (scheduleType != null && ("HOURLY".equals(scheduleType) || "DAILY".equals(scheduleType) || "WEEKLY".equals(scheduleType)))
applicationsPage().scheduleType(instances, scheduleType);
}
public void hourlyScheduleRangeAction() {
String beginScheduleWindow = (String) context().request().formValueForKey("hourBegin");
String endScheduleWindow = (String) context().request().formValueForKey("hourEnd");
String interval = (String) context().request().formValueForKey("interval");
if (beginScheduleWindow != null && endScheduleWindow != null && interval != null)
applicationsPage().hourlyStartHours(instances, Integer.parseInt(beginScheduleWindow), Integer.parseInt(endScheduleWindow), Integer.parseInt(interval));
}
public void dailyScheduleRangeAction() {
String beginScheduleWindow = (String) context().request().formValueForKey("hourBegin");
String endScheduleWindow = (String) context().request().formValueForKey("hourEnd");
if (beginScheduleWindow != null && endScheduleWindow != null)
applicationsPage().dailyStartHours(instances, Integer.parseInt(beginScheduleWindow), Integer.parseInt(endScheduleWindow));
}
public void weeklyScheduleRangeAction() {
String beginScheduleWindow = (String) context().request().formValueForKey("hourBegin");
String endScheduleWindow = (String) context().request().formValueForKey("hourEnd");
String weekDay = (String) context().request().formValueForKey("weekDay");
if (beginScheduleWindow != null && endScheduleWindow != null && weekDay != null)
applicationsPage().weeklyStartHours(instances, Integer.parseInt(beginScheduleWindow), Integer.parseInt(endScheduleWindow), Integer.parseInt(weekDay));
}
public void turnScheduledOnAction() {
applicationsPage().turnScheduledOn(instances);
}
public void turnScheduledOffAction() {
applicationsPage().turnScheduledOff(instances);
}
public void turnRefuseNewSessionsOnAction() {
applicationsPage().turnRefuseNewSessionsOn(instances);
}
public void turnRefuseNewSessionsOffAction() {
applicationsPage().turnRefuseNewSessionsOff(instances);
}
public void turnAutoRecoverOnAction() {
applicationsPage().turnAutoRecoverOn(instances);
}
public void turnAutoRecoverOffAction() {
applicationsPage().turnAutoRecoverOff(instances);
}
public void forceQuitAction() {
applicationsPage().forceQuit(instances);
}
public void stopAction() {
applicationsPage().stop(instances);
}
public void startAction() {
applicationsPage().start(instances);
}
protected void prepareApplications(NSArray<String> appNames) {
if (appNames == null)
throw new DirectActionException("at least one application name needs to be specified for type app", 406);
for (Enumeration enumeration = appNames.objectEnumerator(); enumeration.hasMoreElements();) {
String s = (String) enumeration.nextElement();
MApplication mapplication = siteConfig().applicationWithName(s);
if (mapplication != null) {
applications.addObject(mapplication);
addInstancesForApplication(mapplication);
}
else
throw new DirectActionException("Unknown application " + s, 404);
}
}
protected void prepareApplicationsOnHosts(NSArray<String> appNames, NSArray<String> hostNames) {
if (appNames == null)
throw new DirectActionException("at least one application name needs to be specified for type app", 406);
for (Enumeration enumeration = appNames.objectEnumerator(); enumeration.hasMoreElements();) {
String s = (String) enumeration.nextElement();
MApplication mapplication = siteConfig().applicationWithName(s);
if (mapplication != null) {
NSArray<MInstance> hostInstances = MInstance.HOST_NAME.in(hostNames).filtered(mapplication.instanceArray());
instances.addObjectsFromArray(hostInstances);
}
else
throw new DirectActionException("Unknown application " + s, 404);
}
}
protected void prepareInstances(NSArray<String> appNamesAndNumbers) {
if (appNamesAndNumbers == null)
throw new DirectActionException("at least one instance name needs to be specified for type ins", 406);
for (Enumeration enumeration = appNamesAndNumbers.objectEnumerator(); enumeration.hasMoreElements();) {
String s = (String) enumeration.nextElement();
MInstance minstance = siteConfig().instanceWithName(s);
if (minstance != null)
instances.addObject(minstance);
else
throw new DirectActionException("Unknown instance " + s, 404);
}
}
protected void addInstancesForApplication(MApplication mapplication) {
instances.addObjectsFromArray(mapplication.instanceArray());
}
protected void refreshInformation() {
for (Enumeration enumeration = (new NSSet((NSArray) instances.valueForKey("application"))).objectEnumerator(); enumeration.hasMoreElements();) {
MApplication mapplication = (MApplication) enumeration.nextElement();
@SuppressWarnings("unused")
AppDetailPage dummy = AppDetailPage.create(context(), mapplication);
}
}
public WOActionResults performMonitorActionNamed(String s) {
String s1 = (String) context().request().formValueForKey("type");
if ("all".equalsIgnoreCase(s1)) {
prepareApplications((NSArray) siteConfig().applicationArray().valueForKey("name"));
} else {
NSArray appNames = context().request().formValuesForKey("name");
NSArray hosts = context().request().formValuesForKey("host");
if ("app".equalsIgnoreCase(s1)) {
if (hosts == null || hosts.isEmpty()) {
prepareApplications(appNames);
} else {
prepareApplicationsOnHosts(appNames, hosts);
}
} else if ("ins".equalsIgnoreCase(s1))
prepareInstances(appNames);
else
throw new DirectActionException("Invalid type " + s1, 406);
}
refreshInformation();
_handler.startReading();
try {
WOActionResults woactionresults = super.performActionNamed(s);
return woactionresults;
} finally {
_handler.endReading();
}
}
private MSiteConfig siteConfig() {
return WOTaskdHandler.siteConfig();
}
@Override
public WOActionResults performActionNamed(String s) {
WOResponse woresponse = new ERXResponse();
if (!siteConfig().isPasswordRequired() || siteConfig().compareStringWithPassword(context().request().stringFormValueForKey("pw"))) {
try {
WOActionResults woactionresults = performMonitorActionNamed(s);
if (woactionresults != null && (woactionresults instanceof WOResponse)) {
woresponse = (WOResponse) woactionresults;
} else {
woresponse.setContent("OK");
}
} catch (DirectActionException directactionexception) {
woresponse.setStatus(directactionexception.status);
woresponse.setContent(s + " action failed: " + directactionexception.getMessage());
} catch (Exception throwable) {
woresponse.setStatus(ERXHttpStatusCodes.INTERNAL_ERROR);
woresponse.setContent(s + " action failed: " + throwable.getMessage() + ". See Monitor's log for a stack trace.");
throwable.printStackTrace();
}
} else {
woresponse.setStatus(ERXHttpStatusCodes.FORBIDDEN);
woresponse.setContent("Monitor is password protected - password missing or incorrect.");
}
return woresponse;
}
public Session mySession() {
return (Session) session();
}
}