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