blob: 3114c07eacbb5195025d8cbac14388babae8658a [file] [log] [blame]
lmrc6bd3a62009-06-10 19:38:06 +00001#!/usr/bin/python
lmr6f669ce2009-05-31 19:02:42 +00002"""
3KVM configuration file utility functions.
4
5@copyright: Red Hat 2008-2009
6"""
7
lmrb635b862009-09-10 14:53:21 +00008import logging, re, os, sys, StringIO, optparse
9import common
10from autotest_lib.client.common_lib import error
11from autotest_lib.client.common_lib import logging_config, logging_manager
12
lmra89bdac2009-08-10 14:01:26 +000013
14class KvmLoggingConfig(logging_config.LoggingConfig):
15 def configure_logging(self, results_dir=None, verbose=False):
16 super(KvmLoggingConfig, self).configure_logging(use_console=True,
17 verbose=verbose)
18
19
lmr6f669ce2009-05-31 19:02:42 +000020class config:
21 """
22 Parse an input file or string that follows the KVM Test Config File format
23 and generate a list of dicts that will be later used as configuration
24 parameters by the the KVM tests.
25
26 @see: http://www.linux-kvm.org/page/KVM-Autotest/Test_Config_File
27 """
28
29 def __init__(self, filename=None, debug=False):
30 """
31 Initialize the list and optionally parse filename.
32
lmra89bdac2009-08-10 14:01:26 +000033 @param filename: Path of the file that will be taken.
34 @param debug: Whether to turn debugging output.
lmr6f669ce2009-05-31 19:02:42 +000035 """
36 self.list = [{"name": "", "shortname": "", "depend": []}]
37 self.debug = debug
38 self.filename = filename
39 if filename:
40 self.parse_file(filename)
41
42
lmr6f669ce2009-05-31 19:02:42 +000043 def parse_file(self, filename):
44 """
45 Parse filename, return the resulting list and store it in .list. If
46 filename does not exist, raise an exception.
47
lmrfc0e1412009-06-10 20:03:26 +000048 @param filename: Path of the configuration file.
lmr6f669ce2009-05-31 19:02:42 +000049 """
50 if not os.path.exists(filename):
51 raise Exception, "File %s not found" % filename
52 self.filename = filename
53 file = open(filename, "r")
54 self.list = self.parse(file, self.list)
55 file.close()
56 return self.list
57
58
59 def parse_string(self, str):
60 """
61 Parse a string, return the resulting list and store it in .list.
62
lmrfc0e1412009-06-10 20:03:26 +000063 @param str: String that will be parsed.
lmr6f669ce2009-05-31 19:02:42 +000064 """
65 file = StringIO.StringIO(str)
66 self.list = self.parse(file, self.list)
67 file.close()
68 return self.list
69
70
71 def get_list(self):
72 """
73 Return the list of dictionaries. This should probably be called after
74 parsing something.
75 """
76 return self.list
77
78
79 def match(self, filter, dict):
80 """
81 Return True if dict matches filter.
82
lmrfc0e1412009-06-10 20:03:26 +000083 @param filter: A regular expression that defines the filter.
84 @param dict: Dictionary that will be inspected.
lmr6f669ce2009-05-31 19:02:42 +000085 """
lmr5e419062009-08-07 21:04:35 +000086 filter = re.compile(r"(\.|^)" + filter + r"(\.|$)")
87 return bool(filter.search(dict["name"]))
lmr6f669ce2009-05-31 19:02:42 +000088
89
90 def filter(self, filter, list=None):
91 """
92 Filter a list of dicts.
93
lmrfc0e1412009-06-10 20:03:26 +000094 @param filter: A regular expression that will be used as a filter.
95 @param list: A list of dictionaries that will be filtered.
lmr6f669ce2009-05-31 19:02:42 +000096 """
lmr5e419062009-08-07 21:04:35 +000097 if list is None:
lmr6f669ce2009-05-31 19:02:42 +000098 list = self.list
lmr5e419062009-08-07 21:04:35 +000099 return [dict for dict in list if self.match(filter, dict)]
lmr6f669ce2009-05-31 19:02:42 +0000100
101
lmr6f669ce2009-05-31 19:02:42 +0000102 def split_and_strip(self, str, sep="="):
103 """
104 Split str and strip quotes from the resulting parts.
105
lmrfc0e1412009-06-10 20:03:26 +0000106 @param str: String that will be processed
107 @param sep: Separator that will be used to split the string
lmr6f669ce2009-05-31 19:02:42 +0000108 """
lmr0015c712009-06-08 13:51:14 +0000109 temp = str.split(sep, 1)
lmr6f669ce2009-05-31 19:02:42 +0000110 for i in range(len(temp)):
111 temp[i] = temp[i].strip()
lmr434252b2009-06-10 19:27:14 +0000112 if re.findall("^\".*\"$", temp[i]):
113 temp[i] = temp[i].strip("\"")
114 elif re.findall("^\'.*\'$", temp[i]):
115 temp[i] = temp[i].strip("\'")
lmr6f669ce2009-05-31 19:02:42 +0000116 return temp
117
118
119 def get_next_line(self, file):
120 """
121 Get the next non-empty, non-comment line in a file like object.
122
lmrfc0e1412009-06-10 20:03:26 +0000123 @param file: File like object
124 @return: If no line is available, return None.
lmr6f669ce2009-05-31 19:02:42 +0000125 """
126 while True:
127 line = file.readline()
128 if line == "": return None
129 stripped_line = line.strip()
130 if len(stripped_line) > 0 \
131 and not stripped_line.startswith('#') \
132 and not stripped_line.startswith('//'):
133 return line
134
135
136 def get_next_line_indent(self, file):
137 """
138 Return the indent level of the next non-empty, non-comment line in file.
139
lmrf402d232009-07-27 13:16:12 +0000140 @param file: File like object.
141 @return: If no line is available, return -1.
lmr6f669ce2009-05-31 19:02:42 +0000142 """
143 pos = file.tell()
144 line = self.get_next_line(file)
145 if not line:
146 file.seek(pos)
147 return -1
148 line = line.expandtabs()
149 indent = 0
150 while line[indent] == ' ':
151 indent += 1
152 file.seek(pos)
153 return indent
154
155
156 def add_name(self, str, name, append=False):
157 """
158 Add name to str with a separator dot and return the result.
159
lmrf402d232009-07-27 13:16:12 +0000160 @param str: String that will be processed
161 @param name: name that will be appended to the string.
162 @return: If append is True, append name to str.
163 Otherwise, pre-pend name to str.
lmr6f669ce2009-05-31 19:02:42 +0000164 """
165 if str == "":
166 return name
167 # Append?
168 elif append:
169 return str + "." + name
170 # Prepend?
171 else:
172 return name + "." + str
173
174
175 def parse_variants(self, file, list, subvariants=False, prev_indent=-1):
176 """
177 Read and parse lines from file like object until a line with an indent
178 level lower than or equal to prev_indent is encountered.
179
lmrf402d232009-07-27 13:16:12 +0000180 @brief: Parse a 'variants' or 'subvariants' block from a file-like
181 object.
182 @param file: File-like object that will be parsed
183 @param list: List of dicts to operate on
184 @param subvariants: If True, parse in 'subvariants' mode;
185 otherwise parse in 'variants' mode
186 @param prev_indent: The indent level of the "parent" block
187 @return: The resulting list of dicts.
lmr6f669ce2009-05-31 19:02:42 +0000188 """
189 new_list = []
190
191 while True:
192 indent = self.get_next_line_indent(file)
193 if indent <= prev_indent:
194 break
195 indented_line = self.get_next_line(file).rstrip()
196 line = indented_line.strip()
197
198 # Get name and dependencies
199 temp = line.strip("- ").split(":")
200 name = temp[0]
201 if len(temp) == 1:
202 dep_list = []
203 else:
204 dep_list = temp[1].split()
205
206 # See if name should be added to the 'shortname' field
207 add_to_shortname = True
208 if name.startswith("@"):
209 name = name.strip("@")
210 add_to_shortname = False
211
212 # Make a deep copy of list
213 temp_list = []
214 for dict in list:
215 new_dict = dict.copy()
216 new_dict["depend"] = dict["depend"][:]
217 temp_list.append(new_dict)
218
219 if subvariants:
lmr5e419062009-08-07 21:04:35 +0000220 # If we're parsing 'subvariants', first modify the list
221 self.__modify_list_subvariants(temp_list, name, dep_list,
222 add_to_shortname)
lmr6f669ce2009-05-31 19:02:42 +0000223 temp_list = self.parse(file, temp_list,
224 restricted=True, prev_indent=indent)
225 else:
lmr5e419062009-08-07 21:04:35 +0000226 # If we're parsing 'variants', parse before modifying the list
lmr6f669ce2009-05-31 19:02:42 +0000227 if self.debug:
lmr5e419062009-08-07 21:04:35 +0000228 self.__debug_print(indented_line,
229 "Entering variant '%s' "
230 "(variant inherits %d dicts)" %
231 (name, len(list)))
lmr6f669ce2009-05-31 19:02:42 +0000232 temp_list = self.parse(file, temp_list,
233 restricted=False, prev_indent=indent)
lmr5e419062009-08-07 21:04:35 +0000234 self.__modify_list_variants(temp_list, name, dep_list,
235 add_to_shortname)
lmr6f669ce2009-05-31 19:02:42 +0000236
237 new_list += temp_list
238
239 return new_list
240
241
242 def parse(self, file, list, restricted=False, prev_indent=-1):
243 """
244 Read and parse lines from file until a line with an indent level lower
245 than or equal to prev_indent is encountered.
246
lmrf402d232009-07-27 13:16:12 +0000247 @brief: Parse a file-like object.
248 @param file: A file-like object
249 @param list: A list of dicts to operate on (list is modified in
250 place and should not be used after the call)
251 @param restricted: if True, operate in restricted mode
252 (prohibit 'variants')
253 @param prev_indent: the indent level of the "parent" block
254 @return: Return the resulting list of dicts.
255 @note: List is destroyed and should not be used after the call.
256 Only the returned list should be used.
lmr6f669ce2009-05-31 19:02:42 +0000257 """
258 while True:
259 indent = self.get_next_line_indent(file)
260 if indent <= prev_indent:
261 break
262 indented_line = self.get_next_line(file).rstrip()
263 line = indented_line.strip()
264 words = line.split()
265
266 len_list = len(list)
267
268 # Look for a known operator in the line
269 operators = ["?+=", "?<=", "?=", "+=", "<=", "="]
270 op_found = None
lmr4f397fd2009-07-27 13:14:55 +0000271 op_pos = len(line)
lmr6f669ce2009-05-31 19:02:42 +0000272 for op in operators:
lmr4f397fd2009-07-27 13:14:55 +0000273 pos = line.find(op)
274 if pos >= 0 and pos < op_pos:
lmr6f669ce2009-05-31 19:02:42 +0000275 op_found = op
lmr4f397fd2009-07-27 13:14:55 +0000276 op_pos = pos
lmr6f669ce2009-05-31 19:02:42 +0000277
278 # Found an operator?
279 if op_found:
280 if self.debug and not restricted:
281 self.__debug_print(indented_line,
lmrf402d232009-07-27 13:16:12 +0000282 "Parsing operator (%d dicts in current "
283 "context)" % len_list)
lmr6f669ce2009-05-31 19:02:42 +0000284 (left, value) = self.split_and_strip(line, op_found)
285 filters_and_key = self.split_and_strip(left, ":")
286 filters = filters_and_key[:-1]
287 key = filters_and_key[-1]
288 filtered_list = list
289 for filter in filters:
290 filtered_list = self.filter(filter, filtered_list)
291 # Apply the operation to the filtered list
lmr9018a802009-08-17 20:47:01 +0000292 if op_found == "=":
293 for dict in filtered_list:
lmr6f669ce2009-05-31 19:02:42 +0000294 dict[key] = value
lmr9018a802009-08-17 20:47:01 +0000295 elif op_found == "+=":
296 for dict in filtered_list:
lmr6f669ce2009-05-31 19:02:42 +0000297 dict[key] = dict.get(key, "") + value
lmr9018a802009-08-17 20:47:01 +0000298 elif op_found == "<=":
299 for dict in filtered_list:
lmr6f669ce2009-05-31 19:02:42 +0000300 dict[key] = value + dict.get(key, "")
lmr9018a802009-08-17 20:47:01 +0000301 elif op_found.startswith("?"):
302 exp = re.compile("^(%s)$" % key)
303 if op_found == "?=":
304 for dict in filtered_list:
305 for key in dict.keys():
306 if exp.match(key):
307 dict[key] = value
308 elif op_found == "?+=":
309 for dict in filtered_list:
310 for key in dict.keys():
311 if exp.match(key):
312 dict[key] = dict.get(key, "") + value
313 elif op_found == "?<=":
314 for dict in filtered_list:
315 for key in dict.keys():
316 if exp.match(key):
317 dict[key] = value + dict.get(key, "")
lmr6f669ce2009-05-31 19:02:42 +0000318
319 # Parse 'no' and 'only' statements
320 elif words[0] == "no" or words[0] == "only":
321 if len(words) <= 1:
322 continue
323 filters = words[1:]
324 filtered_list = []
325 if words[0] == "no":
326 for dict in list:
327 for filter in filters:
328 if self.match(filter, dict):
329 break
330 else:
331 filtered_list.append(dict)
332 if words[0] == "only":
333 for dict in list:
334 for filter in filters:
335 if self.match(filter, dict):
336 filtered_list.append(dict)
337 break
338 list = filtered_list
339 if self.debug and not restricted:
340 self.__debug_print(indented_line,
lmrf402d232009-07-27 13:16:12 +0000341 "Parsing no/only (%d dicts in current "
342 "context, %d remain)" %
343 (len_list, len(list)))
lmr6f669ce2009-05-31 19:02:42 +0000344
345 # Parse 'variants'
346 elif line == "variants:":
347 # 'variants' not allowed in restricted mode
348 # (inside an exception or inside subvariants)
349 if restricted:
350 e_msg = "Using variants in this context is not allowed"
lmrc6bd3a62009-06-10 19:38:06 +0000351 raise error.AutotestError(e_msg)
lmr6f669ce2009-05-31 19:02:42 +0000352 if self.debug and not restricted:
353 self.__debug_print(indented_line,
lmrf402d232009-07-27 13:16:12 +0000354 "Entering variants block (%d dicts in "
355 "current context)" % len_list)
lmr6f669ce2009-05-31 19:02:42 +0000356 list = self.parse_variants(file, list, subvariants=False,
357 prev_indent=indent)
358
359 # Parse 'subvariants' (the block is parsed for each dict
360 # separately)
361 elif line == "subvariants:":
362 if self.debug and not restricted:
363 self.__debug_print(indented_line,
lmrf402d232009-07-27 13:16:12 +0000364 "Entering subvariants block (%d dicts in "
365 "current context)" % len_list)
lmr6f669ce2009-05-31 19:02:42 +0000366 new_list = []
367 # Remember current file position
368 pos = file.tell()
369 # Read the lines in any case
370 self.parse_variants(file, [], subvariants=True,
371 prev_indent=indent)
372 # Iterate over the list...
373 for index in range(len(list)):
374 # Revert to initial file position in this 'subvariants'
375 # block
376 file.seek(pos)
377 # Everything inside 'subvariants' should be parsed in
378 # restricted mode
379 new_list += self.parse_variants(file, list[index:index+1],
380 subvariants=True,
381 prev_indent=indent)
382 list = new_list
383
384 # Parse 'include' statements
385 elif words[0] == "include":
386 if len(words) <= 1:
387 continue
388 if self.debug and not restricted:
389 self.__debug_print(indented_line,
lmr5e419062009-08-07 21:04:35 +0000390 "Entering file %s" % words[1])
lmr6f669ce2009-05-31 19:02:42 +0000391 if self.filename:
392 filename = os.path.join(os.path.dirname(self.filename),
393 words[1])
lmr33177c92009-08-07 21:45:02 +0000394 if os.path.exists(filename):
395 new_file = open(filename, "r")
396 list = self.parse(new_file, list, restricted)
397 new_file.close()
398 if self.debug and not restricted:
399 self.__debug_print("", "Leaving file %s" % words[1])
400 else:
lmra89bdac2009-08-10 14:01:26 +0000401 logging.warning("Cannot include %s -- file not found",
402 filename)
lmr6f669ce2009-05-31 19:02:42 +0000403 else:
lmra89bdac2009-08-10 14:01:26 +0000404 logging.warning("Cannot include %s because no file is "
405 "currently open", words[1])
lmr6f669ce2009-05-31 19:02:42 +0000406
407 # Parse multi-line exceptions
408 # (the block is parsed for each dict separately)
409 elif line.endswith(":"):
410 if self.debug and not restricted:
411 self.__debug_print(indented_line,
lmrf402d232009-07-27 13:16:12 +0000412 "Entering multi-line exception block "
413 "(%d dicts in current context outside "
414 "exception)" % len_list)
lmr6f669ce2009-05-31 19:02:42 +0000415 line = line.strip(":")
416 new_list = []
417 # Remember current file position
418 pos = file.tell()
419 # Read the lines in any case
420 self.parse(file, [], restricted=True, prev_indent=indent)
421 # Iterate over the list...
422 for index in range(len(list)):
423 if self.match(line, list[index]):
424 # Revert to initial file position in this
425 # exception block
426 file.seek(pos)
427 # Everything inside an exception should be parsed in
428 # restricted mode
429 new_list += self.parse(file, list[index:index+1],
lmr5e419062009-08-07 21:04:35 +0000430 restricted=True,
431 prev_indent=indent)
lmr6f669ce2009-05-31 19:02:42 +0000432 else:
433 new_list += list[index:index+1]
434 list = new_list
435
436 return list
437
438
439 def __debug_print(self, str1, str2=""):
440 """
441 Nicely print two strings and an arrow.
442
lmrf402d232009-07-27 13:16:12 +0000443 @param str1: First string
444 @param str2: Second string
lmr6f669ce2009-05-31 19:02:42 +0000445 """
446 if str2:
447 str = "%-50s ---> %s" % (str1, str2)
448 else:
449 str = str1
lmra89bdac2009-08-10 14:01:26 +0000450 logging.debug(str)
lmr6f669ce2009-05-31 19:02:42 +0000451
452
453 def __modify_list_variants(self, list, name, dep_list, add_to_shortname):
454 """
455 Make some modifications to list, as part of parsing a 'variants' block.
456
lmrf402d232009-07-27 13:16:12 +0000457 @param list: List to be processed
458 @param name: Name to be prepended to the dictionary's 'name' key
459 @param dep_list: List of dependencies to be added to the dictionary's
460 'depend' key
461 @param add_to_shortname: Boolean indicating whether name should be
462 prepended to the dictionary's 'shortname' key as well
lmr6f669ce2009-05-31 19:02:42 +0000463 """
464 for dict in list:
465 # Prepend name to the dict's 'name' field
466 dict["name"] = self.add_name(dict["name"], name)
467 # Prepend name to the dict's 'shortname' field
468 if add_to_shortname:
469 dict["shortname"] = self.add_name(dict["shortname"], name)
470 # Prepend name to each of the dict's dependencies
471 for i in range(len(dict["depend"])):
472 dict["depend"][i] = self.add_name(dict["depend"][i], name)
473 # Add new dependencies
474 dict["depend"] += dep_list
475
476
477 def __modify_list_subvariants(self, list, name, dep_list, add_to_shortname):
478 """
lmrf402d232009-07-27 13:16:12 +0000479 Make some modifications to list, as part of parsing a 'subvariants'
480 block.
lmr6f669ce2009-05-31 19:02:42 +0000481
lmrf402d232009-07-27 13:16:12 +0000482 @param list: List to be processed
483 @param name: Name to be appended to the dictionary's 'name' key
484 @param dep_list: List of dependencies to be added to the dictionary's
485 'depend' key
486 @param add_to_shortname: Boolean indicating whether name should be
487 appended to the dictionary's 'shortname' as well
lmr6f669ce2009-05-31 19:02:42 +0000488 """
489 for dict in list:
490 # Add new dependencies
491 for dep in dep_list:
492 dep_name = self.add_name(dict["name"], dep, append=True)
493 dict["depend"].append(dep_name)
494 # Append name to the dict's 'name' field
495 dict["name"] = self.add_name(dict["name"], name, append=True)
496 # Append name to the dict's 'shortname' field
497 if add_to_shortname:
498 dict["shortname"] = self.add_name(dict["shortname"], name,
499 append=True)
500
501
502if __name__ == "__main__":
lmra89bdac2009-08-10 14:01:26 +0000503 parser = optparse.OptionParser()
504 parser.add_option('-f', '--file', dest="filename", action='store_true',
505 help='path to a config file that will be parsed. '
506 'If not specified, will parse kvm_tests.cfg '
507 'located inside the kvm test dir.')
508 parser.add_option('--verbose', dest="debug", action='store_true',
509 help='include debug messages in console output')
510
511 options, args = parser.parse_args()
512 filename = options.filename
513 debug = options.debug
514
515 if not filename:
lmr6f669ce2009-05-31 19:02:42 +0000516 filename = os.path.join(os.path.dirname(sys.argv[0]), "kvm_tests.cfg")
lmra89bdac2009-08-10 14:01:26 +0000517
518 # Here we configure the stand alone program to use the autotest
519 # logging system.
520 logging_manager.configure_logging(KvmLoggingConfig(), verbose=debug)
521 list = config(filename, debug=debug).get_list()
lmr6f669ce2009-05-31 19:02:42 +0000522 i = 0
523 for dict in list:
lmra89bdac2009-08-10 14:01:26 +0000524 logging.info("Dictionary #%d:", i)
lmr6f669ce2009-05-31 19:02:42 +0000525 keys = dict.keys()
526 keys.sort()
527 for key in keys:
lmra89bdac2009-08-10 14:01:26 +0000528 logging.info(" %s = %s", key, dict[key])
lmr6f669ce2009-05-31 19:02:42 +0000529 i += 1