blob: 4d689623d724d5ea9f2cd4f429df8fa731f04390 [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);
Thomas Vachuska177ece62015-06-26 00:18:21 -0700312 if (!inactiveSteps.containsValue(step)) {
313 processRequirements(step, requires, namespace);
314 }
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700315 }
316
317 /**
318 * Processes the specified requiremenst string and adds dependency for
319 * each requirement of the given step.
320 *
321 * @param src source step
322 * @param requires comma-separated list of required steps
323 * @param namespace optional namespace
324 */
325 private void processRequirements(Step src, String requires, String namespace) {
Thomas Vachuska18571b02015-05-31 22:30:06 -0700326 split(requires).forEach(n -> {
327 boolean isSoft = n.startsWith("~");
328 String name = n.replaceFirst("^~", "");
329 name = previous != null && name.equals("^") ? previous : name;
330 Step dst = getStep(name, namespace);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700331 if (!inactiveSteps.containsValue(dst)) {
332 dependencies.add(new Dependency(src, dst, isSoft));
333 }
334 });
335 }
336
337 /**
338 * Retrieves the step or group with the specified name.
339 *
340 * @param name step or group name
341 * @param namespace optional namespace
342 * @return step or group; null if none found in active or inactive steps
343 */
344 private Step getStep(String name, String namespace) {
345 String dName = prefix(name, namespace);
346 Step step = steps.get(dName);
347 step = step != null ? step : inactiveSteps.get(dName);
348 checkArgument(step != null, "Unknown step %s", dName);
349 return step;
350 }
351
352 /**
353 * Prefixes the specified name with the given namespace.
354 *
355 * @param name name of a step or a group
356 * @param namespace optional namespace
357 * @return composite name
358 */
359 private String prefix(String name, String namespace) {
360 return namespace != null ? namespace + "." + name : name;
361 }
362
363 /**
364 * Expands any environment variables in the specified
365 * string. These are specified as ${property} tokens.
366 *
367 * @param string string to be processed
368 * @return original string with expanded substitutions
369 */
370 private String expand(String string) {
371 if (string == null) {
372 return null;
373 }
374
375 String pString = string;
376 StringBuilder sb = new StringBuilder();
377 int start, end, last = 0;
378 while ((start = pString.indexOf(PROP_START, last)) >= 0) {
379 end = pString.indexOf(PROP_END, start + PROP_START.length());
380 checkArgument(end > start, "Malformed property in %s", pString);
381 sb.append(pString.substring(last, start));
382 String prop = pString.substring(start + PROP_START.length(), end);
383 String value;
384 if (prop.equals(HASH)) {
385 value = parallels.get(0).toString();
386 } else if (prop.endsWith(HASH)) {
387 pString = pString.replaceFirst("#}", parallels.get(0).toString() + "}");
388 last = start;
389 continue;
390 } else {
391 // Try system property first, then fall back to env. variable.
392 value = System.getProperty(prop);
393 if (value == null) {
394 value = System.getenv(prop);
395 }
396 }
397 sb.append(value != null ? value : "");
398 last = end + 1;
399 }
400 sb.append(pString.substring(last));
401 return sb.toString();
402 }
403
404 /**
405 * Splits the comma-separated string into a list of strings.
406 *
407 * @param string string to split
408 * @return list of strings
409 */
410 private List<String> split(String string) {
411 ImmutableList.Builder<String> builder = ImmutableList.builder();
412 String[] fields = string != null ? string.split(",") : new String[0];
413 for (String field : fields) {
414 builder.add(field.trim());
415 }
416 return builder.build();
417 }
418
419 /**
420 * Prints formatted output.
421 *
422 * @param format printf format string
423 * @param args arguments to be printed
424 */
425 private void print(String format, Object... args) {
426 if (debugOn) {
427 System.err.println(pfx + String.format(format, args));
428 }
429 }
430
431}