diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index 3fbab43fe2355c4d628e47afb1662182b7761be9..39cb113d1fc325fc5bc319c8ca651b3b0415d04a 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -67,7 +67,6 @@ import hudson.util.NamingThreadFactory; import jenkins.model.Jenkins; import jenkins.util.ContextResettingExecutorService; import jenkins.security.MasterToSlaveCallable; -import jenkins.security.NotReallyRoleSensitiveCallable; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; @@ -1411,7 +1410,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces } Node result = node.reconfigure(req, req.getSubmittedForm()); - replaceBy(result); + Jenkins.getInstance().getNodesObject().replaceNode(this.getNode(), result); // take the user back to the agent top page. rsp.sendRedirect2("../" + result.getNodeName() + '/'); @@ -1445,28 +1444,6 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces rsp.sendError(SC_BAD_REQUEST); } - /** - * Replaces the current {@link Node} by another one. - */ - private void replaceBy(final Node newNode) throws ServletException, IOException { - final Jenkins app = Jenkins.getInstance(); - - // use the queue lock until Nodes has a way of directly modifying a single node. - Queue.withLock(new NotReallyRoleSensitiveCallable() { - public Void call() throws IOException { - List nodes = new ArrayList(app.getNodes()); - Node node = getNode(); - int i = (node != null) ? nodes.indexOf(node) : -1; - if(i<0) { - throw new IOException("This agent appears to be removed while you were editing the configuration"); - } - nodes.set(i, newNode); - app.setNodes(nodes); - return null; - } - }); - } - /** * Updates Job by its XML definition. * @@ -1475,7 +1452,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces public void updateByXml(final InputStream source) throws IOException, ServletException { checkPermission(CONFIGURE); Node result = (Node)Jenkins.XSTREAM2.fromXML(source); - replaceBy(result); + Jenkins.getInstance().getNodesObject().replaceNode(this.getNode(), result); } /** diff --git a/core/src/main/java/jenkins/model/NodeListener.java b/core/src/main/java/jenkins/model/NodeListener.java new file mode 100644 index 0000000000000000000000000000000000000000..06596e9003bdf273de238b76f6877401215b3deb --- /dev/null +++ b/core/src/main/java/jenkins/model/NodeListener.java @@ -0,0 +1,112 @@ +/* + * The MIT License + * + * Copyright (c) Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.model; + +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.model.Node; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Listen to {@link Node} CRUD operations. + * + * @author ogondza. + * @since TODO + */ +public abstract class NodeListener implements ExtensionPoint { + + private static final Logger LOGGER = Logger.getLogger(NodeListener.class.getName()); + + /** + * Node is being created. + */ + protected void onCreated(@Nonnull Node node) {} + + /** + * Node is being updated. + */ + protected void onUpdated(@Nonnull Node oldOne, @Nonnull Node newOne) {} + + /** + * Node is being deleted. + */ + protected void onDeleted(@Nonnull Node node) {} + + /** + * Inform listeners that node is being created. + * + * @param node A node being created. + */ + public static void fireOnCreated(@Nonnull Node node) { + for (NodeListener nl: all()) { + try { + nl.onCreated(node); + } catch (Throwable ex) { + LOGGER.log(Level.WARNING, "Listener invocation failed", ex); + } + } + } + + /** + * Inform listeners that node is being updated. + * + * @param oldOne Old configuration. + * @param newOne New Configuration. + */ + public static void fireOnUpdated(@Nonnull Node oldOne, @Nonnull Node newOne) { + for (NodeListener nl: all()) { + try { + nl.onUpdated(oldOne, newOne); + } catch (Throwable ex) { + LOGGER.log(Level.WARNING, "Listener invocation failed", ex); + } + } + } + + /** + * Inform listeners that node is being removed. + * + * @param node A node being removed. + */ + public static void fireOnDeleted(@Nonnull Node node) { + for (NodeListener nl: all()) { + try { + nl.onDeleted(node); + } catch (Throwable ex) { + LOGGER.log(Level.WARNING, "Listener invocation failed", ex); + } + } + } + + /** + * Get all {@link NodeListener}s registered in Jenkins. + */ + public static @Nonnull List all() { + return ExtensionList.lookup(NodeListener.class); + } +} diff --git a/core/src/main/java/jenkins/model/Nodes.java b/core/src/main/java/jenkins/model/Nodes.java index f11affc06becdc4a14dd5b1eec6a6a4cf98b6a9a..b232b370bcdef5c458732e93f8b426caae92fe31 100644 --- a/core/src/main/java/jenkins/model/Nodes.java +++ b/core/src/main/java/jenkins/model/Nodes.java @@ -34,6 +34,7 @@ import hudson.model.listeners.SaveableListener; import hudson.slaves.EphemeralNode; import hudson.slaves.OfflineCause; import java.util.concurrent.Callable; + import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -139,6 +140,7 @@ public class Nodes implements Saveable { }); // TODO there is a theoretical race whereby the node instance is updated/removed after lock release persistNode(node); + NodeListener.fireOnCreated(node); } } @@ -198,6 +200,31 @@ public class Nodes implements Saveable { return false; } + /** + * Replace node of given name. + * + * @return {@code true} if node was replaced. + * @since TODO + */ + public boolean replaceNode(final Node oldOne, final @Nonnull Node newOne) throws IOException { + if (oldOne == nodes.get(oldOne.getNodeName())) { + // use the queue lock until Nodes has a way of directly modifying a single node. + Queue.withLock(new Runnable() { + public void run() { + Nodes.this.nodes.remove(oldOne.getNodeName()); + Nodes.this.nodes.put(newOne.getNodeName(), newOne); + jenkins.updateComputerList(); + jenkins.trimLabels(); + } + }); + updateNode(newOne); + NodeListener.fireOnUpdated(oldOne, newOne); + return true; + } else { + return false; + } + } + /** * Removes a node. If the node instance is not in the list of nodes, then this will be a no-op, even if * there is another instance with the same {@link Node#getNodeName()}. @@ -223,6 +250,8 @@ public class Nodes implements Saveable { }); // no need for a full save() so we just do the minimum Util.deleteRecursive(new File(getNodesDir(), node.getNodeName())); + + NodeListener.fireOnDeleted(node); } } diff --git a/test/src/test/java/jenkins/model/NodeListenerTest.java b/test/src/test/java/jenkins/model/NodeListenerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..90f1afe550e6753fcb1722e5b607c20c11b1befb --- /dev/null +++ b/test/src/test/java/jenkins/model/NodeListenerTest.java @@ -0,0 +1,51 @@ +package jenkins.model; + +import hudson.ExtensionList; +import hudson.cli.CLICommand; +import hudson.cli.CLICommandInvoker; +import hudson.cli.CreateNodeCommand; +import hudson.cli.DeleteNodeCommand; +import hudson.cli.GetNodeCommand; +import hudson.cli.UpdateNodeCommand; +import hudson.model.Node; +import hudson.slaves.DumbSlave; +import org.apache.tools.ant.filters.StringInputStream; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.mockito.Mockito.*; + +public class NodeListenerTest { + + @Rule public JenkinsRule j = new JenkinsRule(); + + private NodeListener mock; + + @Before + public void setUp() { + mock = mock(NodeListener.class); + ExtensionList.lookup(NodeListener.class).add(mock); + } + + @Test + public void crud() throws Exception { + DumbSlave slave = j.createSlave(); + String xml = cli(new GetNodeCommand()).invokeWithArgs(slave.getNodeName()).stdout(); + cli(new UpdateNodeCommand()).withStdin(new StringInputStream(xml)).invokeWithArgs(slave.getNodeName()); + cli(new DeleteNodeCommand()).invokeWithArgs(slave.getNodeName()); + + cli(new CreateNodeCommand()).withStdin(new StringInputStream(xml)).invokeWithArgs("replica"); + j.jenkins.getComputer("replica").doDoDelete(); + + verify(mock, times(2)).onCreated(any(Node.class)); + verify(mock, times(1)).onUpdated(any(Node.class), any(Node.class)); + verify(mock, times(2)).onDeleted(any(Node.class)); + verifyNoMoreInteractions(mock); + } + + private CLICommandInvoker cli(CLICommand cmd) { + return new CLICommandInvoker(j, cmd); + } +} diff --git a/test/src/test/resources/hudson/model/node.xml b/test/src/test/resources/hudson/model/node.xml index 65685361ebff25ea503b04fdd3e54ba8a59e7192..89c034121f380b61f8b78a50335f76ef493e4b18 100644 --- a/test/src/test/resources/hudson/model/node.xml +++ b/test/src/test/resources/hudson/model/node.xml @@ -10,9 +10,7 @@ - - "/opt/java6/jre/bin/java" -jar "slave.jar" - + SYSTEM