Node.java 19.7 KB
Newer Older
K
kohsuke 已提交
1 2
/*
 * The MIT License
3
 *
4 5
 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
 * Seiji Sogabe, Stephen Connolly
6
 *
K
kohsuke 已提交
7 8 9 10 11 12
 * 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:
13
 *
K
kohsuke 已提交
14 15
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
16
 *
K
kohsuke 已提交
17 18 19 20 21 22 23 24
 * 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.
 */
K
kohsuke 已提交
25 26
package hudson.model;

27
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
28 29 30 31 32 33
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.FilePath;
import hudson.FileSystemProvisioner;
import hudson.Launcher;
import hudson.Util;
34
import hudson.model.Descriptor.FormException;
35
import hudson.model.Queue.Task;
36
import hudson.model.labels.LabelAtom;
37
import hudson.model.queue.CauseOfBlockage;
K
Kohsuke Kawaguchi 已提交
38
import hudson.remoting.Callable;
39
import hudson.remoting.VirtualChannel;
40
import hudson.security.ACL;
K
kohsuke 已提交
41
import hudson.security.AccessControlled;
42
import hudson.security.Permission;
N
Nathan Parry 已提交
43
import hudson.slaves.ComputerListener;
K
kohsuke 已提交
44
import hudson.slaves.NodeDescriptor;
45 46
import hudson.slaves.NodeProperty;
import hudson.slaves.NodePropertyDescriptor;
N
Nathan Parry 已提交
47
import hudson.slaves.OfflineCause;
48
import hudson.util.ClockDifference;
49
import hudson.util.DescribableList;
K
kohsuke 已提交
50
import hudson.util.EnumConverter;
51 52
import hudson.util.TagCloud;
import hudson.util.TagCloud.WeightFunction;
K
kohsuke 已提交
53
import java.io.IOException;
54
import java.lang.reflect.Type;
55 56
import java.util.Collections;
import java.util.HashSet;
K
kohsuke 已提交
57
import java.util.List;
58
import java.util.Set;
59
import java.util.concurrent.atomic.AtomicReference;
N
Nathan Parry 已提交
60
import java.util.logging.Logger;
61
import javax.annotation.CheckForNull;
62
import javax.annotation.Nonnull;
63
import jenkins.model.Jenkins;
64
import jenkins.util.io.OnMaster;
65
import net.sf.json.JSONObject;
66
import org.acegisecurity.Authentication;
S
ssogabe 已提交
67
import org.jvnet.localizer.Localizable;
68
import org.kohsuke.stapler.BindInterceptor;
69
import org.kohsuke.stapler.Stapler;
70
import org.kohsuke.stapler.StaplerRequest;
K
kohsuke 已提交
71
import org.kohsuke.stapler.export.Exported;
72
import org.kohsuke.stapler.export.ExportedBean;
73

K
kohsuke 已提交
74
/**
75
 * Base type of Jenkins agents (although in practice, you probably extend {@link Slave} to define a new agent type).
K
kohsuke 已提交
76 77
 *
 * <p>
78
 * As a special case, {@link Jenkins} extends from here.
K
kohsuke 已提交
79
 *
80 81 82
 * <p>
 * Nodes are persisted objects that capture user configurations, and instances get thrown away and recreated whenever
 * the configuration changes. Running state of nodes are captured by {@link Computer}s.
83
 *
84 85
 * <p>
 * There is no URL binding for {@link Node}. {@link Computer} and {@link TransientComputerActionFactory} must
86
 * be used to associate new {@link Action}s to agents.
87
 *
K
kohsuke 已提交
88
 * @author Kohsuke Kawaguchi
K
kohsuke 已提交
89
 * @see NodeDescriptor
90
 * @see Computer
K
kohsuke 已提交
91
 */
K
kohsuke 已提交
92
@ExportedBean
93
public abstract class Node extends AbstractModelObject implements ReconfigurableDescribable<Node>, ExtensionPoint, AccessControlled, OnMaster, Saveable {
N
Nathan Parry 已提交
94 95 96

    private static final Logger LOGGER = Logger.getLogger(Node.class.getName());

97
    /**
98
     * Newly copied agents get this flag set, so that Jenkins doesn't try to start/remove this node until its configuration
99 100 101
     * is saved once.
     */
    protected volatile transient boolean holdOffLaunchUntilSave;
102

103 104 105 106 107
    public String getDisplayName() {
        return getNodeName(); // default implementation
    }

