start working on the (events) history listing

This commit is contained in:
Vincent Behar 2011-07-29 17:31:14 +02:00
parent 8cd155f818
commit e1b8ef0bea
10 changed files with 836 additions and 0 deletions

View file

@ -30,6 +30,7 @@ import org.apache.commons.lang.StringUtils;
import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
import org.rundeck.api.domain.RundeckAbort;
import org.rundeck.api.domain.RundeckExecution;
import org.rundeck.api.domain.RundeckHistory;
import org.rundeck.api.domain.RundeckJob;
import org.rundeck.api.domain.RundeckJobsImportMethod;
import org.rundeck.api.domain.RundeckJobsImportResult;
@ -39,6 +40,7 @@ import org.rundeck.api.domain.RundeckSystemInfo;
import org.rundeck.api.domain.RundeckExecution.ExecutionStatus;
import org.rundeck.api.parser.AbortParser;
import org.rundeck.api.parser.ExecutionParser;
import org.rundeck.api.parser.HistoryParser;
import org.rundeck.api.parser.JobParser;
import org.rundeck.api.parser.JobsImportResultParser;
import org.rundeck.api.parser.ListParser;
@ -1895,6 +1897,26 @@ public class RundeckClient implements Serializable {
new AbortParser("result/abort"));
}
/*
* History
*/
/**
* Get the (events) history for the given project
*
* @param project name of the project - mandatory
* @return a {@link RundeckHistory} instance - won't be null
* @throws RundeckApiException in case of error when calling the API (non-existent project with this name)
* @throws RundeckApiLoginException if the login failed
* @throws IllegalArgumentException if the project is blank (null, empty or whitespace)
*/
public RundeckHistory getHistory(String project) throws RundeckApiException, RundeckApiLoginException,
IllegalArgumentException {
AssertUtil.notBlank(project, "project is mandatory to get the history !");
return new ApiCall(this).get(new ApiPathBuilder("/history").param("project", project),
new HistoryParser("result/events"));
}
/*
* Nodes
*/

View file

