Source code to control a Leunig ePowerSwitch-4 connector stripe using cruisecontrol. To build this you need:
.
Cruisecontrol Publisher for Leunig Power Switch
===============================================This project implements a cruisecontrol publisher for electronic power
switches by Leunig (type ePowerSwitch-4 or EPS-4). The Leunig power switch
and this publisher are another simple but powerful implementation of the
build status visualization promoted in the "Pragmatic Automation" book:
http://www.pragmaticautomation.com/cgi-bin/pragauto.cgi/Monitor/Devices/BubbleBubbleBuildsInTrouble.rdocFor the power switches see
http://www.leunig.de/
http://www.leunig.de/_pro/netzwerk/remote_power_switch/eps.htmInstallation of the Publisher
-----------------------------
Register the new publisher in the cruisecontrol config file of your project:<plugin name="epower" classname="ch.netcetera.cruisecontrol.leunigpublisher.LeunigPublisher"/>This usually happens in the very beginning of your cruisecontrol config file.
Like this all projects can re-use the new plugin under the element name
"epower" (see next section).Configuration of the Publisher
------------------------------
To publish build results to a Leunig power switch, edit the cruisecontrol config file
of your project. In the 'publishers' section of the project, add a config like: <publishers>
<leunig host="power-1.foo.com" username="default" password="default" />
</publishers>Note that you can combine the last two sections to what cruisecontrol calls
"Plugin Preconfiguration". This may be quite handy for a Leunig publisher. See
http://cruisecontrol.sourceforge.net/main/plugins.html#registration
for details.Parameter description
---------------------
required parameters:
host IP-number or hostname where the power switch's webserver can be reached
username String for basic auth on the webserver
password String for basic auth on the webserver
optional parameters:
port IP-port where the power switch's webserver can be reached (default: 80)
successPort Power connector number where the device for successful builds is attached
(allowed: 1, 2, 3 or 4). If not given, successful builds will only switch
off the failedPort, but not switch on anything.
failedPort Power connector number where the device for failed builds is attached
(allowed: 1, 2, 3 or 4). If not given, failed builds will only switch
off the successPort, but not switch on anything.
projectGroup As the typical use-case of this kind of publisher is to show ONE overall
status for a project, several build projects can be grouped. The build
status will only be "success" if ALL projects within the same group
have build status "success". The grouping happens with this parameter.
If no projectGroup is given, the publisher works for one project, as usual.
package ch.netcetera.cruisecontrol.leunigpublisher;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;import net.sourceforge.cruisecontrol.CruiseControlException;
import net.sourceforge.cruisecontrol.Publisher;
import net.sourceforge.cruisecontrol.util.XMLLogHelper;import org.apache.log4j.Logger;
import org.jdom.Element;/**
* Publisher to control a Leunig power connector with CruiseControl. It was
* tested with the Leunig EPS-4.
*/
public class LeunigPublisher implements Publisher { private static Logger logger = Logger.getLogger(LeunigPublisher.class); /**
* That's the file part of the URL that we have to call on the Leunig power
* switch.
*/
public static final String POST_PATH = "/config/home_f.html"; /**
* Identifies that a power connector port is not set (will not be switched).
*/
public static final int NO_PORT = -1; /**
* Power connector status "OFF".
*/
public static final int OFF = 0; /**
* Power connector status "ON".
*/
public static final int ON = 1; /**
* Maps project group string to a list of projects that are failed for this
* project group.
*/
private static Map<String, Set<String>> projectGroupMap = new HashMap<String, Set<String>>(); /**
* IP-number or hostname where the power switch's webserver can be reached
* (required param).
*/
private String host = null; /**
* Username string for basic auth on the webserver (default is "").
*/
private String username = ""; /**
* Password string for basic auth on the webserver (default is "").
*/
private String password = ""; /**
* IP-port where the power switch's webserver can be reached (default: 80).
*/
private int port = 80; /**
* Power connector number where the device for successful builds is attached
* (allowed: 1, 2, 3 or 4). If not given, successful builds will only switch
* off the {@link #failedPort}, but not switch on anything.
*/
private int successPort = NO_PORT; /**
* Power connector number where the device for failed builds is attached
* (allowed: 1, 2, 3 or 4). If not given, failed builds will only switch off
* the {@link #successPort}, but not switch on anything.
*/
private int failedPort = NO_PORT; /**
* As the typical use-case of a power switch publisher is to show ONE
* overall status for a project, several build projects can be grouped. The
* build status will only be "success" if ALL projects within the same group
* have build status "success". The grouping happens with this parameter. If
* no projectGroup is given, the publisher works for one project, as usual.
*/
private String projectGroup = null; /**
* Base URL for the power switching. This is essentially {@link #host}
* prefixed with <code>
http://</code> and will be set after a successful
* {@link #validate()} call.
*/
private String postUrl = null; /**
* Set testMode to true if you want to avoid real URL calls.
*/
private boolean testMode = false; /**
* Define the publishing.
*
* @param cruisecontrolLog
* JDOM Element representation of the main cruisecontrol build
* log
*/
public void publish(Element cruisecontrolLog) throws CruiseControlException {
XMLLogHelper helper = new XMLLogHelper(cruisecontrolLog);
publish(helper);
} /**
* Define the publishing.
*
* @param helper
* helper object to inject test data
*/
void publish(XMLLogHelper helper) throws CruiseControlException {
if (projectGroup == null) {
// no project group => do switching according to project status
if (helper.isBuildSuccessful()) {
switchSuccess();
} else {
// build failed
switchFailure();
}
} else {
// project group => some more statistics
if (!projectGroupMap.containsKey(projectGroup)) {
projectGroupMap.put(projectGroup, new HashSet<String>());
} // set of failed projects for this project group
Set<String> failedProjects = projectGroupMap.get(projectGroup); if (helper.isBuildSuccessful()) {
if (failedProjects.remove(helper.getProjectName())) {
logger.debug("Removed project: " + helper.getProjectName()
+ " from failed projects.");
}
} else {
logger.debug("Adding project: " + helper.getProjectName()
+ " to failed projects.");
failedProjects.add(helper.getProjectName());
} if (failedProjects.size() == 0) {
logger.debug("Setting overall buildstatus for group: "
+ projectGroup + " to success.");
switchSuccess();
} else {
logger.debug("Setting overall buildstatus for group: "
+ projectGroup + " to failure.");
switchFailure();
}
}
} /**
* Switch to success mode.
*
* @throws CruiseControlException
* if power switching failed for some reason
*/
void switchSuccess() throws CruiseControlException {
doPowerSwitch(failedPort, OFF);
doPowerSwitch(successPort, ON);
} /**
* Switch to failure mode.
*
* @throws CruiseControlException
* if power switching failed for some reason
*/
void switchFailure() throws CruiseControlException {
doPowerSwitch(successPort, OFF);
doPowerSwitch(failedPort, ON);
} /**
* Call the power switch URL for the given port and set it to the given
* state.
*
* @param powerPort
* number of the power connector to switch
* @param powerState
* power status the power connector should have after the switch
* @return String array containing post data (at index 0) and url where the
* data was posted (at index 1). The returned array is empty if the
* powerPort is equal to {@link #NO_PORT}.
* @throws CruiseControlException
* if calling the power switch failed for some reason
*/
String[] doPowerSwitch(int powerPort, int powerState)
throws CruiseControlException {
if (powerState != ON && powerState != OFF) {
throw new IllegalArgumentException("powerState is: " + powerState
+ " but is only allowed to be " + OFF + " or " + ON);
} // UTF-8 encoding of query string would be nice. But we only have
// ASCIII params, don't we :-)
String postData = "P" + powerPort + "=" + powerState; String[] returnValue = new String[2]; if (powerPort == NO_PORT) {
// nothing to do
return returnValue;
} else {
returnValue[0] = postData;
returnValue[1] = postUrl;
} try { logger.debug("Posting data: " + postData + " to url: " + postUrl); if (testMode) {
logger.info("TEST MODE, not going to call the URL.");
return returnValue;
} // set the base auth thing
Authenticator.setDefault(new BaseAuth()); // open url connection…
URL url = new URL(postUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // ...and post data:
conn.setDoOutput(true);
OutputStreamWriter wr = new OutputStreamWriter(conn
.getOutputStream());
wr.write(postData);
wr.flush();
wr.close(); /*
* Without reading back, the powerswitch did not occur.
*/
BufferedReader rd = new BufferedReader(new InputStreamReader(conn
.getInputStream()));
while (rd.readLine() != null) {
// do nothing
}
rd.close(); logger.debug("Posting worked, power connector number: " + powerPort
+ " should now have state: " + powerState); } catch (Exception e) {
String message = "Failed to post: " + postData + " to url: "
+ postUrl;
logger.error(message, e);
throw new CruiseControlException(message, e);
} return returnValue;
} /**
* Simple Basic Auth Authenticator implementation.
*/
private class BaseAuth extends Authenticator {
/**
* Password authentication using our credentials.
*/
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password.toCharArray());
}
} /**
* Called after the configuration is read to make sure that all the
* mandatory parameters were specified.
*
* @throws CruiseControlException
* if there was a configuration error.
*/
public void validate() throws CruiseControlException {
logger.debug("Start to validate configuration."); validateRequiredParam("host", host);
validateRequiredParam("username", username);
validateRequiredParam("password", password); validateHost("host", host);
validatePowerConnectorPort("successPort", successPort);
validatePowerConnectorPort("failedPort", failedPort); logger.info("Using the following configuration: host: " + host
+ ", port: " + port + ", username: " + username
+ ", password: (hidden), successPort: " + successPort
+ ", failedPort: " + failedPort + ", projectGroup: "
+ projectGroup + ", powerswitch-URL: " + postUrl);
} private void validateRequiredParam(String fieldName, String value)
throws CruiseControlException {
if (value == null || "".equals(value)) {
throw new CruiseControlException("Attribute " + fieldName
+ " is required.");
}
} private void validateHost(String fieldName, String value)
throws CruiseControlException {
String postUrlStr = "";
try {
postUrlStr = "http://" + host;
if (port != 80) {
postUrlStr += ":" + port;
}
postUrlStr += POST_PATH;
postUrl = new URL(postUrlStr).toString();
} catch (MalformedURLException e) {
throw new CruiseControlException(
"Unable to create a valid URL with the config parameters host: "
+ host + " and port: " + port + ". Tried url: "
+ postUrlStr, e);
} } private void validatePowerConnectorPort(String name, int port)
throws CruiseControlException {
if (port != NO_PORT) {
if (port != 1 && port != 2 && port != 3 && port != 4) {
throw new CruiseControlException("Parameter " + name
+ " is set to: " + port
+ " but is only allowed to be 1 or 2 or 3 or 4.");
}
// ports must never be the same number (except if no port is set)
if (successPort != NO_PORT && failedPort != NO_PORT
&& successPort == failedPort) {
throw new CruiseControlException("successPort: " + successPort
+ " and failedPort: " + failedPort
+ " must not be the same.");
}
}
} /**
* Power connector number where the device for failed builds is attached
* (allowed: 1, 2, 3 or 4). If not given, failed builds will only switch off
* the {@link #successPort}, but not switch on anything.
*
* @param failedPort
* port number
*/
public void setFailedPort(int failedPort) {
this.failedPort = failedPort;
} /**
* IP-number or hostname where the power switch's webserver can be reached
* (required param).
*
* @param host
* host name or IP address
*/
public void setHost(String host) {
this.host = host;
} /**
* Password string for basic auth on the webserver (default is "").
*
* @param password
* the password
*/
public void setPassword(String password) {
this.password = password;
} /**
* Username string for basic auth on the webserver (default is "").
*
* @param password
* the username
*/
public void setPort(int port) {
this.port = port;
} /**
* Power connector number where the device for successful builds is attached
* (allowed: 1, 2, 3 or 4). If not given, successful builds will only switch
* off the {@link #failedPort}, but not switch on anything.
*
* @param successPort
* port number
*/
public void setSuccessPort(int successPort) {
this.successPort = successPort;
} /**
* Username string for basic auth on the webserver (default is "").
*
* @param username
* the username
*/
public void setUsername(String username) {
this.username = username;
} /**
* As the typical use-case of a power switch publisher is to show ONE
* overall status for a project, several build projects can be grouped. The
* build status will only be "success" if ALL projects within the same group
* have build status "success". The grouping happens with this parameter. If
* no projectGroup is given, the publisher works for one project, as usual.
*
* @param projectGroup
* the project group to use
*/
public void setProjectGroup(String projectGroup) {
this.projectGroup = projectGroup;
} /**
* Set this to true if you don't want URLs to be called. Package protected,
* as it is only for testing.
*
* @param testMode
* new test mode flag
*/
void setTestMode(boolean testMode) {
this.testMode = testMode;
} /**
* Power connector number where the device for failed builds is attached
* (allowed: 1, 2, 3 or 4). If not given, failed builds will only switch off
* the {@link #successPort}, but not switch on anything.
*
* @return port number
*/
public int getFailedPort() {
return failedPort;
} /**
* IP-number or hostname where the power switch's webserver can be reached
* (required param).
*
* @return host name or IP address
*/
public String getHost() {
return host;
} /**
* Password string for basic auth on the webserver (default is "").
*
* @return the password
*/
public String getPassword() {
return password;
} /**
* Username string for basic auth on the webserver (default is "").
*
* @return the username
*/
public int getPort() {
return port;
} /**
* Power connector number where the device for successful builds is attached
* (allowed: 1, 2, 3 or 4). If not given, successful builds will only switch
* off the {@link #failedPort}, but not switch on anything.
*
* @return port number
*/
public int getSuccessPort() {
return successPort;
} /**
* Username string for basic auth on the webserver (default is "").
*
* @return the username
*/
public String getUsername() {
return username;
} /**
* As the typical use-case of a power switch publisher is to show ONE
* overall status for a project, several build projects can be grouped. The
* build status will only be "success" if ALL projects within the same group
* have build status "success". The grouping happens with this parameter. If
* no projectGroup is given, the publisher works for one project, as usual.
*
* @return the project group
*/
public String getProjectGroup() {
return projectGroup;
}}package ch.netcetera.cruisecontrol.leunigpublisher;import junit.framework.TestCase;
import net.sourceforge.cruisecontrol.CruiseControlException;/**
* Some simple tests for the LeunigPublisher. Does not connect to a power switch
* but does only dummy tests.
*/
public class LeunigPublisherTest extends TestCase { private LeunigPublisher publisher = null; /**
* Set up in *test* mode.
*/
public void setUp() {
publisher = new LeunigPublisher();
publisher.setTestMode(true);
} public void testSet() throws CruiseControlException {
publisher.setHost("testhost");
publisher.setUsername("testuser");
publisher.setPassword("testpassword");
publisher.setSuccessPort(2);
publisher.setFailedPort(3);
publisher.setPort(8080);
publisher.setProjectGroup("testproject"); // should work, no error config
publisher.validate(); assertEquals("testhost", publisher.getHost());
assertEquals("testuser", publisher.getUsername());
assertEquals("testpassword", publisher.getPassword());
assertEquals(2, publisher.getSuccessPort());
assertEquals(3, publisher.getFailedPort());
assertEquals(8080, publisher.getPort());
assertEquals("testproject", publisher.getProjectGroup()); publisher.setHost("127.0.0.1"); // should work, no error config
publisher.validate(); assertEquals("127.0.0.1", publisher.getHost());
} /**
* Check that the validation raises a {@link CruiseControlException} with
* the given message.
*
* @param expectedErrorMessage
* the exact error message that is expected
*/
private void doExceptionValidation(String expectedErrorMessage) {
try {
publisher.validate();
fail("Expected validation to fail with error message: "
+ expectedErrorMessage);
} catch (CruiseControlException e) {
assertEquals(expectedErrorMessage, e.getMessage());
}
} /**
* Negative tests of the validation.
*/
public void testValidation() {
// without any settings
doExceptionValidation("Attribute host is required."); // test forbidden host settings
publisher.setHost(null);
doExceptionValidation("Attribute host is required.");
publisher.setHost("");
doExceptionValidation("Attribute host is required."); publisher.setHost("testhost"); // test forbidden username settings
doExceptionValidation("Attribute username is required.");
publisher.setUsername(null);
doExceptionValidation("Attribute username is required.");
publisher.setUsername("");
doExceptionValidation("Attribute username is required."); publisher.setUsername("testuser"); // test forbidden username settings
doExceptionValidation("Attribute password is required.");
publisher.setPassword(null);
doExceptionValidation("Attribute password is required.");
publisher.setPassword("");
doExceptionValidation("Attribute password is required."); publisher.setPassword("testpassword"); publisher.setFailedPort(0);
doExceptionValidation("Parameter failedPort is set to: 0 but is only allowed to be 1 or 2 or 3 or 4.");
publisher.setFailedPort(5);
doExceptionValidation("Parameter failedPort is set to: 5 but is only allowed to be 1 or 2 or 3 or 4."); publisher.setFailedPort(2); publisher.setSuccessPort(0);
doExceptionValidation("Parameter successPort is set to: 0 but is only allowed to be 1 or 2 or 3 or 4.");
publisher.setSuccessPort(5);
doExceptionValidation("Parameter successPort is set to: 5 but is only allowed to be 1 or 2 or 3 or 4."); publisher.setSuccessPort(2);
doExceptionValidation("successPort: 2 and failedPort: 2 must not be the same."); publisher.setSuccessPort(1);
try {
publisher.validate();
} catch (CruiseControlException e) {
fail("Expected that the condiguration is complete here but got Exception: "
+ e.getMessage());
e.printStackTrace(System.err);
}
} public void testDoPowerSwitch() throws CruiseControlException {
publisher.setHost("test");
publisher.setSuccessPort(1);
publisher.setFailedPort(2);
publisher.setUsername("test");
publisher.setPassword("test");
publisher.validate(); String[] ret = publisher.doPowerSwitch(1, LeunigPublisher.ON);
assertEquals("P1=1", ret[0]); ret = publisher.doPowerSwitch(1, LeunigPublisher.OFF);
assertEquals("P1=0", ret[0]); ret = publisher.doPowerSwitch(3, LeunigPublisher.ON);
assertEquals("P3=1", ret[0]); ret = publisher.doPowerSwitch(4, LeunigPublisher.OFF);
assertEquals("P4=0", ret[0]);
} public void testPublishSuccess() throws CruiseControlException {
publisher.setHost("epower-01.netcetera.ch");
publisher.setSuccessPort(1);
publisher.setFailedPort(2);
publisher.setUsername("PASSIII");
publisher.setPassword("PASSIII");
publisher.validate(); // can't check alot after here…
try {
publisher.publish(XMLLogHelperStub.SUCCESSFUL_BUILD);
} catch (CruiseControlException e) {
fail("Excpect the publication to work but got exception: "
+ e.getMessage());
e.printStackTrace(System.err);
}
} public void testPublishFailure() throws CruiseControlException {
publisher.setHost("epower-01.netcetera.ch");
publisher.setSuccessPort(1);
publisher.setFailedPort(2);
publisher.setUsername("PASSIII");
publisher.setPassword("PASSIII");
publisher.validate(); // can't check alot after here…
try {
publisher.publish(XMLLogHelperStub.FAILED_BUILD);
} catch (CruiseControlException e) {
fail("Excpect the publication to work but got exception: "
+ e.getMessage());
e.printStackTrace(System.err);
}
} public void testProjectGroup() throws CruiseControlException {
publisher.setHost("epower-01.netcetera.ch");
publisher.setSuccessPort(1);
publisher.setFailedPort(2);
publisher.setUsername("PASSIII");
publisher.setPassword("PASSIII");
publisher.setProjectGroup("tkc-003-3");
publisher.validate(); LeunigPublisher publisher2 = new LeunigPublisher();
publisher2.setTestMode(true);
publisher2.setHost("epower-01.netcetera.ch");
publisher2.setSuccessPort(1);
publisher2.setFailedPort(2);
publisher2.setUsername("PASSIII");
publisher2.setPassword("PASSIII");
publisher2.setProjectGroup("tkc-003-3");
publisher2.validate(); XMLLogHelperStub proj = new XMLLogHelperStub("project", true);
XMLLogHelperStub proj2 = new XMLLogHelperStub("project2", true); publisher.publish(proj);
publisher2.publish(proj2); // could have some more tests here…
}}