    public String getSearchUrl() {
108 109 110 111 112
        Computer c = toComputer();
        if (c != null) {
            return c.getUrl();
        }
        return "computer/" + Util.rawEncode(getNodeName());
113 114
    }

115 116 117 118
    public boolean isHoldOffLaunchUntilSave() {
        return holdOffLaunchUntilSave;
    }

119 120
    /**
     * {@inheritDoc}
121
     * @since 1.635.
122 123 124
     */
    @Override
    public void save() throws IOException {
125 126 127 128 129 130
        // this should be a no-op unless this node instance is the node instance in Jenkins' list of nodes
        // thus where Jenkins.getInstance() == null there is no list of nodes, so we do a no-op
        // Nodes.updateNode(n) will only persist the node record if the node instance is in the list of nodes
        // so either path results in the same behaviour: the node instance is only saved if it is in the list of nodes
        // for all other cases we do not know where to persist the node record and hence we follow the default
        // no-op of a Saveable.NOOP
131 132 133 134 135 136
        final Jenkins jenkins = Jenkins.getInstance();
        if (jenkins != null) {
            jenkins.updateNode(this);
        }
    }

K
kohsuke 已提交
137
    /**
K
kohsuke 已提交
138
     * Name of this node.
K
kohsuke 已提交
139 140 141 142
     *
     * @return
     *      "" if this is master
     */
K
kohsuke 已提交
143
    @Exported(visibility=999)
J
Jesse Glick 已提交
144
    public abstract @Nonnull String getNodeName();
K
kohsuke 已提交
145

K
kohsuke 已提交
146 147 148 149 150 151 152 153 154 155
    /**
     * When the user clones a {@link Node}, Hudson uses this method to change the node name right after
     * the cloned {@link Node} object is instantiated.
     *
     * <p>
     * This method is never used for any other purpose, and as such for all practical intents and purposes,
     * the node name should be treated like immutable.
     *
     * @deprecated to indicate that this method isn't really meant to be called by random code.
     */
156
    @Deprecated
157
    public abstract void setNodeName(String name);
K
kohsuke 已提交
158

K
kohsuke 已提交
159 160 161
    /**
     * Human-readable description of this node.
     */
K
kohsuke 已提交
162
    @Exported
163
    public abstract String getNodeDescription();
K
kohsuke 已提交
164 165 166

    /**
     * Returns a {@link Launcher} for executing programs on this node.
K
kohsuke 已提交
167 168
     *
     * <p>
169
     * The callee must call {@link Launcher#decorateFor(Node)} before returning to complete the decoration.
K
kohsuke 已提交
170
     */
171
    public abstract Launcher createLauncher(TaskListener listener);
K
kohsuke 已提交
172 173 174 175 176 177 178

    /**
     * Returns the number of {@link Executor}s.
     *
     * This may be different from <code>getExecutors().size()</code>
     * because it takes time to adjust the number of executors.
     */
K
kohsuke 已提交
179
    @Exported
180
    public abstract int getNumExecutors();
K
kohsuke 已提交
181 182

    /**
183
     * Returns {@link Mode#EXCLUSIVE} if this node is only available
K
kohsuke 已提交
184 185 186
     * for those jobs that exclusively specifies this node
     * as the assigned node.
     */
K
kohsuke 已提交
187
    @Exported
188
    public abstract Mode getMode();
K
kohsuke 已提交
189

190 191 192 193
    /**
     * Gets the corresponding {@link Computer} object.
     *
     * @return
K
kohsuke 已提交
194 195
     *      this method can return null if there's no {@link Computer} object for this node,
     *      such as when this node has no executors at all.
196
     */
197
    public final @CheckForNull Computer toComputer() {
198 199
        AbstractCIBase ciBase = Jenkins.getInstance();
        return ciBase.getComputer(this);
200
    }
201

K
kohsuke 已提交
202 203 204
    /**
     * Gets the current channel, if the node is connected and online, or null.
     *
205
     * This is just a convenience method for {@link Computer#getChannel()} with null check.
K
kohsuke 已提交
206
     */
207
    public final @CheckForNull VirtualChannel getChannel() {
K
kohsuke 已提交
208 209 210 211
        Computer c = toComputer();
        return c==null ? null : c.getChannel();
    }

K
kohsuke 已提交
212 213
    /**
     * Creates a new {@link Computer} object that acts as the UI peer of this {@link Node}.
214
     * Nobody but {@link Jenkins#updateComputerList()} should call this method.
K
kohsuke 已提交
215
     */
216
    protected abstract Computer createComputer();
K
kohsuke 已提交
217

218
    /**
219
     * Returns {@code true} if the node is accepting tasks. Needed to allow agents programmatic suspension of task
220 221 222 223 224 225
     * scheduling that does not overlap with being offline. Called by {@link Computer#isAcceptingTasks()}.
     * This method is distinct from {@link Computer#isAcceptingTasks()} as sometimes the {@link Node} concrete
     * class may not have control over the {@link hudson.model.Computer} concrete class associated with it.
     *
     * @return {@code true} if the node is accepting tasks.
     * @see Computer#isAcceptingTasks()
S
Stephen Connolly 已提交
226
     * @since 1.586
227 228 229 230 231
     */
    public boolean isAcceptingTasks() {
        return true;
    }

N
Nathan Parry 已提交
232 233 234 235 236 237 238 239 240 241 242 243
    /**
     * Let Nodes be aware of the lifecycle of their own {@link Computer}.
     */
    @Extension
    public static class InternalComputerListener extends ComputerListener {
        @Override
        public void onOnline(Computer c, TaskListener listener) {
            Node node = c.getNode();

            // At startup, we need to restore any previously in-effect temp offline cause.
            // We wait until the computer is started rather than getting the data to it sooner
            // so that the normal computer start up processing works as expected.
244
            if (node!= null && node.temporaryOfflineCause != null && node.temporaryOfflineCause != c.getOfflineCause()) {
N
Nathan Parry 已提交
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
                c.setTemporarilyOffline(true, node.temporaryOfflineCause);
            }
        }
    }

