blob: add71eb52a64c643f3309f5e1630ab2daaed17c9 [file] [log] [blame]
Thomas Vachuskaf9c84362015-04-15 11:20:45 -07001/*
2 * Copyright 2015 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.onlab.stc;
17
18import com.google.common.collect.ImmutableList;
19import com.google.common.collect.ImmutableSet;
20import com.google.common.collect.Lists;
21import com.google.common.collect.Maps;
22import com.google.common.collect.Sets;
23import org.apache.commons.configuration.HierarchicalConfiguration;
Thomas Vachuska1c152872015-08-26 18:41:10 -070024import org.onlab.graph.DepthFirstSearch;
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070025
26import java.io.File;
27import java.io.FileInputStream;
28import java.io.IOException;
29import java.util.List;
30import java.util.Map;
31import java.util.Set;
32
Thomas Vachuska1c152872015-08-26 18:41:10 -070033import static com.google.common.base.Preconditions.*;
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -070034import static com.google.common.base.Strings.isNullOrEmpty;
Thomas Vachuska1c152872015-08-26 18:41:10 -070035import static org.onlab.graph.DepthFirstSearch.EdgeType.BACK_EDGE;
36import static org.onlab.graph.GraphPathSearch.ALL_PATHS;
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070037import static org.onlab.stc.Scenario.loadScenario;
38
39/**
40 * Entity responsible for loading a scenario and producing a redy-to-execute
41 * process flow graph.
42 */
43public class Compiler {
44
Thomas Vachuska229dced2015-10-14 14:21:13 -070045 private static final String DEFAULT_LOG_DIR = "${WORKSPACE}/tmp/stc/";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070046
47 private static final String IMPORT = "import";
48 private static final String GROUP = "group";
49 private static final String STEP = "step";
50 private static final String PARALLEL = "parallel";
51 private static final String DEPENDENCY = "dependency";
52
53 private static final String LOG_DIR = "[@logDir]";
54 private static final String NAME = "[@name]";
55 private static final String COMMAND = "[@exec]";
Thomas Vachuska4bfccd542015-05-30 00:35:25 -070056 private static final String ENV = "[@env]";
57 private static final String CWD = "[@cwd]";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070058 private static final String REQUIRES = "[@requires]";
59 private static final String IF = "[@if]";
60 private static final String UNLESS = "[@unless]";
61 private static final String VAR = "[@var]";
62 private static final String FILE = "[@file]";
63 private static final String NAMESPACE = "[@namespace]";
64
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -070065 static final String PROP_START = "${";
66 static final String PROP_END = "}";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070067 private static final String HASH = "#";
68
69 private final Scenario scenario;
70
71 private final Map<String, Step> steps = Maps.newHashMap();
72 private final Map<String, Step> inactiveSteps = Maps.newHashMap();
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -070073 private final Map<String, String> requirements = Maps.newHashMap();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070074 private final Set<Dependency> dependencies = Sets.newHashSet();
75 private final List<Integer> parallels = Lists.newArrayList();
76
77 private ProcessFlow processFlow;
78 private File logDir;
79
Thomas Vachuska18571b02015-05-31 22:30:06 -070080 private String previous = null;
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070081 private String pfx = "";
82 private boolean debugOn = System.getenv("debug") != null;
83
84 /**
85 * Creates a new compiler for the specified scenario.
86 *
87 * @param scenario scenario to be compiled
88 */
89 public Compiler(Scenario scenario) {
90 this.scenario = scenario;
91 }
92
93 /**
94 * Returns the scenario being compiled.
95 *
96 * @return test scenario
97 */
98 public Scenario scenario() {
99 return scenario;
100 }
101
102 /**
103 * Compiles the specified scenario to produce a final process flow graph.
104 */
105 public void compile() {
106 compile(scenario.definition(), null, null);
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700107 compileRequirements();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700108
109 // Produce the process flow
110 processFlow = new ProcessFlow(ImmutableSet.copyOf(steps.values()),
111 ImmutableSet.copyOf(dependencies));
112
Thomas Vachuska1c152872015-08-26 18:41:10 -0700113 scanForCycles();
114
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700115 // Extract the log directory if there was one specified
Thomas Vachuskaa48e3d12015-06-02 09:43:29 -0700116 String defaultPath = DEFAULT_LOG_DIR + scenario.name();
117 String path = scenario.definition().getString(LOG_DIR, defaultPath);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700118 logDir = new File(expand(path));
119 }
120
121 /**
122 * Returns the step with the specified name.
123 *
124 * @param name step or group name
125 * @return test step or group
126 */
127 public Step getStep(String name) {
128 return steps.get(name);
129 }
130
131 /**
132 * Returns the process flow generated from this scenario definition.
133 *
134 * @return process flow as a graph
135 */
136 public ProcessFlow processFlow() {
137 return processFlow;
138 }
139
140 /**
141 * Returns the log directory where scenario logs should be kept.
Thomas Vachuska18571b02015-05-31 22:30:06 -0700142 *
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700143 * @return scenario logs directory
144 */
145 public File logDir() {
146 return logDir;
147 }
148
149 /**
150 * Recursively elaborates this definition to produce a final process flow graph.
151 *
152 * @param cfg hierarchical definition
153 * @param namespace optional namespace
154 * @param parentGroup optional parent group
155 */
156 private void compile(HierarchicalConfiguration cfg,
157 String namespace, Group parentGroup) {
158 String opfx = pfx;
159 pfx = pfx + ">";
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700160 print("pfx=%s namespace=%s", pfx, namespace);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700161
162 // Scan all imports
163 cfg.configurationsAt(IMPORT)
164 .forEach(c -> processImport(c, namespace, parentGroup));
165
166 // Scan all steps
167 cfg.configurationsAt(STEP)
168 .forEach(c -> processStep(c, namespace, parentGroup));
169
170 // Scan all groups
171 cfg.configurationsAt(GROUP)
172 .forEach(c -> processGroup(c, namespace, parentGroup));
173
174 // Scan all parallel groups
175 cfg.configurationsAt(PARALLEL)
176 .forEach(c -> processParallelGroup(c, namespace, parentGroup));
177
178 // Scan all dependencies
179 cfg.configurationsAt(DEPENDENCY)
180 .forEach(c -> processDependency(c, namespace));
181
182 pfx = opfx;
183 }
184
185 /**
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700186 * Compiles requirements for all steps and groups accrued during the
187 * overall compilation process.
188 */
189 private void compileRequirements() {
190 requirements.forEach((name, requires) ->
191 compileRequirements(getStep(name), requires));
192 }
193
194 private void compileRequirements(Step src, String requires) {
195 split(requires).forEach(n -> {
196 boolean isSoft = n.startsWith("~");
197 String name = n.replaceFirst("^~", "");
198 Step dst = getStep(name);
199 if (dst != null) {
200 dependencies.add(new Dependency(src, dst, isSoft));
201 }
202 });
203 }
204
205 /**
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700206 * Processes an import directive.
207 *
208 * @param cfg hierarchical definition
209 * @param namespace optional namespace
210 * @param parentGroup optional parent group
211 */
212 private void processImport(HierarchicalConfiguration cfg,
213 String namespace, Group parentGroup) {
214 String file = checkNotNull(expand(cfg.getString(FILE)),
215 "Import directive must specify 'file'");
216 String newNamespace = expand(prefix(cfg.getString(NAMESPACE), namespace));
217 print("import file=%s namespace=%s", file, newNamespace);
218 try {
219 Scenario importScenario = loadScenario(new FileInputStream(file));
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700220 compile(importScenario.definition(), newNamespace, parentGroup);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700221 } catch (IOException e) {
222 throw new IllegalArgumentException("Unable to import scenario", e);
223 }
224 }
225
226 /**
227 * Processes a step directive.
228 *
229 * @param cfg hierarchical definition
230 * @param namespace optional namespace
231 * @param parentGroup optional parent group
232 */
233 private void processStep(HierarchicalConfiguration cfg,
234 String namespace, Group parentGroup) {
235 String name = expand(prefix(cfg.getString(NAME), namespace));
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700236 String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700237 String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
238 String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700239
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700240 print("step name=%s command=%s env=%s cwd=%s", name, command, env, cwd);
241 Step step = new Step(name, command, env, cwd, parentGroup);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700242 registerStep(step, cfg, namespace, parentGroup);
243 }
244
245 /**
246 * Processes a group directive.
247 *
248 * @param cfg hierarchical definition
249 * @param namespace optional namespace
250 * @param parentGroup optional parent group
251 */
252 private void processGroup(HierarchicalConfiguration cfg,
253 String namespace, Group parentGroup) {
254 String name = expand(prefix(cfg.getString(NAME), namespace));
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700255 String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700256 String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
257 String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700258
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700259 print("group name=%s command=%s env=%s cwd=%s", name, command, env, cwd);
260 Group group = new Group(name, command, env, cwd, parentGroup);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700261 if (registerStep(group, cfg, namespace, parentGroup)) {
262 compile(cfg, namespace, group);
263 }
264 }
265
266 /**
267 * Registers the specified step or group.
268 *
269 * @param step step or group
270 * @param cfg hierarchical definition
271 * @param namespace optional namespace
272 * @param parentGroup optional parent group
273 * @return true of the step or group was registered as active
274 */
275 private boolean registerStep(Step step, HierarchicalConfiguration cfg,
276 String namespace, Group parentGroup) {
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700277 checkState(!steps.containsKey(step.name()), "Step %s already exists", step.name());
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700278 String ifClause = expand(cfg.getString(IF));
279 String unlessClause = expand(cfg.getString(UNLESS));
280
281 if ((ifClause != null && ifClause.length() == 0) ||
282 (unlessClause != null && unlessClause.length() > 0) ||
283 (parentGroup != null && inactiveSteps.containsValue(parentGroup))) {
284 inactiveSteps.put(step.name(), step);
285 return false;
286 }
287
288 if (parentGroup != null) {
289 parentGroup.addChild(step);
290 }
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700291
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700292 steps.put(step.name(), step);
293 processRequirements(step, expand(cfg.getString(REQUIRES)), namespace);
Thomas Vachuska18571b02015-05-31 22:30:06 -0700294 previous = step.name();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700295 return true;
296 }
297
298 /**
299 * Processes a parallel clone group directive.
300 *
301 * @param cfg hierarchical definition
302 * @param namespace optional namespace
303 * @param parentGroup optional parent group
304 */
305 private void processParallelGroup(HierarchicalConfiguration cfg,
306 String namespace, Group parentGroup) {
307 String var = cfg.getString(VAR);
308 print("parallel var=%s", var);
309
310 int i = 1;
311 while (condition(var, i).length() > 0) {
312 parallels.add(0, i);
313 compile(cfg, namespace, parentGroup);
314 parallels.remove(0);
315 i++;
316 }
317 }
318
319 /**
320 * Returns the elaborated repetition construct conditional.
321 *
322 * @param var repetition var property
323 * @param i index to elaborate
324 * @return elaborated string
325 */
326 private String condition(String var, Integer i) {
327 return expand(var.replaceFirst("#", i.toString())).trim();
328 }
329
330 /**
331 * Processes a dependency directive.
332 *
333 * @param cfg hierarchical definition
334 * @param namespace optional namespace
335 */
336 private void processDependency(HierarchicalConfiguration cfg, String namespace) {
337 String name = expand(prefix(cfg.getString(NAME), namespace));
338 String requires = expand(cfg.getString(REQUIRES));
339
340 print("dependency name=%s requires=%s", name, requires);
341 Step step = getStep(name, namespace);
Thomas Vachuska177ece62015-06-26 00:18:21 -0700342 if (!inactiveSteps.containsValue(step)) {
343 processRequirements(step, requires, namespace);
344 }
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700345 }
346
347 /**
348 * Processes the specified requiremenst string and adds dependency for
349 * each requirement of the given step.
350 *
351 * @param src source step
352 * @param requires comma-separated list of required steps
353 * @param namespace optional namespace
354 */
355 private void processRequirements(Step src, String requires, String namespace) {
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700356 String reqs = requirements.get(src.name());
357 for (String n : split(requires)) {
Thomas Vachuska18571b02015-05-31 22:30:06 -0700358 boolean isSoft = n.startsWith("~");
359 String name = n.replaceFirst("^~", "");
360 name = previous != null && name.equals("^") ? previous : name;
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700361 name = (isSoft ? "~" : "") + expand(prefix(name, namespace));
362 reqs = reqs == null ? name : (reqs + "," + name);
363 }
364 requirements.put(src.name(), reqs);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700365 }
366
367 /**
368 * Retrieves the step or group with the specified name.
369 *
370 * @param name step or group name
371 * @param namespace optional namespace
372 * @return step or group; null if none found in active or inactive steps
373 */
374 private Step getStep(String name, String namespace) {
375 String dName = prefix(name, namespace);
376 Step step = steps.get(dName);
377 step = step != null ? step : inactiveSteps.get(dName);
378 checkArgument(step != null, "Unknown step %s", dName);
379 return step;
380 }
381
382 /**
383 * Prefixes the specified name with the given namespace.
384 *
385 * @param name name of a step or a group
386 * @param namespace optional namespace
387 * @return composite name
388 */
389 private String prefix(String name, String namespace) {
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700390 return isNullOrEmpty(namespace) ? name : namespace + "." + name;
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700391 }
392
393 /**
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700394 * Expands any environment variables in the specified string. These are
395 * specified as ${property} tokens.
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700396 *
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700397 * @param string string to be processed
398 * @param keepTokens true if the original unresolved tokens should be kept
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700399 * @return original string with expanded substitutions
400 */
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700401 private String expand(String string, boolean... keepTokens) {
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700402 if (string == null) {
403 return null;
404 }
405
406 String pString = string;
407 StringBuilder sb = new StringBuilder();
408 int start, end, last = 0;
409 while ((start = pString.indexOf(PROP_START, last)) >= 0) {
410 end = pString.indexOf(PROP_END, start + PROP_START.length());
411 checkArgument(end > start, "Malformed property in %s", pString);
412 sb.append(pString.substring(last, start));
413 String prop = pString.substring(start + PROP_START.length(), end);
414 String value;
415 if (prop.equals(HASH)) {
416 value = parallels.get(0).toString();
417 } else if (prop.endsWith(HASH)) {
418 pString = pString.replaceFirst("#}", parallels.get(0).toString() + "}");
419 last = start;
420 continue;
421 } else {
422 // Try system property first, then fall back to env. variable.
423 value = System.getProperty(prop);
424 if (value == null) {
425 value = System.getenv(prop);
426 }
427 }
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700428 if (value == null && keepTokens.length == 1 && keepTokens[0]) {
429 sb.append("${").append(prop).append("}");
430 } else {
431 sb.append(value != null ? value : "");
432 }
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700433 last = end + 1;
434 }
435 sb.append(pString.substring(last));
Thomas Vachuska0ec6ff42015-07-17 11:00:02 -0700436 return sb.toString().replace('\n', ' ').replace('\r', ' ');
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700437 }
438
439 /**
440 * Splits the comma-separated string into a list of strings.
441 *
442 * @param string string to split
443 * @return list of strings
444 */
445 private List<String> split(String string) {
446 ImmutableList.Builder<String> builder = ImmutableList.builder();
447 String[] fields = string != null ? string.split(",") : new String[0];
448 for (String field : fields) {
449 builder.add(field.trim());
450 }
451 return builder.build();
452 }
453
454 /**
Thomas Vachuska1c152872015-08-26 18:41:10 -0700455 * Scans the process flow graph for cyclic dependencies.
456 */
457 private void scanForCycles() {
458 DepthFirstSearch<Step, Dependency> dfs = new DepthFirstSearch<>();
459 // Use a brute-force method of searching paths from all vertices.
460 processFlow().getVertexes().forEach(s -> {
461 DepthFirstSearch<Step, Dependency>.SpanningTreeResult r =
462 dfs.search(processFlow, s, null, null, ALL_PATHS);
463 r.edges().forEach((e, et) -> checkArgument(et != BACK_EDGE,
464 "Process flow has a cycle involving dependency from %s to %s",
465 e.src().name, e.dst().name));
466 });
467 }
468
469
470 /**
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700471 * Prints formatted output.
472 *
473 * @param format printf format string
474 * @param args arguments to be printed
475 */
476 private void print(String format, Object... args) {
477 if (debugOn) {
478 System.err.println(pfx + String.format(format, args));
479 }
480 }
481
482}