blob: 2a8a2ceb1767290851373816aca2d00e38cfaea5 [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;
24
25import java.io.File;
26import java.io.FileInputStream;
27import java.io.IOException;
28import java.util.List;
29import java.util.Map;
30import java.util.Set;
31
32import static com.google.common.base.Preconditions.checkArgument;
33import static com.google.common.base.Preconditions.checkNotNull;
34import static org.onlab.stc.Scenario.loadScenario;
35
36/**
37 * Entity responsible for loading a scenario and producing a redy-to-execute
38 * process flow graph.
39 */
40public class Compiler {
41
42 private static final String DEFAULT_LOG_DIR = "${env.WORKSPACE}/tmp/stc/";
43
44 private static final String IMPORT = "import";
45 private static final String GROUP = "group";
46 private static final String STEP = "step";
47 private static final String PARALLEL = "parallel";
48 private static final String DEPENDENCY = "dependency";
49
50 private static final String LOG_DIR = "[@logDir]";
51 private static final String NAME = "[@name]";
52 private static final String COMMAND = "[@exec]";
Thomas Vachuska4bfccd542015-05-30 00:35:25 -070053 private static final String ENV = "[@env]";
54 private static final String CWD = "[@cwd]";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070055 private static final String REQUIRES = "[@requires]";
56 private static final String IF = "[@if]";
57 private static final String UNLESS = "[@unless]";
58 private static final String VAR = "[@var]";
59 private static final String FILE = "[@file]";
60 private static final String NAMESPACE = "[@namespace]";
61
62 private static final String PROP_START = "${";
63 private static final String PROP_END = "}";
64 private static final String HASH = "#";
65
66 private final Scenario scenario;
67
68 private final Map<String, Step> steps = Maps.newHashMap();
69 private final Map<String, Step> inactiveSteps = Maps.newHashMap();
70 private final Set<Dependency> dependencies = Sets.newHashSet();
71 private final List<Integer> parallels = Lists.newArrayList();
72
73 private ProcessFlow processFlow;
74 private File logDir;
75
Thomas Vachuska18571b02015-05-31 22:30:06 -070076 private String previous = null;
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070077 private String pfx = "";
78 private boolean debugOn = System.getenv("debug") != null;
79
80 /**
81 * Creates a new compiler for the specified scenario.
82 *
83 * @param scenario scenario to be compiled
84 */
85 public Compiler(Scenario scenario) {
86 this.scenario = scenario;
87 }
88
89 /**
90 * Returns the scenario being compiled.
91 *
92 * @return test scenario
93 */
94 public Scenario scenario() {
95 return scenario;
96 }
97
98 /**
99 * Compiles the specified scenario to produce a final process flow graph.
100 */
101 public void compile() {
102 compile(scenario.definition(), null, null);
103
104 // Produce the process flow
105 processFlow = new ProcessFlow(ImmutableSet.copyOf(steps.values()),
106 ImmutableSet.copyOf(dependencies));
107
108 // Extract the log directory if there was one specified
Thomas Vachuskaa48e3d12015-06-02 09:43:29 -0700109 String defaultPath = DEFAULT_LOG_DIR + scenario.name();
110 String path = scenario.definition().getString(LOG_DIR, defaultPath);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700111 logDir = new File(expand(path));
112 }
113
114 /**
115 * Returns the step with the specified name.
116 *
117 * @param name step or group name
118 * @return test step or group
119 */
120 public Step getStep(String name) {
121 return steps.get(name);
122 }
123
124 /**
125 * Returns the process flow generated from this scenario definition.
126 *
127 * @return process flow as a graph
128 */
129 public ProcessFlow processFlow() {
130 return processFlow;
131 }
132
133 /**
134 * Returns the log directory where scenario logs should be kept.
Thomas Vachuska18571b02015-05-31 22:30:06 -0700135 *
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700136 * @return scenario logs directory
137 */
138 public File logDir() {
139 return logDir;
140 }
141
142 /**
143 * Recursively elaborates this definition to produce a final process flow graph.
144 *
145 * @param cfg hierarchical definition
146 * @param namespace optional namespace
147 * @param parentGroup optional parent group
148 */
149 private void compile(HierarchicalConfiguration cfg,
150 String namespace, Group parentGroup) {
151 String opfx = pfx;
152 pfx = pfx + ">";
153
154 // Scan all imports
155 cfg.configurationsAt(IMPORT)
156 .forEach(c -> processImport(c, namespace, parentGroup));
157
158 // Scan all steps
159 cfg.configurationsAt(STEP)
160 .forEach(c -> processStep(c, namespace, parentGroup));
161
162 // Scan all groups
163 cfg.configurationsAt(GROUP)
164 .forEach(c -> processGroup(c, namespace, parentGroup));
165
166 // Scan all parallel groups
167 cfg.configurationsAt(PARALLEL)
168 .forEach(c -> processParallelGroup(c, namespace, parentGroup));
169
170 // Scan all dependencies
171 cfg.configurationsAt(DEPENDENCY)
172 .forEach(c -> processDependency(c, namespace));
173
174 pfx = opfx;
175 }
176
177 /**
178 * Processes an import directive.
179 *
180 * @param cfg hierarchical definition
181 * @param namespace optional namespace
182 * @param parentGroup optional parent group
183 */
184 private void processImport(HierarchicalConfiguration cfg,
185 String namespace, Group parentGroup) {
186 String file = checkNotNull(expand(cfg.getString(FILE)),
187 "Import directive must specify 'file'");
188 String newNamespace = expand(prefix(cfg.getString(NAMESPACE), namespace));
189 print("import file=%s namespace=%s", file, newNamespace);
190 try {
191 Scenario importScenario = loadScenario(new FileInputStream(file));
192 compile(importScenario.definition(), namespace, parentGroup);
193 } catch (IOException e) {
194 throw new IllegalArgumentException("Unable to import scenario", e);
195 }
196 }
197
198 /**
199 * Processes a step directive.
200 *
201 * @param cfg hierarchical definition
202 * @param namespace optional namespace
203 * @param parentGroup optional parent group
204 */
205 private void processStep(HierarchicalConfiguration cfg,
206 String namespace, Group parentGroup) {
207 String name = expand(prefix(cfg.getString(NAME), namespace));
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700208 String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null));
209 String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
210 String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700211
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700212 print("step name=%s command=%s env=%s cwd=%s", name, command, env, cwd);
213 Step step = new Step(name, command, env, cwd, parentGroup);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700214 registerStep(step, cfg, namespace, parentGroup);
215 }
216
217 /**
218 * Processes a group directive.
219 *
220 * @param cfg hierarchical definition
221 * @param namespace optional namespace
222 * @param parentGroup optional parent group
223 */
224 private void processGroup(HierarchicalConfiguration cfg,
225 String namespace, Group parentGroup) {
226 String name = expand(prefix(cfg.getString(NAME), namespace));
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700227 String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null));
228 String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
229 String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700230
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700231 print("group name=%s command=%s env=%s cwd=%s", name, command, env, cwd);
232 Group group = new Group(name, command, env, cwd, parentGroup);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700233 if (registerStep(group, cfg, namespace, parentGroup)) {
234 compile(cfg, namespace, group);
235 }
236 }
237
238 /**
239 * Registers the specified step or group.
240 *
241 * @param step step or group
242 * @param cfg hierarchical definition
243 * @param namespace optional namespace
244 * @param parentGroup optional parent group
245 * @return true of the step or group was registered as active
246 */
247 private boolean registerStep(Step step, HierarchicalConfiguration cfg,
248 String namespace, Group parentGroup) {
249 String ifClause = expand(cfg.getString(IF));
250 String unlessClause = expand(cfg.getString(UNLESS));
251
252 if ((ifClause != null && ifClause.length() == 0) ||
253 (unlessClause != null && unlessClause.length() > 0) ||
254 (parentGroup != null && inactiveSteps.containsValue(parentGroup))) {
255 inactiveSteps.put(step.name(), step);
256 return false;
257 }
258
259 if (parentGroup != null) {
260 parentGroup.addChild(step);
261 }
262 steps.put(step.name(), step);
263 processRequirements(step, expand(cfg.getString(REQUIRES)), namespace);
Thomas Vachuska18571b02015-05-31 22:30:06 -0700264 previous = step.name();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700265 return true;
266 }
267
268 /**
269 * Processes a parallel clone group directive.
270 *
271 * @param cfg hierarchical definition
272 * @param namespace optional namespace
273 * @param parentGroup optional parent group
274 */
275 private void processParallelGroup(HierarchicalConfiguration cfg,
276 String namespace, Group parentGroup) {
277 String var = cfg.getString(VAR);
278 print("parallel var=%s", var);
279
280 int i = 1;
281 while (condition(var, i).length() > 0) {
282 parallels.add(0, i);
283 compile(cfg, namespace, parentGroup);
284 parallels.remove(0);
285 i++;
286 }
287 }
288
289 /**
290 * Returns the elaborated repetition construct conditional.
291 *
292 * @param var repetition var property
293 * @param i index to elaborate
294 * @return elaborated string
295 */
296 private String condition(String var, Integer i) {
297 return expand(var.replaceFirst("#", i.toString())).trim();
298 }
299
300 /**
301 * Processes a dependency directive.
302 *
303 * @param cfg hierarchical definition
304 * @param namespace optional namespace
305 */
306 private void processDependency(HierarchicalConfiguration cfg, String namespace) {
307 String name = expand(prefix(cfg.getString(NAME), namespace));
308 String requires = expand(cfg.getString(REQUIRES));
309
310 print("dependency name=%s requires=%s", name, requires);
311 Step step = getStep(name, namespace);
312 processRequirements(step, requires, namespace);
313 }
314
315 /**
316 * Processes the specified requiremenst string and adds dependency for
317 * each requirement of the given step.
318 *
319 * @param src source step
320 * @param requires comma-separated list of required steps
321 * @param namespace optional namespace
322 */
323 private void processRequirements(Step src, String requires, String namespace) {
Thomas Vachuska18571b02015-05-31 22:30:06 -0700324 split(requires).forEach(n -> {
325 boolean isSoft = n.startsWith("~");
326 String name = n.replaceFirst("^~", "");
327 name = previous != null && name.equals("^") ? previous : name;
328 Step dst = getStep(name, namespace);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700329 if (!inactiveSteps.containsValue(dst)) {
330 dependencies.add(new Dependency(src, dst, isSoft));
331 }
332 });
333 }
334
335 /**
336 * Retrieves the step or group with the specified name.
337 *
338 * @param name step or group name
339 * @param namespace optional namespace
340 * @return step or group; null if none found in active or inactive steps
341 */
342 private Step getStep(String name, String namespace) {
343 String dName = prefix(name, namespace);
344 Step step = steps.get(dName);
345 step = step != null ? step : inactiveSteps.get(dName);
346 checkArgument(step != null, "Unknown step %s", dName);
347 return step;
348 }
349
350 /**
351 * Prefixes the specified name with the given namespace.
352 *
353 * @param name name of a step or a group
354 * @param namespace optional namespace
355 * @return composite name
356 */
357 private String prefix(String name, String namespace) {
358 return namespace != null ? namespace + "." + name : name;
359 }
360
361 /**
362 * Expands any environment variables in the specified
363 * string. These are specified as ${property} tokens.
364 *
365 * @param string string to be processed
366 * @return original string with expanded substitutions
367 */
368 private String expand(String string) {
369 if (string == null) {
370 return null;
371 }
372
373 String pString = string;
374 StringBuilder sb = new StringBuilder();
375 int start, end, last = 0;
376 while ((start = pString.indexOf(PROP_START, last)) >= 0) {
377 end = pString.indexOf(PROP_END, start + PROP_START.length());
378 checkArgument(end > start, "Malformed property in %s", pString);
379 sb.append(pString.substring(last, start));
380 String prop = pString.substring(start + PROP_START.length(), end);
381 String value;
382 if (prop.equals(HASH)) {
383 value = parallels.get(0).toString();
384 } else if (prop.endsWith(HASH)) {
385 pString = pString.replaceFirst("#}", parallels.get(0).toString() + "}");
386 last = start;
387 continue;
388 } else {
389 // Try system property first, then fall back to env. variable.
390 value = System.getProperty(prop);
391 if (value == null) {
392 value = System.getenv(prop);
393 }
394 }
395 sb.append(value != null ? value : "");
396 last = end + 1;
397 }
398 sb.append(pString.substring(last));
399 return sb.toString();
400 }
401
402 /**
403 * Splits the comma-separated string into a list of strings.
404 *
405 * @param string string to split
406 * @return list of strings
407 */
408 private List<String> split(String string) {
409 ImmutableList.Builder<String> builder = ImmutableList.builder();
410 String[] fields = string != null ? string.split(",") : new String[0];
411 for (String field : fields) {
412 builder.add(field.trim());
413 }
414 return builder.build();
415 }
416
417 /**
418 * Prints formatted output.
419 *
420 * @param format printf format string
421 * @param args arguments to be printed
422 */
423 private void print(String format, Object... args) {
424 if (debugOn) {
425 System.err.println(pfx + String.format(format, args));
426 }
427 }
428
429}