blob: 919cbd5bdde32cdd6fcaa37c4eacd08565f5c939 [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";
Thomas Vachuska4be30542015-10-21 18:15:52 -070051 private static final String SEQUENTIAL = "sequential";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070052 private static final String DEPENDENCY = "dependency";
53
54 private static final String LOG_DIR = "[@logDir]";
55 private static final String NAME = "[@name]";
56 private static final String COMMAND = "[@exec]";
Thomas Vachuska4bfccd542015-05-30 00:35:25 -070057 private static final String ENV = "[@env]";
58 private static final String CWD = "[@cwd]";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070059 private static final String REQUIRES = "[@requires]";
60 private static final String IF = "[@if]";
61 private static final String UNLESS = "[@unless]";
62 private static final String VAR = "[@var]";
Thomas Vachuska4be30542015-10-21 18:15:52 -070063 private static final String STARTS = "[@starts]";
64 private static final String ENDS = "[@ends]";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070065 private static final String FILE = "[@file]";
66 private static final String NAMESPACE = "[@namespace]";
67
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -070068 static final String PROP_START = "${";
69 static final String PROP_END = "}";
Thomas Vachuska4be30542015-10-21 18:15:52 -070070
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070071 private static final String HASH = "#";
Thomas Vachuska4be30542015-10-21 18:15:52 -070072 private static final String HASH_PREV = "#-1";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070073
74 private final Scenario scenario;
75
76 private final Map<String, Step> steps = Maps.newHashMap();
77 private final Map<String, Step> inactiveSteps = Maps.newHashMap();
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -070078 private final Map<String, String> requirements = Maps.newHashMap();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070079 private final Set<Dependency> dependencies = Sets.newHashSet();
Thomas Vachuska4be30542015-10-21 18:15:52 -070080 private final List<Integer> clonables = Lists.newArrayList();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070081
82 private ProcessFlow processFlow;
83 private File logDir;
84
Thomas Vachuska18571b02015-05-31 22:30:06 -070085 private String previous = null;
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070086 private String pfx = "";
87 private boolean debugOn = System.getenv("debug") != null;
88
89 /**
90 * Creates a new compiler for the specified scenario.
91 *
92 * @param scenario scenario to be compiled
93 */
94 public Compiler(Scenario scenario) {
95 this.scenario = scenario;
96 }
97
98 /**
99 * Returns the scenario being compiled.
100 *
101 * @return test scenario
102 */
103 public Scenario scenario() {
104 return scenario;
105 }
106
107 /**
108 * Compiles the specified scenario to produce a final process flow graph.
109 */
110 public void compile() {
111 compile(scenario.definition(), null, null);
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700112 compileRequirements();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700113
114 // Produce the process flow
115 processFlow = new ProcessFlow(ImmutableSet.copyOf(steps.values()),
116 ImmutableSet.copyOf(dependencies));
117
Thomas Vachuska1c152872015-08-26 18:41:10 -0700118 scanForCycles();
119
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700120 // Extract the log directory if there was one specified
Thomas Vachuskaa48e3d12015-06-02 09:43:29 -0700121 String defaultPath = DEFAULT_LOG_DIR + scenario.name();
122 String path = scenario.definition().getString(LOG_DIR, defaultPath);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700123 logDir = new File(expand(path));
124 }
125
126 /**
127 * Returns the step with the specified name.
128 *
129 * @param name step or group name
130 * @return test step or group
131 */
132 public Step getStep(String name) {
133 return steps.get(name);
134 }
135
136 /**
137 * Returns the process flow generated from this scenario definition.
138 *
139 * @return process flow as a graph
140 */
141 public ProcessFlow processFlow() {
142 return processFlow;
143 }
144
145 /**
146 * Returns the log directory where scenario logs should be kept.
Thomas Vachuska18571b02015-05-31 22:30:06 -0700147 *
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700148 * @return scenario logs directory
149 */
150 public File logDir() {
151 return logDir;
152 }
153
154 /**
155 * Recursively elaborates this definition to produce a final process flow graph.
156 *
157 * @param cfg hierarchical definition
158 * @param namespace optional namespace
159 * @param parentGroup optional parent group
160 */
161 private void compile(HierarchicalConfiguration cfg,
162 String namespace, Group parentGroup) {
163 String opfx = pfx;
164 pfx = pfx + ">";
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700165 print("pfx=%s namespace=%s", pfx, namespace);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700166
167 // Scan all imports
168 cfg.configurationsAt(IMPORT)
169 .forEach(c -> processImport(c, namespace, parentGroup));
170
171 // Scan all steps
172 cfg.configurationsAt(STEP)
173 .forEach(c -> processStep(c, namespace, parentGroup));
174
175 // Scan all groups
176 cfg.configurationsAt(GROUP)
177 .forEach(c -> processGroup(c, namespace, parentGroup));
178
179 // Scan all parallel groups
180 cfg.configurationsAt(PARALLEL)
181 .forEach(c -> processParallelGroup(c, namespace, parentGroup));
182
Thomas Vachuska4be30542015-10-21 18:15:52 -0700183 // Scan all sequential groups
184 cfg.configurationsAt(SEQUENTIAL)
185 .forEach(c -> processSequentialGroup(c, namespace, parentGroup));
186
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700187 // Scan all dependencies
188 cfg.configurationsAt(DEPENDENCY)
189 .forEach(c -> processDependency(c, namespace));
190
191 pfx = opfx;
192 }
193
194 /**
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700195 * Compiles requirements for all steps and groups accrued during the
196 * overall compilation process.
197 */
198 private void compileRequirements() {
199 requirements.forEach((name, requires) ->
200 compileRequirements(getStep(name), requires));
201 }
202
203 private void compileRequirements(Step src, String requires) {
204 split(requires).forEach(n -> {
205 boolean isSoft = n.startsWith("~");
206 String name = n.replaceFirst("^~", "");
207 Step dst = getStep(name);
208 if (dst != null) {
209 dependencies.add(new Dependency(src, dst, isSoft));
210 }
211 });
212 }
213
214 /**
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700215 * Processes an import directive.
216 *
217 * @param cfg hierarchical definition
218 * @param namespace optional namespace
219 * @param parentGroup optional parent group
220 */
221 private void processImport(HierarchicalConfiguration cfg,
222 String namespace, Group parentGroup) {
223 String file = checkNotNull(expand(cfg.getString(FILE)),
224 "Import directive must specify 'file'");
225 String newNamespace = expand(prefix(cfg.getString(NAMESPACE), namespace));
226 print("import file=%s namespace=%s", file, newNamespace);
227 try {
228 Scenario importScenario = loadScenario(new FileInputStream(file));
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700229 compile(importScenario.definition(), newNamespace, parentGroup);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700230 } catch (IOException e) {
231 throw new IllegalArgumentException("Unable to import scenario", e);
232 }
233 }
234
235 /**
236 * Processes a step directive.
237 *
238 * @param cfg hierarchical definition
239 * @param namespace optional namespace
240 * @param parentGroup optional parent group
241 */
242 private void processStep(HierarchicalConfiguration cfg,
243 String namespace, Group parentGroup) {
244 String name = expand(prefix(cfg.getString(NAME), namespace));
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700245 String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700246 String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
247 String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700248
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700249 print("step name=%s command=%s env=%s cwd=%s", name, command, env, cwd);
250 Step step = new Step(name, command, env, cwd, parentGroup);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700251 registerStep(step, cfg, namespace, parentGroup);
252 }
253
254 /**
255 * Processes a group directive.
256 *
257 * @param cfg hierarchical definition
258 * @param namespace optional namespace
259 * @param parentGroup optional parent group
260 */
261 private void processGroup(HierarchicalConfiguration cfg,
262 String namespace, Group parentGroup) {
263 String name = expand(prefix(cfg.getString(NAME), namespace));
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700264 String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700265 String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
266 String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700267
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700268 print("group name=%s command=%s env=%s cwd=%s", name, command, env, cwd);
269 Group group = new Group(name, command, env, cwd, parentGroup);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700270 if (registerStep(group, cfg, namespace, parentGroup)) {
271 compile(cfg, namespace, group);
272 }
273 }
274
275 /**
276 * Registers the specified step or group.
277 *
278 * @param step step or group
279 * @param cfg hierarchical definition
280 * @param namespace optional namespace
281 * @param parentGroup optional parent group
282 * @return true of the step or group was registered as active
283 */
284 private boolean registerStep(Step step, HierarchicalConfiguration cfg,
285 String namespace, Group parentGroup) {
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700286 checkState(!steps.containsKey(step.name()), "Step %s already exists", step.name());
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700287 String ifClause = expand(cfg.getString(IF));
288 String unlessClause = expand(cfg.getString(UNLESS));
289
290 if ((ifClause != null && ifClause.length() == 0) ||
291 (unlessClause != null && unlessClause.length() > 0) ||
292 (parentGroup != null && inactiveSteps.containsValue(parentGroup))) {
293 inactiveSteps.put(step.name(), step);
294 return false;
295 }
296
297 if (parentGroup != null) {
298 parentGroup.addChild(step);
299 }
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700300
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700301 steps.put(step.name(), step);
302 processRequirements(step, expand(cfg.getString(REQUIRES)), namespace);
Thomas Vachuska18571b02015-05-31 22:30:06 -0700303 previous = step.name();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700304 return true;
305 }
306
307 /**
308 * Processes a parallel clone group directive.
309 *
310 * @param cfg hierarchical definition
311 * @param namespace optional namespace
312 * @param parentGroup optional parent group
313 */
314 private void processParallelGroup(HierarchicalConfiguration cfg,
315 String namespace, Group parentGroup) {
316 String var = cfg.getString(VAR);
317 print("parallel var=%s", var);
318
319 int i = 1;
320 while (condition(var, i).length() > 0) {
Thomas Vachuska4be30542015-10-21 18:15:52 -0700321 clonables.add(0, i);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700322 compile(cfg, namespace, parentGroup);
Thomas Vachuska4be30542015-10-21 18:15:52 -0700323 clonables.remove(0);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700324 i++;
325 }
326 }
327
328 /**
Thomas Vachuska4be30542015-10-21 18:15:52 -0700329 * Processes a sequential clone group directive.
330 *
331 * @param cfg hierarchical definition
332 * @param namespace optional namespace
333 * @param parentGroup optional parent group
334 */
335 private void processSequentialGroup(HierarchicalConfiguration cfg,
336 String namespace, Group parentGroup) {
337 String var = cfg.getString(VAR);
338 String starts = cfg.getString(STARTS);
339 String ends = cfg.getString(ENDS);
340 print("sequential var=%s", var);
341
342 int i = 1;
343 while (condition(var, i).length() > 0) {
344 clonables.add(0, i);
345 compile(cfg, namespace, parentGroup);
346 if (i > 1) {
347 processSequentialRequirements(starts, ends, namespace);
348 }
349 clonables.remove(0);
350 i++;
351 }
352 }
353
354 /**
355 * Hooks starts of this sequence tier to the previous tier.
356 *
357 * @param starts comma-separated list of start steps
358 * @param ends comma-separated list of end steps
359 * @param namespace optional namespace
360 */
361 private void processSequentialRequirements(String starts, String ends,
362 String namespace) {
363 for (String s : split(starts)) {
364 String start = expand(prefix(s, namespace));
365 String reqs = requirements.get(s);
366 for (String n : split(ends)) {
367 boolean isSoft = n.startsWith("~");
368 String name = n.replaceFirst("^~", "");
369 name = (isSoft ? "~" : "") + expand(prefix(name, namespace));
370 reqs = reqs == null ? name : (reqs + "," + name);
371 }
372 requirements.put(start, reqs);
373 }
374 }
375
376 /**
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700377 * Returns the elaborated repetition construct conditional.
378 *
379 * @param var repetition var property
380 * @param i index to elaborate
381 * @return elaborated string
382 */
383 private String condition(String var, Integer i) {
384 return expand(var.replaceFirst("#", i.toString())).trim();
385 }
386
387 /**
388 * Processes a dependency directive.
389 *
390 * @param cfg hierarchical definition
391 * @param namespace optional namespace
392 */
393 private void processDependency(HierarchicalConfiguration cfg, String namespace) {
394 String name = expand(prefix(cfg.getString(NAME), namespace));
395 String requires = expand(cfg.getString(REQUIRES));
396
397 print("dependency name=%s requires=%s", name, requires);
398 Step step = getStep(name, namespace);
Thomas Vachuska177ece62015-06-26 00:18:21 -0700399 if (!inactiveSteps.containsValue(step)) {
400 processRequirements(step, requires, namespace);
401 }
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700402 }
403
404 /**
405 * Processes the specified requiremenst string and adds dependency for
406 * each requirement of the given step.
407 *
408 * @param src source step
409 * @param requires comma-separated list of required steps
410 * @param namespace optional namespace
411 */
412 private void processRequirements(Step src, String requires, String namespace) {
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700413 String reqs = requirements.get(src.name());
414 for (String n : split(requires)) {
Thomas Vachuska18571b02015-05-31 22:30:06 -0700415 boolean isSoft = n.startsWith("~");
416 String name = n.replaceFirst("^~", "");
417 name = previous != null && name.equals("^") ? previous : name;
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700418 name = (isSoft ? "~" : "") + expand(prefix(name, namespace));
419 reqs = reqs == null ? name : (reqs + "," + name);
420 }
421 requirements.put(src.name(), reqs);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700422 }
423
424 /**
425 * Retrieves the step or group with the specified name.
426 *
427 * @param name step or group name
428 * @param namespace optional namespace
429 * @return step or group; null if none found in active or inactive steps
430 */
431 private Step getStep(String name, String namespace) {
432 String dName = prefix(name, namespace);
433 Step step = steps.get(dName);
434 step = step != null ? step : inactiveSteps.get(dName);
435 checkArgument(step != null, "Unknown step %s", dName);
436 return step;
437 }
438
439 /**
440 * Prefixes the specified name with the given namespace.
441 *
442 * @param name name of a step or a group
443 * @param namespace optional namespace
444 * @return composite name
445 */
446 private String prefix(String name, String namespace) {
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700447 return isNullOrEmpty(namespace) ? name : namespace + "." + name;
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700448 }
449
450 /**
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700451 * Expands any environment variables in the specified string. These are
452 * specified as ${property} tokens.
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700453 *
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700454 * @param string string to be processed
455 * @param keepTokens true if the original unresolved tokens should be kept
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700456 * @return original string with expanded substitutions
457 */
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700458 private String expand(String string, boolean... keepTokens) {
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700459 if (string == null) {
460 return null;
461 }
462
463 String pString = string;
464 StringBuilder sb = new StringBuilder();
465 int start, end, last = 0;
466 while ((start = pString.indexOf(PROP_START, last)) >= 0) {
467 end = pString.indexOf(PROP_END, start + PROP_START.length());
468 checkArgument(end > start, "Malformed property in %s", pString);
469 sb.append(pString.substring(last, start));
470 String prop = pString.substring(start + PROP_START.length(), end);
471 String value;
472 if (prop.equals(HASH)) {
Thomas Vachuska4be30542015-10-21 18:15:52 -0700473 value = Integer.toString(clonables.get(0));
474 } else if (prop.equals(HASH_PREV)) {
475 value = Integer.toString(clonables.get(0) - 1);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700476 } else if (prop.endsWith(HASH)) {
Thomas Vachuska4be30542015-10-21 18:15:52 -0700477 pString = pString.replaceFirst("#}", clonables.get(0) + "}");
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700478 last = start;
479 continue;
480 } else {
481 // Try system property first, then fall back to env. variable.
482 value = System.getProperty(prop);
483 if (value == null) {
484 value = System.getenv(prop);
485 }
486 }
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700487 if (value == null && keepTokens.length == 1 && keepTokens[0]) {
488 sb.append("${").append(prop).append("}");
489 } else {
490 sb.append(value != null ? value : "");
491 }
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700492 last = end + 1;
493 }
494 sb.append(pString.substring(last));
Thomas Vachuska0ec6ff42015-07-17 11:00:02 -0700495 return sb.toString().replace('\n', ' ').replace('\r', ' ');
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700496 }
497
498 /**
499 * Splits the comma-separated string into a list of strings.
500 *
501 * @param string string to split
502 * @return list of strings
503 */
504 private List<String> split(String string) {
505 ImmutableList.Builder<String> builder = ImmutableList.builder();
506 String[] fields = string != null ? string.split(",") : new String[0];
507 for (String field : fields) {
508 builder.add(field.trim());
509 }
510 return builder.build();
511 }
512
513 /**
Thomas Vachuska1c152872015-08-26 18:41:10 -0700514 * Scans the process flow graph for cyclic dependencies.
515 */
516 private void scanForCycles() {
517 DepthFirstSearch<Step, Dependency> dfs = new DepthFirstSearch<>();
518 // Use a brute-force method of searching paths from all vertices.
519 processFlow().getVertexes().forEach(s -> {
520 DepthFirstSearch<Step, Dependency>.SpanningTreeResult r =
521 dfs.search(processFlow, s, null, null, ALL_PATHS);
522 r.edges().forEach((e, et) -> checkArgument(et != BACK_EDGE,
523 "Process flow has a cycle involving dependency from %s to %s",
524 e.src().name, e.dst().name));
525 });
526 }
527
528
529 /**
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700530 * Prints formatted output.
531 *
532 * @param format printf format string
533 * @param args arguments to be printed
534 */
535 private void print(String format, Object... args) {
536 if (debugOn) {
537 System.err.println(pfx + String.format(format, args));
538 }
539 }
540
541}