    private OfflineCause temporaryOfflineCause;

    /**
     * Enable a {@link Computer} to inform its node when it is taken
     * temporarily offline.
     */
    void setTemporaryOfflineCause(OfflineCause cause) {
        try {
            if (temporaryOfflineCause != cause) {
                temporaryOfflineCause = cause;
260
                save();
N
Nathan Parry 已提交
261 262 263 264 265 266
            }
        } catch (java.io.IOException e) {
            LOGGER.warning("Unable to complete save, temporary offline status will not be persisted: " + e.getMessage());
        }
    }

267 268 269
    /**
     * Return the possibly empty tag cloud for the labels of this node.
     */
270 271 272
    public TagCloud<LabelAtom> getLabelCloud() {
        return new TagCloud<LabelAtom>(getAssignedLabels(),new WeightFunction<LabelAtom>() {
            public float weight(LabelAtom item) {
273
                return item.getTiedJobCount();
274 275 276
            }
        });
    }
277 278
    /**
     * Returns the possibly empty set of labels that are assigned to this node,
279 280 281 282 283
     * including the automatic {@link #getSelfLabel() self label}, manually
     * assigned labels and dynamically assigned labels via the
     * {@link LabelFinder} extension point.
     *
     * This method has a side effect of updating the hudson-wide set of labels
284
     * and should be called after events that will change that - e.g. a agent
285
     * connecting.
286
     */
K
kohsuke 已提交
287
    @Exported
288 289
    public Set<LabelAtom> getAssignedLabels() {
        Set<LabelAtom> r = Label.parse(getLabelString());
290 291 292 293
        r.add(getSelfLabel());
        r.addAll(getDynamicLabels());
        return Collections.unmodifiableSet(r);
    }
294

K
kohsuke 已提交
295
    /**
296 297 298 299
     * Return all the labels assigned dynamically to this node.
     * This calls all the LabelFinder implementations with the node converts
     * the results into Labels.
     * @return HashSet<Label>.
K
kohsuke 已提交
300
     */
301 302 303
    private HashSet<LabelAtom> getDynamicLabels() {
        HashSet<LabelAtom> result = new HashSet<LabelAtom>();
        for (LabelFinder labeler : LabelFinder.all()) {
304
            // Filter out any bad(null) results from plugins
305
            // for compatibility reasons, findLabels may return LabelExpression and not atom.
306
            for (Label label : labeler.findLabels(this))
307 308
                if (label instanceof LabelAtom) result.add((LabelAtom)label);
        }
309 310 311
        return result;
    }

K
kohsuke 已提交
312

313 314
    /**
     * Returns the manually configured label for a node. The list of assigned
315
     * and dynamically determined labels is available via
316 317
     * {@link #getAssignedLabels()} and includes all labels that have been
     * manually configured.
318
     *
319
     * Mainly for form binding.
320
     */
321
    public abstract String getLabelString();
322

323 324 325 326 327
    /**
     * Sets the label string for a node. This value will be returned by {@link #getLabelString()}.
     *
     * @param labelString
     *      The new label string to use.
328
     * @since 1.477
329 330
     */
    public void setLabelString(String labelString) throws IOException {
331
        throw new UnsupportedOperationException();
332 333
    }

334 335 336
    /**
     * Gets the special label that represents this node itself.
     */
337 338 339
    @WithBridgeMethods(Label.class)
    public LabelAtom getSelfLabel() {
        return LabelAtom.get(getNodeName());
340
    }
341

342 343 344 345 346 347 348 349 350
    /**
     * Called by the {@link Queue} to determine whether or not this node can
     * take the given task. The default checks include whether or not this node
     * is part of the task's assigned label, whether this node is in
     * {@link Mode#EXCLUSIVE} mode if it is not in the task's assigned label,
     * and whether or not any of this node's {@link NodeProperty}s say that the
     * task cannot be run.
     *
     * @since 1.360
351 352
     * @deprecated as of 1.413
     *      Use {@link #canTake(Queue.BuildableItem)}
353
     */
354
    @Deprecated
355
    public CauseOfBlockage canTake(Task task) {
356 357 358 359 360 361 362 363 364 365 366 367 368 369
        return null;
    }

