[ start | index | login ]

source

Created by stefan. Last edited by stefan, 5 years and 332 days ago. Viewed 653 times. #3
[diff] [history] [edit] [rdf]
labels
attachments
Source code to control a Leunig ePowerSwitch-4 connector stripe using cruisecontrol. To build this you need: The readymade JAR containing the publisher class shown below can be downloaded >>here.

Small description for the configuration

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.rdoc

For the power switches see >>http://www.leunig.de/ >>http://www.leunig.de/_pro/netzwerk/remote_power_switch/eps.htm

Installation 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.

Source class

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; }

}

Test class

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… }

}

Stub needed for test class

package ch.netcetera.cruisecontrol.leunigpublisher;

import java.util.Set;

import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.util.XMLLogHelper;

/** * Stub object to simulate cruisecontrol builds. Only a minimal set of methods * is supported, see source code. */ public class XMLLogHelperStub extends XMLLogHelper {

/** * A stub object that represents a successful build. */ public static final XMLLogHelperStub SUCCESSFUL_BUILD = new XMLLogHelperStub( true);

/** * A stub object that represents a failed build. */ public static final XMLLogHelperStub FAILED_BUILD = new XMLLogHelperStub( false);

private boolean buildSuccessful = false;

private String projectName = null;

/** * Create a log helper that represents the given build status. * * @param buildSuccessful * build status */ private XMLLogHelperStub(boolean buildSuccessful) { super(null); this.buildSuccessful = buildSuccessful; this.projectName = "default"; }

/** * Create a log helper that represents the project with the given name and * build status. * * @param buildSuccessful * build status * @param projectName * name of the project */ XMLLogHelperStub(String projectName, boolean buildSuccessful) { super(null); this.buildSuccessful = buildSuccessful; this.projectName = projectName; }

public String getAntProperty(String propertyName) throws CruiseControlException { throw new UnsupportedOperationException( "not supported in the stub implementation"); }

public Set getBuildParticipants() { throw new UnsupportedOperationException( "not supported in the stub implementation"); }

public String getBuildTimestamp() throws CruiseControlException { throw new UnsupportedOperationException( "not supported in the stub implementation"); }

public String getCruiseControlInfoProperty(String propertyName) throws CruiseControlException { throw new UnsupportedOperationException( "not supported in the stub implementation"); }

public String getLabel() throws CruiseControlException { throw new UnsupportedOperationException( "not supported in the stub implementation"); }

public String getLogFileName() throws CruiseControlException { throw new UnsupportedOperationException( "not supported in the stub implementation"); }

public Set getModifications() { throw new UnsupportedOperationException( "not supported in the stub implementation"); }

public String getProjectName() throws CruiseControlException { return projectName; }

public boolean isBuildFix() throws CruiseControlException { throw new UnsupportedOperationException( "not supported in the stub implementation"); }

public boolean isBuildNecessary() { throw new UnsupportedOperationException( "not supported in the stub implementation"); }

public boolean isBuildSuccessful() { return buildSuccessful; }

public boolean wasPreviousBuildSuccessful() throws CruiseControlException { throw new UnsupportedOperationException( "not supported in the stub implementation"); }

}

no comments | post comment
search www.stefanrufer.ch
Google

Content

Me?


Blog Calendar

< February 2012 >
SunMonTueWedThuFriSat
1234
567891011
12131415161718
19202122232425
26272829

Weblog summary 2007, 2006, 2005, 2004


Content managed by SnipSnap

M

snipsnap.org | Copyright 2000-2002 Matthias L. Jugel and Stephan J. Schmidt