blob: 5d9b69e397ac7d464e45b83b79e138385eca1933 [file] [log] [blame]
lmr6f669ce2009-05-31 19:02:42 +00001import re, os, sys, StringIO
2from autotest_lib.client.common_lib import error
3
4"""
5KVM configuration file utility functions.
6
7@copyright: Red Hat 2008-2009
8"""
9
10class config:
11 """
12 Parse an input file or string that follows the KVM Test Config File format
13 and generate a list of dicts that will be later used as configuration
14 parameters by the the KVM tests.
15
16 @see: http://www.linux-kvm.org/page/KVM-Autotest/Test_Config_File
17 """
18
19 def __init__(self, filename=None, debug=False):
20 """
21 Initialize the list and optionally parse filename.
22
23 @param filename: Path of the file that will be taken
24 """
25 self.list = [{"name": "", "shortname": "", "depend": []}]
26 self.debug = debug
27 self.filename = filename
28 if filename:
29 self.parse_file(filename)
30
31
32 def set_debug(self, debug=False):
33 """
34 Enable or disable debugging output.
35
36 @param debug: Whether debug is enabled (True) or disabled (False).
37 """
38 self.debug = debug
39
40
41 def parse_file(self, filename):
42 """
43 Parse filename, return the resulting list and store it in .list. If
44 filename does not exist, raise an exception.
45
46 @param filename: Path of the configuration file.
47 """
48 if not os.path.exists(filename):
49 raise Exception, "File %s not found" % filename
50 self.filename = filename
51 file = open(filename, "r")
52 self.list = self.parse(file, self.list)
53 file.close()
54 return self.list
55
56
57 def parse_string(self, str):
58 """
59 Parse a string, return the resulting list and store it in .list.
60
61 @param str: String that will be parsed.
62 """
63 file = StringIO.StringIO(str)
64 self.list = self.parse(file, self.list)
65 file.close()
66 return self.list
67
68
69 def get_list(self):
70 """
71 Return the list of dictionaries. This should probably be called after
72 parsing something.
73 """
74 return self.list
75
76
77 def match(self, filter, dict):
78 """
79 Return True if dict matches filter.
80
81 @param filter: A regular expression that defines the filter.
82 @param dict: Dictionary that will be inspected.
83 """
84 filter = re.compile("(\\.|^)" + filter + "(\\.|$)")
85 return filter.search(dict["name"]) != None
86
87
88 def filter(self, filter, list=None):
89 """
90 Filter a list of dicts.
91
92 @param filter: A regular expression that will be used as a filter.
93 @param list: A list of dictionaries that will be filtered.
94 """
95 if list == None:
96 list = self.list
97 filtered_list = []
98 for dict in list:
99 if self.match(filter, dict):
100 filtered_list.append(dict)
101 return filtered_list
102
103
104 # Currently unused, will be removed if it remains unused
105 def get_match_block_indices(self, filter, list=None):
106 """
107 Get the indexes of a list that match a given filter.
108
109 @param filter: A regular expression that will filter the list.
110 @param list: List which we want to know the indexes that match
111 a filter.
112 """
113 if list == None:
114 list = self.list
115 block_list = []
116 prev_match = False
117 for index in range(len(list)):
118 dict = list[index]
119 if self.match(filter, dict):
120 if not prev_match:
121 block_list.append([index])
122 prev_match = True
123 else:
124 if prev_match:
125 block_list[-1].append(index)
126 prev_match = False
127 if prev_match:
128 block_list[-1].append(len(list))
129 return block_list
130
131
132 def split_and_strip(self, str, sep="="):
133 """
134 Split str and strip quotes from the resulting parts.
135
136 @param str: String that will be processed
137 @param sep: Separator that will be used to split the string
138 """
lmr0015c712009-06-08 13:51:14 +0000139 temp = str.split(sep, 1)
lmr6f669ce2009-05-31 19:02:42 +0000140 for i in range(len(temp)):
141 temp[i] = temp[i].strip()
lmr434252b2009-06-10 19:27:14 +0000142 if re.findall("^\".*\"$", temp[i]):
143 temp[i] = temp[i].strip("\"")
144 elif re.findall("^\'.*\'$", temp[i]):
145 temp[i] = temp[i].strip("\'")
lmr6f669ce2009-05-31 19:02:42 +0000146 return temp
147
148
149 def get_next_line(self, file):
150 """
151 Get the next non-empty, non-comment line in a file like object.
152
153 @param file: File like object
154 @return: If no line is available, return None.
155 """
156 while True:
157 line = file.readline()
158 if line == "": return None
159 stripped_line = line.strip()
160 if len(stripped_line) > 0 \
161 and not stripped_line.startswith('#') \
162 and not stripped_line.startswith('//'):
163 return line
164
165
166 def get_next_line_indent(self, file):
167 """
168 Return the indent level of the next non-empty, non-comment line in file.
169
170 @param file: File like object.
171 @return: If no line is available, return -1.
172 """
173 pos = file.tell()
174 line = self.get_next_line(file)
175 if not line:
176 file.seek(pos)
177 return -1
178 line = line.expandtabs()
179 indent = 0
180 while line[indent] == ' ':
181 indent += 1
182 file.seek(pos)
183 return indent
184
185
186 def add_name(self, str, name, append=False):
187 """
188 Add name to str with a separator dot and return the result.
189
190 @param str: String that will be processed
191 @param name: name that will be appended to the string.
192 @return: If append is True, append name to str.
193 Otherwise, pre-pend name to str.
194 """
195 if str == "":
196 return name
197 # Append?
198 elif append:
199 return str + "." + name
200 # Prepend?
201 else:
202 return name + "." + str
203
204
205 def parse_variants(self, file, list, subvariants=False, prev_indent=-1):
206 """
207 Read and parse lines from file like object until a line with an indent
208 level lower than or equal to prev_indent is encountered.
209
210 @brief: Parse a 'variants' or 'subvariants' block from a file-like
211 object.
212 @param file: File-like object that will be parsed
213 @param list: List of dicts to operate on
214 @param subvariants: If True, parse in 'subvariants' mode;
215 otherwise parse in 'variants' mode
216 @param prev_indent: The indent level of the "parent" block
217 @return: The resulting list of dicts.
218 """
219 new_list = []
220
221 while True:
222 indent = self.get_next_line_indent(file)
223 if indent <= prev_indent:
224 break
225 indented_line = self.get_next_line(file).rstrip()
226 line = indented_line.strip()
227
228 # Get name and dependencies
229 temp = line.strip("- ").split(":")
230 name = temp[0]
231 if len(temp) == 1:
232 dep_list = []
233 else:
234 dep_list = temp[1].split()
235
236 # See if name should be added to the 'shortname' field
237 add_to_shortname = True
238 if name.startswith("@"):
239 name = name.strip("@")
240 add_to_shortname = False
241
242 # Make a deep copy of list
243 temp_list = []
244 for dict in list:
245 new_dict = dict.copy()
246 new_dict["depend"] = dict["depend"][:]
247 temp_list.append(new_dict)
248
249 if subvariants:
250 # If we're parsing 'subvariants', we need to modify the list first
251 self.__modify_list_subvariants(temp_list, name, dep_list, add_to_shortname)
252 temp_list = self.parse(file, temp_list,
253 restricted=True, prev_indent=indent)
254 else:
255 # If we're parsing 'variants', we need to parse first and then modify the list
256 if self.debug:
257 self.__debug_print(indented_line, "Entering variant '%s' (variant inherits %d dicts)" % (name, len(list)))
258 temp_list = self.parse(file, temp_list,
259 restricted=False, prev_indent=indent)
260 self.__modify_list_variants(temp_list, name, dep_list, add_to_shortname)
261
262 new_list += temp_list
263
264 return new_list
265
266
267 def parse(self, file, list, restricted=False, prev_indent=-1):
268 """
269 Read and parse lines from file until a line with an indent level lower
270 than or equal to prev_indent is encountered.
271
272 @brief: Parse a file-like object.
273 @param file: A file-like object
274 @param list: A list of dicts to operate on (list is modified in
275 place and should not be used after the call)
276 @param restricted: if True, operate in restricted mode
277 (prohibit 'variants')
278 @param prev_indent: the indent level of the "parent" block
279 @return: Return the resulting list of dicts.
280 @note: List is destroyed and should not be used after the call.
281 Only the returned list should be used.
282 """
283 while True:
284 indent = self.get_next_line_indent(file)
285 if indent <= prev_indent:
286 break
287 indented_line = self.get_next_line(file).rstrip()
288 line = indented_line.strip()
289 words = line.split()
290
291 len_list = len(list)
292
293 # Look for a known operator in the line
294 operators = ["?+=", "?<=", "?=", "+=", "<=", "="]
295 op_found = None
296 for op in operators:
297 if op in line:
298 op_found = op
299 break
300
301 # Found an operator?
302 if op_found:
303 if self.debug and not restricted:
304 self.__debug_print(indented_line,
305 "Parsing operator (%d dicts in current"
306 "context)" % len_list)
307 (left, value) = self.split_and_strip(line, op_found)
308 filters_and_key = self.split_and_strip(left, ":")
309 filters = filters_and_key[:-1]
310 key = filters_and_key[-1]
311 filtered_list = list
312 for filter in filters:
313 filtered_list = self.filter(filter, filtered_list)
314 # Apply the operation to the filtered list
315 for dict in filtered_list:
316 if op_found == "=":
317 dict[key] = value
318 elif op_found == "+=":
319 dict[key] = dict.get(key, "") + value
320 elif op_found == "<=":
321 dict[key] = value + dict.get(key, "")
322 elif op_found.startswith("?") and dict.has_key(key):
323 if op_found == "?=":
324 dict[key] = value
325 elif op_found == "?+=":
326 dict[key] = dict.get(key, "") + value
327 elif op_found == "?<=":
328 dict[key] = value + dict.get(key, "")
329
330 # Parse 'no' and 'only' statements
331 elif words[0] == "no" or words[0] == "only":
332 if len(words) <= 1:
333 continue
334 filters = words[1:]
335 filtered_list = []
336 if words[0] == "no":
337 for dict in list:
338 for filter in filters:
339 if self.match(filter, dict):
340 break
341 else:
342 filtered_list.append(dict)
343 if words[0] == "only":
344 for dict in list:
345 for filter in filters:
346 if self.match(filter, dict):
347 filtered_list.append(dict)
348 break
349 list = filtered_list
350 if self.debug and not restricted:
351 self.__debug_print(indented_line,
352 "Parsing no/only (%d dicts in current"
353 "context, %d remain)" %
354 (len_list, len(list)))
355
356 # Parse 'variants'
357 elif line == "variants:":
358 # 'variants' not allowed in restricted mode
359 # (inside an exception or inside subvariants)
360 if restricted:
361 e_msg = "Using variants in this context is not allowed"
362 raise error.AutotestError()
363 if self.debug and not restricted:
364 self.__debug_print(indented_line,
365 "Entering variants block (%d dicts in"
366 "current context)" % len_list)
367 list = self.parse_variants(file, list, subvariants=False,
368 prev_indent=indent)
369
370 # Parse 'subvariants' (the block is parsed for each dict
371 # separately)
372 elif line == "subvariants:":
373 if self.debug and not restricted:
374 self.__debug_print(indented_line,
375 "Entering subvariants block (%d dicts in"
376 "current context)" % len_list)
377 new_list = []
378 # Remember current file position
379 pos = file.tell()
380 # Read the lines in any case
381 self.parse_variants(file, [], subvariants=True,
382 prev_indent=indent)
383 # Iterate over the list...
384 for index in range(len(list)):
385 # Revert to initial file position in this 'subvariants'
386 # block
387 file.seek(pos)
388 # Everything inside 'subvariants' should be parsed in
389 # restricted mode
390 new_list += self.parse_variants(file, list[index:index+1],
391 subvariants=True,
392 prev_indent=indent)
393 list = new_list
394
395 # Parse 'include' statements
396 elif words[0] == "include":
397 if len(words) <= 1:
398 continue
399 if self.debug and not restricted:
400 self.__debug_print(indented_line,
401 "Entering file %s" % words[1])
402 if self.filename:
403 filename = os.path.join(os.path.dirname(self.filename),
404 words[1])
405 if not os.path.exists(filename):
406 e_msg = "Cannot include %s -- file not found" % filename
407 raise error.AutotestError(e_msg)
408 new_file = open(filename, "r")
409 list = self.parse(new_file, list, restricted)
410 new_file.close()
411 if self.debug and not restricted:
412 self.__debug_print("", "Leaving file %s" % words[1])
413 else:
414 e_msg = "Cannot include anything because no file is open"
415 raise error.AutotestError(e_msg)
416
417 # Parse multi-line exceptions
418 # (the block is parsed for each dict separately)
419 elif line.endswith(":"):
420 if self.debug and not restricted:
421 self.__debug_print(indented_line,
422 "Entering multi-line exception block"
423 "(%d dicts in current context outside"
424 "exception)" % len_list)
425 line = line.strip(":")
426 new_list = []
427 # Remember current file position
428 pos = file.tell()
429 # Read the lines in any case
430 self.parse(file, [], restricted=True, prev_indent=indent)
431 # Iterate over the list...
432 for index in range(len(list)):
433 if self.match(line, list[index]):
434 # Revert to initial file position in this
435 # exception block
436 file.seek(pos)
437 # Everything inside an exception should be parsed in
438 # restricted mode
439 new_list += self.parse(file, list[index:index+1],
440 restricted=True, prev_indent=indent)
441 else:
442 new_list += list[index:index+1]
443 list = new_list
444
445 return list
446
447
448 def __debug_print(self, str1, str2=""):
449 """
450 Nicely print two strings and an arrow.
451
452 @param str1: First string
453 @param str2: Second string
454 """
455 if str2:
456 str = "%-50s ---> %s" % (str1, str2)
457 else:
458 str = str1
459 print str
460
461
462 def __modify_list_variants(self, list, name, dep_list, add_to_shortname):
463 """
464 Make some modifications to list, as part of parsing a 'variants' block.
465
466 @param list
467 """
468 for dict in list:
469 # Prepend name to the dict's 'name' field
470 dict["name"] = self.add_name(dict["name"], name)
471 # Prepend name to the dict's 'shortname' field
472 if add_to_shortname:
473 dict["shortname"] = self.add_name(dict["shortname"], name)
474 # Prepend name to each of the dict's dependencies
475 for i in range(len(dict["depend"])):
476 dict["depend"][i] = self.add_name(dict["depend"][i], name)
477 # Add new dependencies
478 dict["depend"] += dep_list
479
480
481 def __modify_list_subvariants(self, list, name, dep_list, add_to_shortname):
482 """
483 Make some modifications to list, as part of parsing a
484 'subvariants' block.
485
486 @param list: List that will be processed
487 @param name: Name that will be prepended to the dictionary name
488 @param dep_list: List of dependencies to be added to the list
489 dictionaries
490 @param add_to_shortname: Whether we'll add a shortname parameter to
491 the dictionaries.
492 """
493 for dict in list:
494 # Add new dependencies
495 for dep in dep_list:
496 dep_name = self.add_name(dict["name"], dep, append=True)
497 dict["depend"].append(dep_name)
498 # Append name to the dict's 'name' field
499 dict["name"] = self.add_name(dict["name"], name, append=True)
500 # Append name to the dict's 'shortname' field
501 if add_to_shortname:
502 dict["shortname"] = self.add_name(dict["shortname"], name,
503 append=True)
504
505
506if __name__ == "__main__":
507 if len(sys.argv) >= 2:
508 filename = sys.argv[1]
509 else:
510 filename = os.path.join(os.path.dirname(sys.argv[0]), "kvm_tests.cfg")
511 list = config(filename, debug=True).get_list()
512 i = 0
513 for dict in list:
514 print "Dictionary #%d:" % i
515 keys = dict.keys()
516 keys.sort()
517 for key in keys:
518 print " %s = %s" % (key, dict[key])
519 i += 1