    /**
     * Called by the {@link Queue} to determine whether or not this node can
     * take the given task. The default checks include whether or not this node
     * is part of the task's assigned label, whether this node is in
     * {@link Mode#EXCLUSIVE} mode if it is not in the task's assigned label,
     * and whether or not any of this node's {@link NodeProperty}s say that the
     * task cannot be run.
     *
     * @since 1.413
     */
    public CauseOfBlockage canTake(Queue.BuildableItem item) {
370
        Label l = item.getAssignedLabel();
371 372 373
        if(l!=null && !l.contains(this))
            return CauseOfBlockage.fromMessage(Messages._Node_LabelMissing(getNodeName(),l));   // the task needs to be executed on label that this node doesn't have.

374 375 376 377 378 379 380 381 382 383
        if(l==null && getMode()== Mode.EXCLUSIVE) {
            // flyweight tasks need to get executed somewhere, if every node
            if (!(item.task instanceof Queue.FlyweightTask && (
                    this instanceof Jenkins
                            || Jenkins.getInstance().getNumExecutors() < 1
                            || Jenkins.getInstance().getMode() == Mode.EXCLUSIVE)
            )) {
                return CauseOfBlockage.fromMessage(Messages._Node_BecauseNodeIsReserved(getNodeName()));   // this node is reserved for tasks that are tied to it
            }
        }
384

385
        Authentication identity = item.authenticate();
386
        if (!getACL().hasPermission(identity,Computer.BUILD)) {
387 388 389 390
            // doesn't have a permission
            return CauseOfBlockage.fromMessage(Messages._Node_LackingBuildPermission(identity.getName(),getNodeName()));
        }

391 392 393
        // Check each NodeProperty to see whether they object to this node
        // taking the task
        for (NodeProperty prop: getNodeProperties()) {
394
            CauseOfBlockage c = prop.canTake(item);
395 396 397
            if (c!=null)    return c;
        }

398 399 400 401
        if (!isAcceptingTasks()) {
            return CauseOfBlockage.fromMessage(Messages._Node_BecauseNodeIsNotAcceptingTasks(getNodeName()));
        }

402 403 404 405
        // Looks like we can take the task
        return null;
    }

406 407 408 409 410 411
    /**
     * Returns a "workspace" directory for the given {@link TopLevelItem}.
     *
     * <p>
     * Workspace directory is usually used for keeping out the checked out
     * source code, but it can be used for anything.
K
kohsuke 已提交
412 413 414
     *
     * @return
     *      null if this node is not connected hence the path is not available
415
     */
K
kohsuke 已提交
416
    // TODO: should this be modified now that getWorkspace is moved from AbstractProject to AbstractBuild?
J
Jesse Glick 已提交
417
    public abstract @CheckForNull FilePath getWorkspaceFor(TopLevelItem item);
418

K
kohsuke 已提交
419 420 421 422 423 424 425 426 427 428 429
    /**
     * Gets the root directory of this node.
     *
     * <p>
     * Hudson always owns a directory on every node. This method
     * returns that.
     *
     * @return
     *      null if the node is offline and hence the {@link FilePath}
     *      object is not available.
     */
J
Jesse Glick 已提交
430
    public abstract @CheckForNull FilePath getRootPath();
K
kohsuke 已提交
431

432 433 434
    /**
     * Gets the {@link FilePath} on this node.
     */
435
    public @CheckForNull FilePath createPath(String absolutePath) {
K
kohsuke 已提交
436
        VirtualChannel ch = getChannel();
437 438 439 440
        if(ch==null)    return null;    // offline
        return new FilePath(ch,absolutePath);
    }

441 442 443 444 445
    public FileSystemProvisioner getFileSystemProvisioner() {
        // TODO: make this configurable or auto-detectable or something else
        return FileSystemProvisioner.DEFAULT;
    }

446 447 448
    /**
     * Gets the {@link NodeProperty} instances configured for this {@link Node}.
     */
449
    public abstract @Nonnull DescribableList<NodeProperty<?>, NodePropertyDescriptor> getNodeProperties();
450

K
kohsuke 已提交
451 452 453 454
    // used in the Jelly script to expose descriptors
    public List<NodePropertyDescriptor> getNodePropertyDescriptors() {
        return NodeProperty.for_(this);
    }
455

456
    public ACL getACL() {
457
        return Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
458
    }
459

460 461 462 463 464 465 466
    public final void checkPermission(Permission permission) {
        getACL().checkPermission(permission);
    }

