/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts * * 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 hudson.model; import antlr.ANTLRException; import static hudson.Util.fixNull; import hudson.model.labels.LabelAtom; import hudson.model.labels.LabelExpression; import hudson.model.labels.LabelExpression.And; import hudson.model.labels.LabelExpression.Binary; import hudson.model.labels.LabelExpression.Iff; import hudson.model.labels.LabelExpression.Implies; import hudson.model.labels.LabelExpression.Not; import hudson.model.labels.LabelExpression.Or; import hudson.model.labels.LabelExpression.Paren; import hudson.model.labels.LabelExpressionLexer; import hudson.model.labels.LabelExpressionParser; import hudson.model.labels.LabelOperatorPrecedence; import hudson.model.labels.LabelVisitor; import hudson.model.queue.SubTask; import hudson.security.ACL; import hudson.slaves.NodeProvisioner; import hudson.slaves.Cloud; import hudson.util.QuotedStringTokenizer; import hudson.util.VariableResolver; import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithChildren; import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Collection; import java.util.Stack; import java.util.TreeSet; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.HierarchicalStreamReader; /** * Group of {@link Node}s. * * @author Kohsuke Kawaguchi * @see Jenkins#getLabels() * @see Jenkins#getLabel(String) */ @ExportedBean public abstract class Label extends Actionable implements Comparable<Label>, ModelObjectWithChildren { /** * Display name of this label. */ protected transient final String name; private transient volatile Set<Node> nodes; private transient volatile Set<Cloud> clouds; private transient volatile int tiedJobsCount; @Exported public transient final LoadStatistics loadStatistics; public transient final NodeProvisioner nodeProvisioner; public Label(String name) { this.name = name; // passing these causes an infinite loop - getTotalExecutors(),getBusyExecutors()); this.loadStatistics = new LoadStatistics(0,0) { @Override public int computeIdleExecutors() { return Label.this.getIdleExecutors(); } @Override public int computeTotalExecutors() { return Label.this.getTotalExecutors(); } @Override public int computeQueueLength() { return Jenkins.getInstance().getQueue().countBuildableItemsFor(Label.this); } @Override protected Set<Node> getNodes() { return Label.this.getNodes(); } @Override protected boolean matches(Queue.Item item, SubTask subTask) { final Label l = item.getAssignedLabelFor(subTask); return l != null && Label.this.matches(l.name); } }; this.nodeProvisioner = new NodeProvisioner(this, loadStatistics); } /** * Alias for {@link #getDisplayName()}. */ @Exported public final String getName() { return getDisplayName(); } /** * Returns a human-readable text that represents this label. */ public String getDisplayName() { return name; } /** * Returns a label expression that represents this label. */ public abstract String getExpression(); /** * Relative URL from the context path, that ends with '/'. */ public String getUrl() { return "label/"+name+'/'; } public String getSearchUrl() { return getUrl(); } /** * Returns true iff this label is an atom. * * @since 1.580 */ public boolean isAtom() { return false; } /** * Evaluates whether the label expression is true given the specified value assignment. * IOW, returns true if the assignment provided by the resolver matches this label expression. */ public abstract boolean matches(VariableResolver<Boolean> resolver); /** * Evaluates whether the label expression is true when an entity owns the given set of * {@link LabelAtom}s. */ public final boolean matches(final Collection<LabelAtom> labels) { return matches(new VariableResolver<Boolean>() { public Boolean resolve(String name) { for (LabelAtom a : labels) if (a.getName().equals(name)) return true; return false; } }); } public final boolean matches(Node n) { return matches(n.getAssignedLabels()); } /** * Returns true if this label is a "self label", * which means the label is the name of a {@link Node}. */ public boolean isSelfLabel() { Set<Node> nodes = getNodes(); return nodes.size() == 1 && nodes.iterator().next().getSelfLabel() == this; } /** * Gets all {@link Node}s that belong to this label. */ @Exported public Set<Node> getNodes() { Set<Node> nodes = this.nodes; if(nodes!=null) return nodes; Set<Node> r = new HashSet<Node>(); Jenkins h = Jenkins.getInstance(); if(this.matches(h)) r.add(h); for (Node n : h.getNodes()) { if(this.matches(n)) r.add(n); } return this.nodes = Collections.unmodifiableSet(r); } /** * Gets all {@link Cloud}s that can launch for this label. */ @Exported public Set<Cloud> getClouds() { if(clouds==null) { Set<Cloud> r = new HashSet<Cloud>(); Jenkins h = Jenkins.getInstance(); for (Cloud c : h.clouds) { if(c.canProvision(this)) r.add(c); } clouds = Collections.unmodifiableSet(r); } return clouds; } /** * Can jobs be assigned to this label? * <p> * The answer is yes if there is a reasonable basis to believe that Hudson can have * an executor under this label, given the current configuration. This includes * situations such as (1) there are offline agents that have this label (2) clouds exist * that can provision agents that have this label. */ public boolean isAssignable() { for (Node n : getNodes()) if(n.getNumExecutors()>0) return true; return !getClouds().isEmpty(); } /** * Number of total {@link Executor}s that belong to this label. * <p> * This includes executors that belong to offline nodes, so the result * can be thought of as a potential capacity, whereas {@link #getTotalExecutors()} * is the currently functioning total number of executors. * <p> * This method doesn't take the dynamically allocatable nodes (via {@link Cloud}) * into account. If you just want to test if there's some executors, use {@link #isAssignable()}. */ public int getTotalConfiguredExecutors() { int r=0; for (Node n : getNodes()) r += n.getNumExecutors(); return r; } /** * Number of total {@link Executor}s that belong to this label that are functioning. * <p> * This excludes executors that belong to offline nodes. */ @Exported public int getTotalExecutors() { int r=0; for (Node n : getNodes()) { Computer c = n.toComputer(); if(c!=null && c.isOnline()) r += c.countExecutors(); } return r; } /** * Number of busy {@link Executor}s that are carrying out some work right now. */ @Exported public int getBusyExecutors() { int r=0; for (Node n : getNodes()) { Computer c = n.toComputer(); if(c!=null && c.isOnline()) r += c.countBusy(); } return r; } /** * Number of idle {@link Executor}s that can start working immediately. */ @Exported public int getIdleExecutors() { int r=0; for (Node n : getNodes()) { Computer c = n.toComputer(); if(c!=null && (c.isOnline() || c.isConnecting()) && c.isAcceptingTasks()) r += c.countIdle(); } return r; } /** * Returns true if all the nodes of this label is offline. */ @Exported public boolean isOffline() { for (Node n : getNodes()) { Computer c = n.toComputer(); if(c != null && !c.isOffline()) return false; } return true; } /** * Returns a human readable text that explains this label. */ @Exported public String getDescription() { Set<Node> nodes = getNodes(); if(nodes.isEmpty()) { Set<Cloud> clouds = getClouds(); if(clouds.isEmpty()) return Messages.Label_InvalidLabel(); return Messages.Label_ProvisionedFrom(toString(clouds)); } if(nodes.size()==1) return nodes.iterator().next().getNodeDescription(); return Messages.Label_GroupOf(toString(nodes)); } private String toString(Collection<? extends ModelObject> model) { boolean first=true; StringBuilder buf = new StringBuilder(); for (ModelObject c : model) { if(buf.length()>80) { buf.append(",..."); break; } if(!first) buf.append(','); else first=false; buf.append(c.getDisplayName()); } return buf.toString(); } /** * Returns projects that are tied on this node. */ @Exported public List<AbstractProject> getTiedJobs() { List<AbstractProject> r = new ArrayList<AbstractProject>(); for (AbstractProject<?,?> p : Jenkins.getInstance().getAllItems(AbstractProject.class)) { if(p instanceof TopLevelItem && this.equals(p.getAssignedLabel())) r.add(p); } return r; } /** * Returns an approximate count of projects that are tied on this node. * * In a system without security this should be the same * as {@code getTiedJobs().size()} but significantly faster as it involves fewer temporary objects and avoids * sorting the intermediary list. In a system with security, this will likely return a higher value as it counts * all jobs (mostly) irrespective of access. * @return a count of projects that are tied on this node. */ public int getTiedJobCount() { if (tiedJobsCount != -1) return tiedJobsCount; // denormalize for performance // we don't need to respect security as much when returning a simple count SecurityContext context = ACL.impersonate(ACL.SYSTEM); try { int result = 0; // top level gives the map without checking security of items in the map // therefore best performance for (TopLevelItem topLevelItem : Jenkins.getInstance().getItemMap().values()) { if (topLevelItem instanceof AbstractProject) { final AbstractProject project = (AbstractProject) topLevelItem; if (matches(project.getAssignedLabelString())) { result++; } } if (topLevelItem instanceof ItemGroup) { Stack<ItemGroup> q = new Stack<ItemGroup>(); q.push((ItemGroup) topLevelItem); while (!q.isEmpty()) { ItemGroup<?> parent = q.pop(); // we run the risk of permissions checks in ItemGroup#getItems() // not much we can do here though for (Item i : parent.getItems()) { if (i instanceof AbstractProject) { final AbstractProject project = (AbstractProject) i; if (matches(project.getAssignedLabelString())) { result++; } } if (i instanceof ItemGroup) { q.push((ItemGroup) i); } } } } } return tiedJobsCount = result; } finally { SecurityContextHolder.setContext(context); } } public boolean contains(Node node) { return getNodes().contains(node); } /** * If there's no such label defined in {@link Node} or {@link Cloud}. * This is usually used as a signal that this label is invalid. */ public boolean isEmpty() { return getNodes().isEmpty() && getClouds().isEmpty(); } /*package*/ void reset() { nodes = null; clouds = null; tiedJobsCount = -1; } /** * Expose this object to the remote API. */ public Api getApi() { return new Api(this); } /** * Accepts a visitor and call its respective "onXYZ" method based no the actual type of 'this'. */ public abstract <V,P> V accept(LabelVisitor<V,P> visitor, P param); /** * Lists up all the atoms contained in in this label. * * @since 1.420 */ public Set<LabelAtom> listAtoms() { Set<LabelAtom> r = new HashSet<LabelAtom>(); accept(ATOM_COLLECTOR,r); return r; } /** * Returns the label that represents "this&rhs" */ public Label and(Label rhs) { return new LabelExpression.And(this,rhs); } /** * Returns the label that represents "this|rhs" */ public Label or(Label rhs) { return new LabelExpression.Or(this,rhs); } /** * Returns the label that represents "this<->rhs" */ public Label iff(Label rhs) { return new LabelExpression.Iff(this,rhs); } /** * Returns the label that represents "this->rhs" */ public Label implies(Label rhs) { return new LabelExpression.Implies(this,rhs); } /** * Returns the label that represents "!this" */ public Label not() { return new LabelExpression.Not(this); } /** * Returns the label that represents "(this)" * This is a pointless operation for machines, but useful * for humans who find the additional parenthesis often useful */ public Label paren() { return new LabelExpression.Paren(this); } /** * Precedence of the top most operator. */ public abstract LabelOperatorPrecedence precedence(); @Override public final boolean equals(Object that) { if (this == that) return true; if (that == null || getClass() != that.getClass()) return false; return matches(((Label)that).name); } @Override public final int hashCode() { return name.hashCode(); } public final int compareTo(Label that) { return this.name.compareTo(that.name); } /** * Evaluates whether the current label name is equal to the name parameter. * */ private final boolean matches(String name) { return this.name.equals(name); } @Override public String toString() { return name; } public ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { ContextMenu menu = new ContextMenu(); for (Node node : getNodes()) { menu.add(node); } return menu; } public static final class ConverterImpl implements Converter { public ConverterImpl() { } public boolean canConvert(Class type) { return Label.class.isAssignableFrom(type); } public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { Label src = (Label) source; writer.setValue(src.getExpression()); } public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) { return Jenkins.getInstance().getLabel(reader.getValue()); } } /** * Convers a whitespace-separate list of tokens into a set of {@link Label}s. * * @param labels * Strings like "abc def ghi". Can be empty or null. * @return * Can be empty but never null. A new writable set is always returned, * so that the caller can add more to the set. * @since 1.308 */ public static Set<LabelAtom> parse(String labels) { Set<LabelAtom> r = new TreeSet<LabelAtom>(); labels = fixNull(labels); if(labels.length()>0) for( String l : new QuotedStringTokenizer(labels).toArray()) r.add(Jenkins.getInstance().getLabelAtom(l)); return r; } /** * Obtains a label by its {@linkplain #getName() name}. */ public static Label get(String l) { return Jenkins.getInstance().getLabel(l); } /** * Parses the expression into a label expression tree. * * TODO: replace this with a real parser later */ public static Label parseExpression(String labelExpression) throws ANTLRException { LabelExpressionLexer lexer = new LabelExpressionLexer(new StringReader(labelExpression)); return new LabelExpressionParser(lexer).expr(); } /** * Collects all the atoms in the expression. */ private static final LabelVisitor<Void,Set<LabelAtom>> ATOM_COLLECTOR = new LabelVisitor<Void,Set<LabelAtom>>() { @Override public Void onAtom(LabelAtom a, Set<LabelAtom> param) { param.add(a); return null; } @Override public Void onParen(Paren p, Set<LabelAtom> param) { return p.base.accept(this,param); } @Override public Void onNot(Not p, Set<LabelAtom> param) { return p.base.accept(this,param); } @Override public Void onAnd(And p, Set<LabelAtom> param) { return onBinary(p,param); } @Override public Void onOr(Or p, Set<LabelAtom> param) { return onBinary(p,param); } @Override public Void onIff(Iff p, Set<LabelAtom> param) { return onBinary(p,param); } @Override public Void onImplies(Implies p, Set<LabelAtom> param) { return onBinary(p,param); } private Void onBinary(Binary b, Set<LabelAtom> param) { b.lhs.accept(this,param); b.rhs.accept(this,param); return null; } }; }