@ -0,0 +1,372 @@
/*
* Copyright 2011 Vincent Behar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rundeck.api.domain;
import java.io.Serializable;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.time.DurationFormatUtils;
/**
* Represents a RunDeck event
*
* @author Vincent Behar
*/
public class RundeckEvent implements Serializable {
private static final long serialVersionUID = 1L;
private String title;
private EventStatus status;
private String summary;
private NodeSummary nodeSummary;
private String user;
private String project;
private Date startedAt;
private Date endedAt;
/** only if the execution was aborted */
private String abortedBy;
/** only if associated with an execution */
private Long executionId;
/** only if associated with a job */
private String jobId;
/**
* @return the duration of the event in milliseconds (or null if the dates are invalid)
*/
public Long getDurationInMillis() {
if (startedAt == null || endedAt == null) {
return null;
}
return endedAt.getTime() - startedAt.getTime();
}
/**
* @return the duration of the event in seconds (or null if the dates are invalid)
*/
public Long getDurationInSeconds() {
Long durationInMillis = getDurationInMillis();
return durationInMillis != null ? TimeUnit.MILLISECONDS.toSeconds(durationInMillis) : null;
}
/**
* @return the duration of the event, as a human-readable string : "3 minutes 34 seconds" (or null if the dates are
* invalid)
*/
public String getDuration() {
Long durationInMillis = getDurationInMillis();
return durationInMillis != null ? DurationFormatUtils.formatDurationWords(durationInMillis, true, true) : null;
}
/**
* @return the duration of the event, as a "short" human-readable string : "0:03:34.187" (or null if the dates are
* invalid)
*/
public String getShortDuration() {
Long durationInMillis = getDurationInMillis();
return durationInMillis != null ? DurationFormatUtils.formatDurationHMS(durationInMillis) : null;
}
/**
* @return true if this event is for an ad-hoc command or script, false otherwise (for a job)
*/
public boolean isAdhoc() {
return "adhoc".equals(title);
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
/**
* @return the status of the event - see {@link EventStatus}
*/
public EventStatus getStatus() {
return status;
}
public void setStatus(EventStatus status) {
this.status = status;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
/**
* @return the node summary - see {@link NodeSummary}
*/
public NodeSummary getNodeSummary() {
return nodeSummary;
}
public void setNodeSummary(NodeSummary nodeSummary) {
this.nodeSummary = nodeSummary;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getProject() {
return project;
}
public void setProject(String project) {
this.project = project;
}
public Date getStartedAt() {
return (startedAt != null) ? new Date(startedAt.getTime()) : null;
}
public void setStartedAt(Date startedAt) {
this.startedAt = ((startedAt != null) ? new Date(startedAt.getTime()) : null);
}
public Date getEndedAt() {
return (endedAt != null) ? new Date(endedAt.getTime()) : null;
}
public void setEndedAt(Date endedAt) {
this.endedAt = ((endedAt != null) ? new Date(endedAt.getTime()) : null);
}
public String getAbortedBy() {
return abortedBy;
}
public void setAbortedBy(String abortedBy) {
this.abortedBy = abortedBy;
}
/**
* @return the ID of the execution associated with this event, or null if there is not
*/
public Long getExecutionId() {
return executionId;
}
public void setExecutionId(Long executionId) {
this.executionId = executionId;
}
/**
* @return the ID of the job associated with this event, or null in the case of an ad-hoc command or script
*/
public String getJobId() {
return jobId;
}
public void setJobId(String jobId) {
this.jobId = jobId;
}
@Override
public String toString() {
return "RundeckEvent [abortedBy=" + abortedBy + ", endedAt=" + endedAt + ", executionId=" + executionId
+ ", jobId=" + jobId + ", nodeSummary=" + nodeSummary + ", project=" + project + ", startedAt="
+ startedAt + ", status=" + status + ", summary=" + summary + ", title=" + title + ", user=" + user
+ "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((abortedBy == null) ? 0 : abortedBy.hashCode());
result = prime * result + ((endedAt == null) ? 0 : endedAt.hashCode());
result = prime * result + ((executionId == null) ? 0 : executionId.hashCode());
result = prime * result + ((jobId == null) ? 0 : jobId.hashCode());
result = prime * result + ((nodeSummary == null) ? 0 : nodeSummary.hashCode());
result = prime * result + ((project == null) ? 0 : project.hashCode());
result = prime * result + ((startedAt == null) ? 0 : startedAt.hashCode());
result = prime * result + ((status == null) ? 0 : status.hashCode());
result = prime * result + ((summary == null) ? 0 : summary.hashCode());
result = prime * result + ((title == null) ? 0 : title.hashCode());
result = prime * result + ((user == null) ? 0 : user.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RundeckEvent other = (RundeckEvent) obj;
if (abortedBy == null) {
if (other.abortedBy != null)
return false;
} else if (!abortedBy.equals(other.abortedBy))
return false;
if (endedAt == null) {
if (other.endedAt != null)
return false;
} else if (!endedAt.equals(other.endedAt))
return false;
if (executionId == null) {
if (other.executionId != null)
return false;
} else if (!executionId.equals(other.executionId))
return false;
if (jobId == null) {
if (other.jobId != null)
return false;
} else if (!jobId.equals(other.jobId))
return false;
if (nodeSummary == null) {
if (other.nodeSummary != null)
return false;
} else if (!nodeSummary.equals(other.nodeSummary))
return false;
if (project == null) {
if (other.project != null)
return false;
} else if (!project.equals(other.project))
return false;
if (startedAt == null) {
if (other.startedAt != null)
return false;
} else if (!startedAt.equals(other.startedAt))
return false;
if (status == null) {
if (other.status != null)
return false;
} else if (!status.equals(other.status))
return false;
if (summary == null) {
if (other.summary != null)
return false;
} else if (!summary.equals(other.summary))
return false;
if (title == null) {
if (other.title != null)
return false;
} else if (!title.equals(other.title))
return false;
if (user == null) {
if (other.user != null)
return false;
} else if (!user.equals(other.user))
return false;
return true;
}
/**
* Summary for nodes
*/
public static class NodeSummary implements Serializable {
private static final long serialVersionUID = 1L;
private int succeeded;
private int failed;
private int total;
public int getSucceeded() {
return succeeded;
}
public void setSucceeded(int succeeded) {
this.succeeded = succeeded;
}
public int getFailed() {
return failed;
}
public void setFailed(int failed) {
this.failed = failed;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
@Override
public String toString() {
return "NodeSummary [succeeded=" + succeeded + ", failed=" + failed + ", total=" + total + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + failed;
result = prime * result + succeeded;
result = prime * result + total;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
NodeSummary other = (NodeSummary) obj;
if (failed != other.failed)
return false;
if (succeeded != other.succeeded)
return false;
if (total != other.total)
return false;
return true;
}
}
/**
* The status of an event
*/
public static enum EventStatus {
SUCCEEDED, FAILED, ABORTED;
}
}

View file

@ -0,0 +1,130 @@
/*
* Copyright 2011 Vincent Behar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rundeck.api.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a portion of the RunDeck (events) history
*
* @author Vincent Behar
*/
public class RundeckHistory implements Serializable {
private static final long serialVersionUID = 1L;
private List<RundeckEvent> events;
private int count;
private int total;
private int max;
private int offset;
public void addEvent(RundeckEvent event) {
if (events == null) {
events = new ArrayList<RundeckEvent>();
}
events.add(event);
}
public List<RundeckEvent> getEvents() {
return events;
}
public void setEvents(List<RundeckEvent> events) {
this.events = events;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public int getMax() {
return max;
}
public void setMax(int max) {
this.max = max;
}
public int getOffset() {
return offset;
}
public void setOffset(int offset) {
this.offset = offset;
}
@Override
public String toString() {
return "RundeckHistory [count=" + count + ", max=" + max + ", offset=" + offset + ", total=" + total + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + count;
result = prime * result + ((events == null) ? 0 : events.hashCode());
result = prime * result + max;
result = prime * result + offset;
result = prime * result + total;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RundeckHistory other = (RundeckHistory) obj;
if (count != other.count)
return false;
if (events == null) {
if (other.events != null)
return false;
} else if (!events.equals(other.events))
return false;
if (max != other.max)
return false;
if (offset != other.offset)
return false;
if (total != other.total)
return false;
return true;
}
}

View file

@ -0,0 +1,87 @@
/*
* Copyright 2011 Vincent Behar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rundeck.api.parser;
import java.util.Date;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Node;
import org.rundeck.api.domain.RundeckEvent;
import org.rundeck.api.domain.RundeckEvent.EventStatus;
import org.rundeck.api.domain.RundeckEvent.NodeSummary;
/**
* Parser for a single {@link RundeckEvent}
*
* @author Vincent Behar
*/
public class EventParser implements XmlNodeParser<RundeckEvent> {
private String xpath;
public EventParser() {
super();
}
/**
* @param xpath of the event element if it is not the root node
*/
public EventParser(String xpath) {
super();
this.xpath = xpath;
}
@Override
public RundeckEvent parseXmlNode(Node node) {
Node eventNode = xpath != null ? node.selectSingleNode(xpath) : node;
RundeckEvent event = new RundeckEvent();
event.setTitle(StringUtils.trimToNull(eventNode.valueOf("title")));
try {
event.setStatus(EventStatus.valueOf(StringUtils.upperCase(eventNode.valueOf("status"))));
} catch (IllegalArgumentException e) {
event.setStatus(null);
}
event.setSummary(StringUtils.trimToNull(eventNode.valueOf("summary")));
NodeSummary nodeSummary = new NodeSummary();
nodeSummary.setSucceeded(Integer.valueOf(eventNode.valueOf("node-summary/@succeeded")));
nodeSummary.setFailed(Integer.valueOf(eventNode.valueOf("node-summary/@failed")));
nodeSummary.setTotal(Integer.valueOf(eventNode.valueOf("node-summary/@total")));
event.setNodeSummary(nodeSummary);
event.setUser(StringUtils.trimToNull(eventNode.valueOf("user")));
event.setProject(StringUtils.trimToNull(eventNode.valueOf("project")));
String startedAt = StringUtils.trimToNull(eventNode.valueOf("@starttime"));
if (startedAt != null) {
event.setStartedAt(new Date(Long.valueOf(startedAt)));
}
String endedAt = StringUtils.trimToNull(eventNode.valueOf("@endtime"));
if (endedAt != null) {
event.setEndedAt(new Date(Long.valueOf(endedAt)));
}
event.setAbortedBy(StringUtils.trimToNull(eventNode.valueOf("abortedby")));
try {
event.setExecutionId(Long.valueOf(eventNode.valueOf("execution/@id")));
} catch (NumberFormatException e) {
event.setExecutionId(null);
}
event.setJobId(StringUtils.trimToNull(eventNode.valueOf("job/@id")));
return event;
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright 2011 Vincent Behar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rundeck.api.parser;
import java.util.List;
import org.dom4j.Node;
import org.rundeck.api.domain.RundeckEvent;
import org.rundeck.api.domain.RundeckHistory;
/**
* Parser for a single {@link RundeckHistory}
*
* @author Vincent Behar
*/
public class HistoryParser implements XmlNodeParser<RundeckHistory> {
private String xpath;
public HistoryParser() {
super();
}
/**
* @param xpath of the history element if it is not the root node
*/
public HistoryParser(String xpath) {
super();
this.xpath = xpath;
}
@Override
public RundeckHistory parseXmlNode(Node node) {
Node eventsNode = xpath != null ? node.selectSingleNode(xpath) : node;
RundeckHistory history = new RundeckHistory();
history.setCount(Integer.valueOf(eventsNode.valueOf("@count")));
history.setTotal(Integer.valueOf(eventsNode.valueOf("@total")));
history.setMax(Integer.valueOf(eventsNode.valueOf("@max")));
history.setOffset(Integer.valueOf(eventsNode.valueOf("@offset")));
@SuppressWarnings("unchecked")
List<Node> eventNodes = eventsNode.selectNodes("event");
EventParser eventParser = new EventParser();
for (Node eventNode : eventNodes) {
RundeckEvent event = eventParser.parseXmlNode(eventNode);
history.addEvent(event);
}
return history;
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright 2011 Vincent Behar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rundeck.api.parser;
import java.io.InputStream;
import java.util.Date;
import org.dom4j.Document;
import org.junit.Assert;
import org.junit.Test;
import org.rundeck.api.domain.RundeckEvent;
import org.rundeck.api.domain.RundeckEvent.EventStatus;
/**
* Test the {@link EventParser}
*
* @author Vincent Behar
*/
public class EventParserTest {
@Test
public void parseSucceededEvent() throws Exception {
InputStream input = getClass().getResourceAsStream("event-succeeded.xml");
Document document = ParserHelper.loadDocument(input);
RundeckEvent event = new EventParser("event").parseXmlNode(document);
Assert.assertFalse(event.isAdhoc());
Assert.assertEquals("job-name", event.getTitle());
Assert.assertEquals(EventStatus.SUCCEEDED, event.getStatus());
Assert.assertEquals("ps", event.getSummary());
Assert.assertEquals(2, event.getNodeSummary().getSucceeded());
Assert.assertEquals(0, event.getNodeSummary().getFailed());
Assert.assertEquals(2, event.getNodeSummary().getTotal());
Assert.assertEquals("admin", event.getUser());
Assert.assertEquals("test", event.getProject());
Assert.assertEquals(new Date(1311946495646L), event.getStartedAt());
Assert.assertEquals(new Date(1311946557618L), event.getEndedAt());
Assert.assertEquals("1", event.getJobId());
Assert.assertEquals(new Long(2), event.getExecutionId());
}
@Test
public void parseAdhocEvent() throws Exception {
InputStream input = getClass().getResourceAsStream("event-adhoc.xml");
Document document = ParserHelper.loadDocument(input);
RundeckEvent event = new EventParser("event").parseXmlNode(document);
Assert.assertTrue(event.isAdhoc());
Assert.assertEquals("adhoc", event.getTitle());
Assert.assertEquals(EventStatus.FAILED, event.getStatus());
Assert.assertEquals("ls $HOME", event.getSummary());
Assert.assertEquals(1, event.getNodeSummary().getSucceeded());
Assert.assertEquals(1, event.getNodeSummary().getFailed());
Assert.assertEquals(2, event.getNodeSummary().getTotal());
Assert.assertEquals("admin", event.getUser());
Assert.assertEquals("test", event.getProject());
Assert.assertEquals(new Date(1311945953547L), event.getStartedAt());
Assert.assertEquals(new Date(1311945963467L), event.getEndedAt());
Assert.assertEquals(null, event.getJobId());
Assert.assertEquals(new Long(1), event.getExecutionId());
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright 2011 Vincent Behar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rundeck.api.parser;
import java.io.InputStream;
import java.util.Date;
import org.dom4j.Document;
import org.junit.Assert;
import org.junit.Test;
import org.rundeck.api.domain.RundeckEvent;
import org.rundeck.api.domain.RundeckHistory;
import org.rundeck.api.domain.RundeckEvent.EventStatus;
/**
* Test the {@link HistoryParser}
*
* @author Vincent Behar
*/
public class HistoryParserTest {
@Test
public void parseHistory() throws Exception {
InputStream input = getClass().getResourceAsStream("history.xml");
Document document = ParserHelper.loadDocument(input);
RundeckHistory history = new HistoryParser("result/events").parseXmlNode(document);
Assert.assertEquals(2, history.getCount());
Assert.assertEquals(4, history.getTotal());
Assert.assertEquals(2, history.getMax());
Assert.assertEquals(0, history.getOffset());
Assert.assertEquals(2, history.getEvents().size());
RundeckEvent event1 = history.getEvents().get(0);
Assert.assertFalse(event1.isAdhoc());
Assert.assertEquals("job-name", event1.getTitle());
Assert.assertEquals(EventStatus.SUCCEEDED, event1.getStatus());
Assert.assertEquals("ps", event1.getSummary());
Assert.assertEquals(2, event1.getNodeSummary().getSucceeded());
Assert.assertEquals(0, event1.getNodeSummary().getFailed());
Assert.assertEquals(2, event1.getNodeSummary().getTotal());
Assert.assertEquals("admin", event1.getUser());
Assert.assertEquals("test", event1.getProject());
Assert.assertEquals(new Date(1311946495646L), event1.getStartedAt());
Assert.assertEquals(new Date(1311946557618L), event1.getEndedAt());
Assert.assertEquals("1", event1.getJobId());
Assert.assertEquals(new Long(2), event1.getExecutionId());
RundeckEvent event2 = history.getEvents().get(1);
Assert.assertTrue(event2.isAdhoc());
Assert.assertEquals("adhoc", event2.getTitle());
Assert.assertEquals(EventStatus.FAILED, event2.getStatus());
Assert.assertEquals("ls $HOME", event2.getSummary());
Assert.assertEquals(1, event2.getNodeSummary().getSucceeded());
Assert.assertEquals(1, event2.getNodeSummary().getFailed());
Assert.assertEquals(2, event2.getNodeSummary().getTotal());
Assert.assertEquals("admin", event2.getUser());
Assert.assertEquals("test", event2.getProject());
Assert.assertEquals(new Date(1311945953547L), event2.getStartedAt());
Assert.assertEquals(new Date(1311945963467L), event2.getEndedAt());
Assert.assertEquals(null, event2.getJobId());
Assert.assertEquals(new Long(1), event2.getExecutionId());
}
}

View file

@ -0,0 +1 @@
<event starttime='1311945953547' endtime='1311945963467'><title>adhoc</title><status>failed</status><summary>ls $HOME</summary><node-summary succeeded='1' failed='1' total='2'/><user>admin</user><project>test</project><date-started>2011-07-29T13:25:53Z</date-started><date-ended>2011-07-29T13:26:03Z</date-ended><execution id='1'/></event>

View file

@ -0,0 +1 @@
<event starttime='1311946495646' endtime='1311946557618'><title>job-name</title><status>succeeded</status><summary>ps</summary><node-summary succeeded='2' failed='0' total='2'/><user>admin</user><project>test</project><date-started>2011-07-29T13:34:55Z</date-started><date-ended>2011-07-29T13:35:57Z</date-ended><job id='1'/><execution id='2'/></event>

View file

@ -0,0 +1 @@
<result success='true' apiversion='1'><events count='2' total='4' max='2' offset='0'><event starttime='1311946495646' endtime='1311946557618'><title>job-name</title><status>succeeded</status><summary>ps</summary><node-summary succeeded='2' failed='0' total='2'/><user>admin</user><project>test</project><date-started>2011-07-29T13:34:55Z</date-started><date-ended>2011-07-29T13:35:57Z</date-ended><job id='1'/><execution id='2'/></event><event starttime='1311945953547' endtime='1311945963467'><title>adhoc</title><status>failed</status><summary>ls $HOME</summary><node-summary succeeded='1' failed='1' total='2'/><user>admin</user><project>test</project><date-started>2011-07-29T13:25:53Z</date-started><date-ended>2011-07-29T13:26:03Z</date-ended><execution id='1'/></event></events></result>