    public final boolean hasPermission(Permission permission) {
        return getACL().hasPermission(permission);
    }
467

S
Seiji Sogabe 已提交
468
    public Node reconfigure(final StaplerRequest req, JSONObject form) throws FormException {
469 470 471
        if (form==null)     return null;

        final JSONObject jsonForProperties = form.optJSONObject("nodeProperties");
472 473
        final AtomicReference<BindInterceptor> old = new AtomicReference<>();
        old.set(req.setBindListener(new BindInterceptor() {
474 475
            @Override
            public Object onConvert(Type targetType, Class targetTypeErasure, Object jsonSource) {
476 477 478
                if (jsonForProperties != jsonSource) {
                    return old.get().onConvert(targetType, targetTypeErasure, jsonSource);
                }
479 480 481 482 483 484 485 486 487 488 489

                try {
                    DescribableList<NodeProperty<?>, NodePropertyDescriptor> tmp = new DescribableList<NodeProperty<?>, NodePropertyDescriptor>(Saveable.NOOP,getNodeProperties().toList());
                    tmp.rebuild(req, jsonForProperties, NodeProperty.all());
                    return tmp.toList();
                } catch (FormException e) {
                    throw new IllegalArgumentException(e);
                } catch (IOException e) {
                    throw new IllegalArgumentException(e);
                }
            }
490
        }));
491 492 493 494

        try {
            return getDescriptor().newInstance(req, form);
        } finally {
495
            req.setBindListener(old.get());
496
        }
497 498
    }

499
    public abstract NodeDescriptor getDescriptor();
K
kohsuke 已提交
500

K
kohsuke 已提交
501
    /**
502
     * Estimates the clock difference with this agent.
K
kohsuke 已提交
503 504
     *
     * @return
505
     *      always non-null.
K
kohsuke 已提交
506 507 508
     * @throws InterruptedException
     *      if the operation is aborted.
     */
K
Kohsuke Kawaguchi 已提交
509 510 511 512 513 514 515 516 517 518 519 520 521
    public ClockDifference getClockDifference() throws IOException, InterruptedException {
        VirtualChannel channel = getChannel();
        if(channel==null)
            throw new IOException(getNodeName()+" is offline");

        return channel.call(getClockDifferenceCallable());
    }

    /**
     * Returns a {@link Callable} that when run on the channel, estimates the clock difference.
     *
     * @return
     *      always non-null.
522
     * @since 1.522
K
Kohsuke Kawaguchi 已提交
523 524
     */
    public abstract Callable<ClockDifference,IOException> getClockDifferenceCallable();
K
kohsuke 已提交
525

K
kohsuke 已提交
526
    /**
527
     * Constants that control how Hudson allocates jobs to agents.
K
kohsuke 已提交
528
     */
K
kohsuke 已提交
529
    public enum Mode {
S
ssogabe 已提交
530 531
        NORMAL(Messages._Node_Mode_NORMAL()),
        EXCLUSIVE(Messages._Node_Mode_EXCLUSIVE());
K
kohsuke 已提交
532

S
ssogabe 已提交
533
        private final Localizable description;
K
kohsuke 已提交
534 535

        public String getDescription() {
S
ssogabe 已提交
536
            return description.toString();
K
kohsuke 已提交
537 538 539 540 541 542
        }

        public String getName() {
            return name();
        }

S
ssogabe 已提交
543
        Mode(Localizable description) {
K
kohsuke 已提交
544 545
            this.description = description;
        }
K
kohsuke 已提交
546 547

        static {
548
            Stapler.CONVERT_UTILS.register(new EnumConverter(), Mode.class);
549 550
        }
    }
551

K
kohsuke 已提交
552
}