blob: 1595f9644fafd5677caacb62dd053d424009bb33 [file] [log] [blame]
/*
* Copyright 2015-present Open Networking Laboratory
*
* 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.onlab.stc;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.onlab.graph.DepthFirstSearch;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.google.common.base.Preconditions.*;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.Integer.parseInt;
import static org.onlab.graph.DepthFirstSearch.EdgeType.BACK_EDGE;
import static org.onlab.graph.GraphPathSearch.ALL_PATHS;
import static org.onlab.stc.Scenario.loadScenario;
/**
* Entity responsible for loading a scenario and producing a redy-to-execute
* process flow graph.
*/
public class Compiler {
private static final String DEFAULT_LOG_DIR = "${WORKSPACE}/tmp/stc/";
private static final String IMPORT = "import";
private static final String GROUP = "group";
private static final String STEP = "step";
private static final String PARALLEL = "parallel";
private static final String SEQUENTIAL = "sequential";
private static final String DEPENDENCY = "dependency";
private static final String LOG_DIR = "[@logDir]";
private static final String NAME = "[@name]";
private static final String COMMAND = "[@exec]";
private static final String ENV = "[@env]";
private static final String CWD = "[@cwd]";
private static final String DELAY = "[@delay]";
private static final String REQUIRES = "[@requires]";
private static final String IF = "[@if]";
private static final String UNLESS = "[@unless]";
private static final String VAR = "[@var]";
private static final String STARTS = "[@starts]";
private static final String ENDS = "[@ends]";
private static final String FILE = "[@file]";
private static final String NAMESPACE = "[@namespace]";
static final String PROP_START = "${";
static final String PROP_END = "}";
private static final String HASH = "#";
private static final String HASH_PREV = "#-1";
private final Scenario scenario;
private final Map<String, Step> steps = Maps.newHashMap();
private final Map<String, Step> inactiveSteps = Maps.newHashMap();
private final Map<String, String> requirements = Maps.newHashMap();
private final Set<Dependency> dependencies = Sets.newHashSet();
private final List<Integer> clonables = Lists.newArrayList();
private ProcessFlow processFlow;
private File logDir;
private String previous = null;
private String pfx = "";
private boolean debugOn = System.getenv("debug") != null;
/**
* Creates a new compiler for the specified scenario.
*
* @param scenario scenario to be compiled
*/
public Compiler(Scenario scenario) {
this.scenario = scenario;
}
/**
* Returns the scenario being compiled.
*
* @return test scenario
*/
public Scenario scenario() {
return scenario;
}
/**
* Compiles the specified scenario to produce a final process flow graph.
*/
public void compile() {
compile(scenario.definition(), null, null);
compileRequirements();
// Produce the process flow
processFlow = new ProcessFlow(ImmutableSet.copyOf(steps.values()),
ImmutableSet.copyOf(dependencies));
scanForCycles();
// Extract the log directory if there was one specified
String defaultPath = DEFAULT_LOG_DIR + scenario.name();
String path = scenario.definition().getString(LOG_DIR, defaultPath);
logDir = new File(expand(path));
}
/**
* Returns the step with the specified name.
*
* @param name step or group name
* @return test step or group
*/
public Step getStep(String name) {
return steps.get(name);
}
/**
* Returns the process flow generated from this scenario definition.
*
* @return process flow as a graph
*/
public ProcessFlow processFlow() {
return processFlow;
}
/**
* Returns the log directory where scenario logs should be kept.
*
* @return scenario logs directory
*/
public File logDir() {
return logDir;
}
/**
* Recursively elaborates this definition to produce a final process flow graph.
*
* @param cfg hierarchical definition
* @param namespace optional namespace
* @param parentGroup optional parent group
*/
private void compile(HierarchicalConfiguration cfg,
String namespace, Group parentGroup) {
String opfx = pfx;
pfx = pfx + ">";
print("pfx=%s namespace=%s", pfx, namespace);
// Scan all imports
cfg.configurationsAt(IMPORT)
.forEach(c -> processImport(c, namespace, parentGroup));
// Scan all steps
cfg.configurationsAt(STEP)
.forEach(c -> processStep(c, namespace, parentGroup));
// Scan all groups
cfg.configurationsAt(GROUP)
.forEach(c -> processGroup(c, namespace, parentGroup));
// Scan all parallel groups
cfg.configurationsAt(PARALLEL)
.forEach(c -> processParallelGroup(c, namespace, parentGroup));
// Scan all sequential groups
cfg.configurationsAt(SEQUENTIAL)
.forEach(c -> processSequentialGroup(c, namespace, parentGroup));
// Scan all dependencies
cfg.configurationsAt(DEPENDENCY)
.forEach(c -> processDependency(c, namespace));
pfx = opfx;
}
/**
* Compiles requirements for all steps and groups accrued during the
* overall compilation process.
*/
private void compileRequirements() {
requirements.forEach((name, requires) ->
compileRequirements(getStep(name), requires));
}
private void compileRequirements(Step src, String requires) {
split(requires).forEach(n -> {
boolean isSoft = n.startsWith("~");
String name = n.replaceFirst("^~", "");
Step dst = getStep(name);
if (dst != null) {
dependencies.add(new Dependency(src, dst, isSoft));
}
});
}
/**
* Processes an import directive.
*
* @param cfg hierarchical definition
* @param namespace optional namespace
* @param parentGroup optional parent group
*/
private void processImport(HierarchicalConfiguration cfg,
String namespace, Group parentGroup) {
String file = checkNotNull(expand(cfg.getString(FILE)),
"Import directive must specify 'file'");
String newNamespace = expand(prefix(cfg.getString(NAMESPACE), namespace));
print("import file=%s namespace=%s", file, newNamespace);
try {
Scenario importScenario = loadScenario(new FileInputStream(file));
compile(importScenario.definition(), newNamespace, parentGroup);
} catch (IOException e) {
throw new IllegalArgumentException("Unable to import scenario", e);
}
}
/**
* Processes a step directive.
*
* @param cfg hierarchical definition
* @param namespace optional namespace
* @param parentGroup optional parent group
*/
private void processStep(HierarchicalConfiguration cfg,
String namespace, Group parentGroup) {
String name = expand(prefix(cfg.getString(NAME), namespace));
String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
int delay = parseInt(expand(cfg.getString(DELAY, parentGroup != null ? "" + parentGroup.delay() : "0")));
print("step name=%s command=%s env=%s cwd=%s delay=%d", name, command, env, cwd, delay);
Step step = new Step(name, command, env, cwd, parentGroup, delay);
registerStep(step, cfg, namespace, parentGroup);
}
/**
* Processes a group directive.
*
* @param cfg hierarchical definition
* @param namespace optional namespace
* @param parentGroup optional parent group
*/
private void processGroup(HierarchicalConfiguration cfg,
String namespace, Group parentGroup) {
String name = expand(prefix(cfg.getString(NAME), namespace));
String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
int delay = parseInt(expand(cfg.getString(DELAY, parentGroup != null ? "" + parentGroup.delay() : "0")));
print("group name=%s command=%s env=%s cwd=%s delay=%d", name, command, env, cwd, delay);
Group group = new Group(name, command, env, cwd, parentGroup, delay);
if (registerStep(group, cfg, namespace, parentGroup)) {
compile(cfg, namespace, group);
}
}
/**
* Registers the specified step or group.
*
* @param step step or group
* @param cfg hierarchical definition
* @param namespace optional namespace
* @param parentGroup optional parent group
* @return true of the step or group was registered as active
*/
private boolean registerStep(Step step, HierarchicalConfiguration cfg,
String namespace, Group parentGroup) {
checkState(!steps.containsKey(step.name()), "Step %s already exists", step.name());
String ifClause = expand(cfg.getString(IF));
String unlessClause = expand(cfg.getString(UNLESS));
if ((ifClause != null && ifClause.length() == 0) ||
(unlessClause != null && unlessClause.length() > 0) ||
(parentGroup != null && inactiveSteps.containsValue(parentGroup))) {
inactiveSteps.put(step.name(), step);
return false;
}
if (parentGroup != null) {
parentGroup.addChild(step);
}
steps.put(step.name(), step);
processRequirements(step, expand(cfg.getString(REQUIRES)), namespace);
previous = step.name();
return true;
}
/**
* Processes a parallel clone group directive.
*
* @param cfg hierarchical definition
* @param namespace optional namespace
* @param parentGroup optional parent group
*/
private void processParallelGroup(HierarchicalConfiguration cfg,
String namespace, Group parentGroup) {
String var = cfg.getString(VAR);
print("parallel var=%s", var);
int i = 1;
while (condition(var, i).length() > 0) {
clonables.add(0, i);
compile(cfg, namespace, parentGroup);
clonables.remove(0);
i++;
}
}
/**
* Processes a sequential clone group directive.
*
* @param cfg hierarchical definition
* @param namespace optional namespace
* @param parentGroup optional parent group
*/
private void processSequentialGroup(HierarchicalConfiguration cfg,
String namespace, Group parentGroup) {
String var = cfg.getString(VAR);
String starts = cfg.getString(STARTS);
String ends = cfg.getString(ENDS);
print("sequential var=%s", var);
int i = 1;
while (condition(var, i).length() > 0) {
clonables.add(0, i);
compile(cfg, namespace, parentGroup);
if (i > 1) {
processSequentialRequirements(starts, ends, namespace);
}
clonables.remove(0);
i++;
}
}
/**
* Hooks starts of this sequence tier to the previous tier.
*
* @param starts comma-separated list of start steps
* @param ends comma-separated list of end steps
* @param namespace optional namespace
*/
private void processSequentialRequirements(String starts, String ends,
String namespace) {
for (String s : split(starts)) {
String start = expand(prefix(s, namespace));
String reqs = requirements.get(s);
for (String n : split(ends)) {
boolean isSoft = n.startsWith("~");
String name = n.replaceFirst("^~", "");
name = (isSoft ? "~" : "") + expand(prefix(name, namespace));
reqs = reqs == null ? name : (reqs + "," + name);
}
requirements.put(start, reqs);
}
}
/**
* Returns the elaborated repetition construct conditional.
*
* @param var repetition var property
* @param i index to elaborate
* @return elaborated string
*/
private String condition(String var, Integer i) {
return expand(var.replaceFirst("#", i.toString())).trim();
}
/**
* Processes a dependency directive.
*
* @param cfg hierarchical definition
* @param namespace optional namespace
*/
private void processDependency(HierarchicalConfiguration cfg, String namespace) {
String name = expand(prefix(cfg.getString(NAME), namespace));
String requires = expand(cfg.getString(REQUIRES));
print("dependency name=%s requires=%s", name, requires);
Step step = getStep(name, namespace);
if (!inactiveSteps.containsValue(step)) {
processRequirements(step, requires, namespace);
}
}
/**
* Processes the specified requiremenst string and adds dependency for
* each requirement of the given step.
*
* @param src source step
* @param requires comma-separated list of required steps
* @param namespace optional namespace
*/
private void processRequirements(Step src, String requires, String namespace) {
String reqs = requirements.get(src.name());
for (String n : split(requires)) {
boolean isSoft = n.startsWith("~");
String name = n.replaceFirst("^~", "");
name = previous != null && name.equals("^") ? previous : name;
name = (isSoft ? "~" : "") + expand(prefix(name, namespace));
reqs = reqs == null ? name : (reqs + "," + name);
}
requirements.put(src.name(), reqs);
}
/**
* Retrieves the step or group with the specified name.
*
* @param name step or group name
* @param namespace optional namespace
* @return step or group; null if none found in active or inactive steps
*/
private Step getStep(String name, String namespace) {
String dName = prefix(name, namespace);
Step step = steps.get(dName);
step = step != null ? step : inactiveSteps.get(dName);
checkArgument(step != null, "Unknown step %s", dName);
return step;
}
/**
* Prefixes the specified name with the given namespace.
*
* @param name name of a step or a group
* @param namespace optional namespace
* @return composite name
*/
private String prefix(String name, String namespace) {
return isNullOrEmpty(namespace) ? name : namespace + "." + name;
}
/**
* Expands any environment variables in the specified string. These are
* specified as ${property} tokens.
*
* @param string string to be processed
* @param keepTokens true if the original unresolved tokens should be kept
* @return original string with expanded substitutions
*/
private String expand(String string, boolean... keepTokens) {
if (string == null) {
return null;
}
String pString = string;
StringBuilder sb = new StringBuilder();
int start, end, last = 0;
while ((start = pString.indexOf(PROP_START, last)) >= 0) {
end = pString.indexOf(PROP_END, start + PROP_START.length());
checkArgument(end > start, "Malformed property in %s", pString);
sb.append(pString.substring(last, start));
String prop = pString.substring(start + PROP_START.length(), end);
String value;
if (prop.equals(HASH)) {
value = Integer.toString(clonables.get(0));
} else if (prop.equals(HASH_PREV)) {
value = Integer.toString(clonables.get(0) - 1);
} else if (prop.endsWith(HASH)) {
pString = pString.replaceFirst("#}", clonables.get(0) + "}");
last = start;
continue;
} else {
// Try system property first, then fall back to env. variable.
value = System.getProperty(prop);
if (value == null) {
value = System.getenv(prop);
}
}
if (value == null && keepTokens.length == 1 && keepTokens[0]) {
sb.append("${").append(prop).append("}");
} else {
sb.append(value != null ? value : "");
}
last = end + 1;
}
sb.append(pString.substring(last));
return sb.toString().replace('\n', ' ').replace('\r', ' ');
}
/**
* Splits the comma-separated string into a list of strings.
*
* @param string string to split
* @return list of strings
*/
private List<String> split(String string) {
ImmutableList.Builder<String> builder = ImmutableList.builder();
String[] fields = string != null ? string.split(",") : new String[0];
for (String field : fields) {
builder.add(field.trim());
}
return builder.build();
}
/**
* Scans the process flow graph for cyclic dependencies.
*/
private void scanForCycles() {
DepthFirstSearch<Step, Dependency> dfs = new DepthFirstSearch<>();
// Use a brute-force method of searching paths from all vertices.
processFlow().getVertexes().forEach(s -> {
DepthFirstSearch<Step, Dependency>.SpanningTreeResult r =
dfs.search(processFlow, s, null, null, ALL_PATHS);
r.edges().forEach((e, et) -> checkArgument(et != BACK_EDGE,
"Process flow has a cycle involving dependency from %s to %s",
e.src().name, e.dst().name));
});
}
/**
* Prints formatted output.
*
* @param format printf format string
* @param args arguments to be printed
*/
private void print(String format, Object... args) {
if (debugOn) {
System.err.println(pfx + String.format(format, args));
}
}
}